pythonqtmodel-view-controllerpyqtqabstractitemmodel

Qt and Python - QIdentityProxyModel does not get the right column count when nested on top of a QSortFilterProxyModel


I have three models, a QAbstractItemModel "sourceModel", QSortFilterProxyModel "proxyModel" for filtering, and a QIdentityProxyModel "dataModel" for modifying columns and displayed data.

When I only layer QAbstractItemModel -> QIdentityProxyModel , it all works well. My source model only has one default column given, I can add multiple new columns on my QIdentifyProxyModel, and my view displays all the right data. This allows me to reuse the same model on multiple views with different data, great.

However, if I layer QAbstractItemModel -> QSortFilterProxyModel -> QIdentityProxyModel, then my view only shows data in the first column, and the selection model breaks. If I define an equal or higher amount of columns in my source model, then all the QIdentityProxyModel columns behaves correctly in my view, and shows different data than what I defined in my source model.

When proxies are nested, the QIdentityProxyModel class can still access the source item with the data, but the index passed to the QIdentityProxyModel's data-function only queries the amount columns defined in the source model.

Any idea how to go about this? Any help is much appreciated!

from PySide2 import QtCore, QtWidgets, QtGui

class Item(dict):
    def __init__(self, data=None):
        super(Item, self).__init__()

        self._children = list()
        self._parent = None
        if data:
            self.update(data)

    def childCount(self):
        return len(self._children)

    def child(self, row):
        if row >= len(self._children):
            return
        return self._children[row]

    def children(self):
        return self._children

    def parent(self):
        return self._parent

    def row(self):
        if self._parent is not None:
            siblings = self.parent().children()
            return siblings.index(self)

    def add_child(self, child):
        child._parent = self
        self._children.append(child)

class TreeModel(QtCore.QAbstractItemModel):
    Columns = list()
    ItemRole = QtCore.Qt.UserRole + 1

    def __init__(self, parent=None):
        super(TreeModel, self).__init__(parent)
        self._root_item = Item()

    def rowCount(self, parent):
        if parent.isValid():
            item = parent.internalPointer()
        else:
            item = self._root_item
        return item.childCount()

    def columnCount(self, parent):
        return len(self.Columns)

    def setColumns(self, keys):
        assert isinstance(keys, (list, tuple))
        self.Columns = keys

    def data(self, index, role):
        if not index.isValid():
            return None
        if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
            item = index.internalPointer()
            column = index.column()

            key = self.Columns[column]
            return item.get(key, None)

        if role == self.ItemRole:
            return index.internalPointer()

    def headerData(self, section, orientation, role):
        if role == QtCore.Qt.DisplayRole:
            if section < len(self.Columns):
                return self.Columns[section]
        super(TreeModel, self).headerData(section, orientation, role)


    def parent(self, index):
        item = index.internalPointer()
        parent_item = item.parent()

        if parent_item == self._root_item or not parent_item:
            return QtCore.QModelIndex()

        return self.createIndex(parent_item.row(), 0, parent_item)

    def index(self, row, column, parent):
        if not parent.isValid():
            parent_item = self._root_item
        else:
            parent_item = parent.internalPointer()

        child_item = parent_item.child(row)
        if child_item:
            return self.createIndex(row, column, child_item)
        else:
            return QtCore.QModelIndex()

    def add_child(self, item, parent=None):
        if parent is None:
            parent = self._root_item
        parent.add_child(item)

    def column_name(self, column):
        if column < len(self.Columns):
            return self.Columns[column]

    def clear(self):
        self.beginResetModel()
        self._root_item = Item()
        self.endResetModel()


class CustomSortFilterProxyModel(QtCore.QSortFilterProxyModel):
    def __init__(self, parent=None):
        super(CustomSortFilterProxyModel, self).__init__(parent)

    def filterAcceptsRow(self, row, parent):
        model = self.sourceModel()
        index = model.index(row, self.filterKeyColumn(), parent)
        item = index.internalPointer()
        if item.get('name'):
            return True
        else:
            return False


