如何无延迟地从绑定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
相关产品推荐
相关产品推荐

