My program has a tree view linked to a model that allows the addition and deletion of elements. When the currently selected item changes I have to perform operations related to the new selected item (such as show an image). As change signal I use the currentChanged()
from selectionModel()
. Everything works fine except in one case: when a parent has more than one child and I remove row 0, endRemoveRows() warns me about an invalid index (-1,0) and the new 0 row of the view is not correctly highlighted as selected: however, the operations on that item are correctly executed.
More in general, I believe that after the deletion of an item the currentChanged() signal is always emitted with indexes that refer to the model before the deletion and this causes problems in the view when I delete the first line.
How can I avoid endRemoveRows() warning about an invalid index and make sure that the new item at row 0 is correctly highlighted?
Three siblings in the model: image0, image1 , image2 at rows 0,1,2.
Minimal working example
If you run the following code on pycharm, go to edit configuration and activate "emulate terminal in output console".
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_())
I found the answer to my question. In the example above, endRemoveRows()
emits warning about invalid index when both of the following conditions occur:
view.selectionModel()
The selectionModel contains an index of the currently selected item. When a function that modifies the model linked to the view (such as removing rows) is performed, that index is not automatically updated and may become invalid. Explicitly invoking selectionModel().clear()
before removing the item resolves the problem.
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())
As you can see, also model_current_item_changed(self, new, old)
has changed: this is because with the new erase_selected_treeitem
when currentChanged()
is executed the QModelIndex associated with the deleted item does not exist, not even in selectionModel
: due do this, asking the model to return an object associated with that index would return None
Edit
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
This can be avoided with the following code
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)
At the moment the selected item is coloured in grey and not cyan: I'll edit when I've solved it, but it's a secondary aspect. The question was about removing the invalid index warning and automatically selecting a row without the possibility of double execution of the code, and the code in this answer meets my requirements.