You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何无延迟地从绑定QListView的QAbstractListModel中正确删除行?

问题描述

我用PyQT6开发了一款图片浏览应用,支持带缩略图的图片列表浏览、查看选中图片,以及添加/删除图片功能。添加功能正常,但删除图片时,QListView里被删的行会先显示下一行的数据,要等半秒到5秒的随机延迟后才会真正移除,列表顺序才恢复正确。我觉得是自己从模型删除项的方式不对,希望删除操作能即时生效。

以下是最小复现代码:

import PyQt6 as qt
import PyQt6.QtCore as QtCore
from PyQt6.QtCore import Qt, QAbstractListModel, QModelIndex
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
import os
import sys
import traceback

class ImageDataModel(QAbstractListModel):
    def __init__(self, images=None):
        super(ImageDataModel, self).__init__()
        if images is None:
            self.images = []
        else:
            self.images = images
            self.thumbnails = []
            for img_path in self.images:
                icon = QPixmap(img_path).scaledToHeight(20)
                self.thumbnails.append(icon)
            
    def data(self, index, role):
        if role == Qt.ItemDataRole.DisplayRole:
            img_path = self.images[index.row()]
            return img_path
        if role == Qt.ItemDataRole.DecorationRole:
            thumbnail = self.thumbnails[index.row()]
            return thumbnail
        
    def rowCount(self, index):
        return len(self.images)
    
    def removeRow(self, index):
        self.images.pop(index)
        self.thumbnails.pop(index)
        
class myListView(QListView):
    def __init__(self, parent=None):
        super().__init__()
        self.parent = parent
        self.setSelectionMode(QListView.SelectionMode.ExtendedSelection)
        
    def currentChanged(self, current: QtCore.QModelIndex, previous: QtCore.QModelIndex) -> None:
        if (current.row() >= 0):
            self.parent.get_selection(current)
        return super().currentChanged(current, previous)
    
class MyMenu(QMainWindow):
    def __init__(self):
        super().__init__()
        self.layout = QHBoxLayout()
        self.list = myListView(self)
        try:
            image_file_list = [x for x in os.listdir('path/to/image/directory') if x.lower().endswith(".png")]
        except:
            image_file_list = []

        image_file_list.sort()
        self.model = ImageDataModel(image_file_list)
        self.list.setModel(self.model)
        self.list.clicked.connect(self.get_selection)
        self.list.setCurrentIndex(self.model.index(0,0))
        
        self.layout.addWidget(self.list)
        
        self.widget = QWidget()
        self.widget.setLayout(self.layout)
        self.setCentralWidget(self.widget)

    def delete_image(self):
        if self.list.currentIndex().row() != 0:
            new_index = self.list.currentIndex().row() - 1
        else:
            new_index = 0

        try:
            self.list.model().removeRow(self.list.currentIndex().row())
            os.remove(self.img_path)
            
            index = self.model.createIndex(new_index, 0)
            self.list.setCurrentIndex(index)
        except:
            traceback.print_exc()
            
    def get_selection(self, item):
        print(item.row())
        
    def keyPressEvent(self, e) -> None:
        global model_enabled
        if (e.key() == Qt.Key.Key_Escape):
            app.quit()
 
        if (e.key() == Qt.Key.Key_Delete):
            self.delete_image()
        
def main():
    app = QApplication(sys.argv)
    window = MyMenu()
    window.show()
    app.exec()
    
main()
解决方案

核心问题是PyQt的模型视图架构要求修改模型数据后必须发出信号通知视图更新,你当前的removeRow方法只修改了数据列表,没有通知视图,导致视图无法即时同步。

1. 修正模型的removeRow方法

必须用beginRemoveRows和endRemoveRows包裹数据修改操作,这两个方法会自动触发视图更新的信号:

def removeRow(self, row, parent=QModelIndex()):
    # 通知视图即将移除行
    self.beginRemoveRows(parent, row, row)
    self.images.pop(row)
    self.thumbnails.pop(row)
    # 通知视图移除操作完成
    self.endRemoveRows()
    return True  # 返回操作是否成功

注意:原方法参数名index容易和QModelIndex混淆,改为row更准确,符合QAbstractListModel的方法定义。

2. 完善删除逻辑中的索引处理

删除最后一行时要避免设置无效索引,同时补全self.img_path的定义:

def get_selection(self, item):
    print(item.row())
    # 保存当前选中的图片路径,用于删除文件
    self.img_path = self.model.images[item.row()]