class IdentityProxyModel(QtCore.QIdentityProxyModel):
    def __init__(self, *args, **kwargs):
        super(IdentityProxyModel, self).__init__(*args, **kwargs)
        self.Columns = []
        self._root_item = Item()

    def setColumns(self, keys):
        assert isinstance(keys, (list, tuple))
        self.Columns = keys
    #
    def columnCount(self, parent):
        # return 3
        return len(self.Columns)

    def column_name(self, column):
        if column < len(self.Columns):
            return self.Columns[column]

    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
        if role == QtCore.Qt.DisplayRole:
            if section < len(self.Columns):
                return self.Columns[section]
        super(IdentityProxyModel, self).headerData(section, orientation, role)

    def data(self, index, role):
        if not index.isValid():
            return
        if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
            item = self.mapToSource(index).data(TreeModel.ItemRole)
            column = index.column()

            key = self.Columns[column]
            return item.get(key, None)

        return super(IdentityProxyModel, self).data(index, role)


if __name__ == '__main__':
    import sys

    sourceModel = TreeModel()
    sourceModel.setColumns(['name'])
    sourceModel.add_child(Item({'name': 'itemA', 'number': '1', 'info': 'A'}))
    sourceModel.add_child(Item({'name': 'itemB', 'number': '2', 'info': 'B'}))

    proxyModel = CustomSortFilterProxyModel()
    proxyModel.setSourceModel(sourceModel)

    dataModel = IdentityProxyModel()
    dataModel.setSourceModel(proxyModel)
    dataModel.setColumns(['name', 'info'])

    app = QtWidgets.QApplication(sys.argv)
    view = QtWidgets.QTreeView()
    view.setModel(dataModel)
    view.show()
    sys.exit(app.exec_())

Solution

  • Tree models are tricky. A lot.

    Whenever you have to deal with them, you have to really understand recursion. And with Qt models (which are somehow complex) that becomes even harder, and often a cause of massive headaches and late nights.

    I am basing the following code on a previous answer of mine which is very basic and doesn't consider the possibility of having inconsistent number of child columns.

    The concept is that proxy models that are based on tree structures need to keep track of the hierarchy (which is usually based on the parent index internal pointer or id), and that must be considered whenever "ghost" columns are created.

    All of the above is always very important. Item views need all of the implementation in order to properly update their contents and allow valid user interaction.

    class IdentityProxyModel(QtCore.QIdentityProxyModel):
        def __init__(self, *args, **kwargs):
            super(IdentityProxyModel, self).__init__(*args, **kwargs)
            self.Columns = []
            self._parents = {}
            self._root_item = Item()
    
        # ...
        def _isInvalid(self, column):
            # we assume that the model always has the same column count
            return column > self.sourceModel().columnCount() - 1
    
        def mapToSource(self, index):
            if self._isInvalid(index.column()):
                index = index.sibling(index.row(), 0)
            return super().mapToSource(index)
    
        def index(self, row, column, parent=QtCore.QModelIndex()):
            if self._isInvalid(column):
                index = self.createIndex(row, column, parent.internalId())
                self._parents[index] = parent
                return index
            return super().index(row, column, parent)
    
        def parent(self, index):
            if self._isInvalid(index.column()):
                return self._parents[index]
            return super().parent(index)
    
        def flags(self, index):
            if self._isInvalid(index.column()):
                return self.flags(index.sibling(index.row(), 0))
            return super().flags(index)
    
        def sibling(self, row, column, idx):
            if self._isInvalid(column):
                return self.index(row, column, idx.parent())
            elif self._isInvalid(idx.column()):
                idx = self.index(idx.row(), 0, idx.parent())
            return super().sibling(row, column, idx)
    

    Note: overrides should always use the signature of their base implementation. For instance, both rowCount() and columnCount() must accept an invalid keyword parent argument, which is not used for "fast calls" on the top level of the model. The common practice is to use a basic QModelIndex instance, since it is fundamentally an immutable object (but None is commonly accepted too):

        def rowCount(self, parent=QtCore.QModelIndex()):
            # ...
    
        def columnCount(self, parent=QtCore.QModelIndex()):
            # ...