Qt endRemoveRows() 关于无效索引的警告

Qt endRemoveRows() warning about invalid index

我的程序有一个链接到允许添加和删除元素的模型的树视图。当当前选择的项目发生变化时,我必须执行与新选择的项目相关的操作(例如显示图像)。作为更改信号,我使用 selectionModel() 中的 currentChanged()。一切正常,除了一种情况:当 parent 有多个 child 并且我删除了第 0 行时,endRemoveRows() 警告我无效索引 (-1,0) 和新的 0 行视图的 未正确突出显示为选中:但是,正确执行了对该项目的操作。 更一般地说,我相信在删除一个项目后,currentChanged() 信号总是与删除前引用模型的索引一起发出,这会在我删除第一行时导致视图出现问题。

如何避免 endRemoveRows() 关于无效索引的警告并确保正确突出显示第 0 行的新项目?

模型中的三个兄弟姐妹:第 0、1、2 行的 image0、image1、image2。

最小工作示例

如果您 运行 pycharm 上的以下代码,请转到编辑配置并激活“在输出控制台中模拟终端”。

from PyQt5.QtGui import QStandardItemModel
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtGui import QStandardItem
from PyQt5.QtCore import Qt
from PyQt5 import QtWidgets, uic, QtCore



class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(800, 600)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.treeView = QtWidgets.QTreeView(self.centralwidget)
        self.treeView.setGeometry(QtCore.QRect(140, 100, 241, 391))
        self.treeView.setObjectName("treeView")
        self.pushButton = QtWidgets.QPushButton(self.centralwidget)
        self.pushButton.setGeometry(QtCore.QRect(180, 60, 151, 23))
        self.pushButton.setObjectName("pushButton")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 21))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.pushButton.setText(_translate("MainWindow", "deleteSelectedItem"))

    def mySetup(self):
        self.myModel = QStandardItemModel(0,1)
        self.myModel.setHeaderData(0, Qt.Horizontal, "File name")
        self.myModel.parent = self
        image0 = QStandardItem()
        image0.setText("image0.png")
        image1 = QStandardItem()
        image1.setText("image1.png")
        image2 = QStandardItem()
        image2.setText("image2.png")

        folder = QStandardItem()
        folder.setText("folderA")
        folder.appendRows([image0,image1,image2])
        self.myModel.appendRow(folder)

        self.treeView.setModel(self.myModel)
        self.treeView.selectionModel().currentChanged.connect(self.model_current_item_changed)
        self.pushButton.clicked.connect(self.erase_selected_treeitem)



    def model_current_item_changed(self, new, old):
        print("new:", new.row())
        print("old", old.row())
        #print("current:", self.treeView.selectionModel().currentIndex().row())

        if new.isValid() and not self.treeView.model().itemFromIndex(new).hasChildren():  # show item image
            print("Is image:" , self.treeView.model().itemFromIndex(new).text())
        else:
            print("Is Folder:", self.treeView.model().itemFromIndex(new).text())

    def erase_selected_treeitem(self):
        index = self.treeView.selectionModel().currentIndex()
        if index is not None and index.isValid():
            parent = index.parent()
            self.myModel.beginRemoveRows(parent, index.row(), index.row())
            self.myModel.removeRow(index.row(), parent)
            self.myModel.endRemoveRows()



if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    ui.mySetup()
    MainWindow.show()
    sys.exit(app.exec_())

我找到了问题的答案。在上面的示例中,endRemoveRows() 在以下两种情况发生时发出有关无效索引的警告:

  1. 您想删除第 0 行子项(示例中的 Image0)
  2. 在视图中或关联的 view.selectionModel()
  3. 中选择了任何项目

selectionModel 包含当前所选项目的索引。当执行修改链接到视图的模型的功能(例如删除行)时,该索引不会自动更新并且可能变得无效。在删除项目之前显式调用 selectionModel().clear() 可解决问题。

def erase_selected_treeitem(self):
    index = self.treeView.selectionModel().currentIndex()
    if index is not None and index.isValid():
        parent = index.parent()
        self.treeView.selectionModel().clear()
        self.myModel.beginRemoveRows(parent, index.row(), index.row())
        self.myModel.removeRow(index.row(), parent)
        self.myModel.endRemoveRows()

def model_current_item_changed(self, new, old):
        print("new:", new.row())
        print("old", old.row())
    

如您所见,model_current_item_changed(self, new, old) 也发生了变化:这是因为使用新的 erase_selected_treeitem 时,当执行 currentChanged() 时,与已删除项目关联的 QModelIndex 不存在,不即使在 selectionModel 中:由于这样做,要求模型 return 与该索引关联的对象将 return None

编辑

Since no item in the view is selected, every click I make produces a currentChanged(). So if I click on the new 0 row I get that the related print is executed again

这可以通过以下代码避免

def erase_selected_treeitem(self):
    index = self.treeView.selectionModel().currentIndex()
    row =index.row()
    if index is not None and index.isValid():
        parent = index.parent()
        self.treeView.selectionModel().clear()
        self.myModel.beginRemoveRows(parent, index.row(), index.row())
        self.myModel.removeRow(index.row(), parent)
        self.myModel.endRemoveRows()

        #select sibling of the deleted item
        if index.siblingAtRow(index.row()-1).isValid():
            self.treeView.selectionModel().setCurrentIndex(index.siblingAtRow(index.row()-1), QItemSelectionModel.ClearAndSelect)
        elif index.siblingAtRow(index.row()).isValid():
            self.treeView.selectionModel().setCurrentIndex(index.siblingAtRow(index.row()), QItemSelectionModel.ClearAndSelect)

目前所选项目的颜色为灰色而不是青色:我会在解决后进行编辑,但这是次要方面。题目是去掉invalid index warning自动选择一行,不会重复执行代码,这个答案的代码符合我的要求。