Search code examples
pythonpyqtmvpqtreeviewqabstractitemmodel

QTreeView requests index for invalid row


Have a look at the following MWE.

It is a simple QAbstractItemModel with only a single level, storing its items in a list. I create a QTreeView to display the model, and a button to remove the 2nd item.

from PyQt5.QtCore import QModelIndex, QAbstractItemModel, Qt
from PyQt5.QtWidgets import QTreeView, QApplication, QPushButton


class Item:
    def __init__(self, title):
        self.title = title


class TreeModel(QAbstractItemModel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._items = []  # typing.List[Item]

    def addItem(self, item: Item):
        self.beginInsertRows(QModelIndex(), len(self._items), len(self._items))
        self._items.append(item)
        self.endInsertRows()

    def removeItem(self, item: Item):
        index = self._items.index(item)
        self.beginRemoveRows(QModelIndex(), index, index)
        self._items.remove(item)
        self.endRemoveRows()

    # ----- overridden methods from QAbstractItemModel -----

    # noinspection PyMethodOverriding
    def data(self, index: QModelIndex, role):
        item = index.internalPointer()
        if role == Qt.DisplayRole:
            return item.title

    # noinspection PyMethodOverriding
    def rowCount(self, parent=QModelIndex()):
        if not parent.isValid():
            return len(self._items)
        return 0

    # noinspection PyMethodOverriding
    def columnCount(self, parent=QModelIndex()):
        return 1

    # noinspection PyMethodOverriding
    def index(self, row: int, col: int, parent=QModelIndex()):
        assert not parent.isValid()
        return self.createIndex(row, 0, self._items[row])

    def parent(self, index=QModelIndex()):
        return QModelIndex()


def removeItem():
    model.removeItem(item2)


if __name__ == '__main__':
    app = QApplication([])
    model = TreeModel()
    button = QPushButton('Delete')
    button.clicked.connect(removeItem)
    button.show()
    item1 = Item('Item 1')
    model.addItem(item1)
    item2 = Item('Item 2')
    model.addItem(item2)
    treeView = QTreeView()
    treeView.setModel(model)
    treeView.show()

    app.exec()

As far as I can tell, the implementation of my model is correct (though very basic). In particular, the row, and column counts it reports are correct, and it never creates indices for data that would not be valid.

Steps to reproduce my issue:

  • Run the code above.
  • In the tree view, select Item 2.
  • Press the Delete button.

On my system, the application crashes in beginRemoveRows(), because the view requests a QModelIndex for row 2. Naturally, row 2 does not exist.

Any idea why the QTreeView would think there would be 3 rows, when the model explicitly reports there are only 2?


Solution

  • When an item is added, moved, removed, etc, what the model does is verify the QPersistentModelIndex are valid or not, so it calls the index() method of QAbstractItemModel. And in that method it is the developer's responsibility to verify if the row or column is valid, and for that the model provides the hasIndex() method that you do not use causing the error you point out, so the solution is:

    def index(self, row: int, col: int, parent=QModelIndex()):
        if not self.hasIndex(row, col, parent):
            return QModelIndex()
        assert not parent.isValid()
        return self.createIndex(row, 0, self._items[row])