def delete_image(self):
    current_row = self.list.currentIndex().row()
    total_rows = len(self.model.images)
    
    # 计算删除后的选中索引:如果不是第一行且还有剩余行,选上一行;否则选第一行(如果还有行)
    if current_row > 0 and total_rows > 1:
        new_index = current_row - 1
    else:
        new_index = 0 if total_rows > 1 else -1

    try:
        self.model.removeRow(current_row)
        os.remove(self.img_path)
        
        # 只有还有行时才设置选中索引
        if new_index != -1:
            index = self.model.index(new_index, 0)
            self.list.setCurrentIndex(index)
    except:
        traceback.print_exc()

3. 修正图片路径问题

原代码中只获取了文件名,没有完整路径,导致加载缩略图和删除文件出错,修改初始化部分:

try:
    image_dir = 'path/to/image/directory'
    # 生成完整图片路径
    image_file_list = [os.path.join(image_dir, x) for x in os.listdir(image_dir) if x.lower().endswith(".png")]
except:
    image_file_list = []
完整修正后的代码
import PyQt6 as qt
import PyQt6.QtCore as QtCore
from PyQt6.QtCore import Qt, QAbstractListModel, QModelIndex
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
import os
import sys
import traceback

class ImageDataModel(QAbstractListModel):
    def __init__(self, images=None):
        super(ImageDataModel, self).__init__()
        if images is None:
            self.images = []
        else:
            self.images = images
            self.thumbnails = []
            for img_path in self.images:
                icon = QPixmap(img_path).scaledToHeight(20)
                self.thumbnails.append(icon)
            
    def data(self, index, role):
        if role == Qt.ItemDataRole.DisplayRole:
            img_path = self.images[index.row()]
            return img_path
        if role == Qt.ItemDataRole.DecorationRole:
            thumbnail = self.thumbnails[index.row()]
            return thumbnail
        
    def rowCount(self, index):
        return len(self.images)
    
    def removeRow(self, row, parent=QModelIndex()):
        self.beginRemoveRows(parent, row, row)
        self.images.pop(row)
        self.thumbnails.pop(row)
        self.endRemoveRows()
        return True
        
class myListView(QListView):
    def __init__(self, parent=None):
        super().__init__()
        self.parent = parent
        self.setSelectionMode(QListView.SelectionMode.ExtendedSelection)
        
    def currentChanged(self, current: QtCore.QModelIndex, previous: QtCore.QModelIndex) -> None:
        if (current.row() >= 0):
            self.parent.get_selection(current)
        return super().currentChanged(current, previous)
    
class MyMenu(QMainWindow):
    def __init__(self):
        super().__init__()
        self.layout = QHBoxLayout()
        self.list = myListView(self)
        try:
            image_dir = 'path/to/image/directory'
            image_file_list = [os.path.join(image_dir, x) for x in os.listdir(image_dir) if x.lower().endswith(".png")]
        except:
            image_file_list = []

        image_file_list.sort()
        self.model = ImageDataModel(image_file_list)
        self.list.setModel(self.model)
        self.list.clicked.connect(self.get_selection)
        if self.model.rowCount(QModelIndex()) > 0:
            self.list.setCurrentIndex(self.model.index(0,0))
        
        self.layout.addWidget(self.list)
        
        self.widget = QWidget()
        self.widget.setLayout(self.layout)
        self.setCentralWidget(self.widget)

    def delete_image(self):
        current_row = self.list.currentIndex().row()
        total_rows = len(self.model.images)
        
        if current_row > 0 and total_rows > 1:
            new_index = current_row - 1
        else:
            new_index = 0 if total_rows > 1 else -1

        try:
            self.model.removeRow(current_row)
            os.remove(self.img_path)
            
            if new_index != -1:
                index = self.model.index(new_index, 0)
                self.list.setCurrentIndex(index)
        except:
            traceback.print_exc()
            
    def get_selection(self, item):
        print(item.row())
        self.img_path = self.model.images[item.row()]
        
    def keyPressEvent(self, e) -> None:
        if (e.key() == Qt.Key.Key_Escape):
            app.quit()
 
        if (e.key() == Qt.Key.Key_Delete):
            self.delete_image()
        
def main():
    app = QApplication(sys.argv)
    window = MyMenu()
    window.show()
    app.exec()
    
main()

内容的提问来源于stack exchange,提问作者harke

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 07:55:20