Search code examples
qtreeviewpyside6qabstractitemmodel

QAbstractItemModel checkboxes are not checkable


Why are the checkboxes in my TreeView not checkable? I think there is an issue with the index() and parent() methods, but do not know how to fix it. setData() also never called...

Any help is really appreciated...

import logging
import sys
from PySide6 import QtCore, QtWidgets
from PySide6.QtCore import QSortFilterProxyModel, QPersistentModelIndex

class DBObject:
    def __init__(self, name, parent, children=None, is_checkable=False):
        self.name = name
        self.parent = parent
        self.children = children or list()
        self.is_checkable = is_checkable

    def __repr__(self):
        return f"name: {self.name}, parent: {self.parent.name if self.parent is not None else '-'}"


class Model(QtCore.QAbstractItemModel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._root = DBObject("root", None)
        self.newData()
        self.checks = {}

    def checkState(self, index):
        if index in self.checks.keys():
            return self.checks[index]
        else:
            return QtCore.Qt.CheckState.Unchecked

    def newData(self):
        items = ["foo", "bar", "baz"]
        for x in items:
            child = DBObject(x + "0", self._root)
            self._root.children.append(child)
            for y in items:
                child.children.append(DBObject(y + "1", child, None, True))

    def columnCount(self, parent=QtCore.QModelIndex()):
        return 1

    def rowCount(self, parent=QtCore.QModelIndex()):
        if not parent.isValid():
            return 1

        parentItem = parent.internalPointer()
        rowCount = len(parentItem.children)
        logging.info(f"rowCount({parentItem}): rowCount={rowCount}")
        return rowCount

    def parent(self, index):
        if not index.isValid():
            return QtCore.QModelIndex()

        item = index.internalPointer()
        parentItem = item.parent

        logging.info(f"parent({item}): parent={parentItem}")
        if parentItem is None:
            return QtCore.QModelIndex()
        else:
            if parentItem.parent is None:
                return self.createIndex(0, 0, parentItem)
            else:
                return self.createIndex(parentItem.parent.children.index(parentItem), 0, parentItem)

    def index(self, row, column, parent=QtCore.QModelIndex()):
        if not parent.isValid():
            if row != 0 or column != 0:
                return QtCore.QModelIndex()
            else:
                logging.info(f"index({row}, {column}, None): index={self._root}")
                return self.createIndex(0, 0, self._root)

        parentItem = parent.internalPointer()

        if 0 <= row < len(parentItem.children):
            logging.info(f"index({row}, {column}, {parentItem}): index={parentItem.children[row]}")
            return self.createIndex(row, column, parentItem.children[row])
        else:
            logging.info(f"index({row}, {column}, {parentItem}): index=None")
            return QtCore.QModelIndex()

    def data(self, index, role=QtCore.Qt.ItemDataRole.DisplayRole):
        row = index.row()
        col = index.column()

        if not index.isValid():
            return None

        item = index.internalPointer()

        if role == QtCore.Qt.ItemDataRole.CheckStateRole and col == 0 and item.is_checkable:
            return QtCore.Qt.CheckState.Checked

        if role == QtCore.Qt.ItemDataRole.DisplayRole:
            return item.name
        else:
            return None

    def setData(self, index, value, role=QtCore.Qt.ItemDataRole.EditRole):

        if not index.isValid():
            return False
        if role == QtCore.Qt.ItemDataRole.CheckStateRole:
            self.checks[QPersistentModelIndex(index)] = value
            return True
        return False


class ProxyModel(QSortFilterProxyModel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFilterKeyColumn(0)
        self.setRecursiveFilteringEnabled(True)

    def flags(self, index):
        if not index.isValid():
            return QtCore.Qt.ItemFlag.NoItemFlags

        return (
                QtCore.Qt.ItemFlag.ItemIsEnabled
                | QtCore.Qt.ItemFlag.ItemIsSelectable)


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setMinimumSize(640, 480)

        centralWidget = QtWidgets.QWidget(self)
        self.setCentralWidget(centralWidget)

        layout = QtWidgets.QVBoxLayout(centralWidget)

        self._treeView = QtWidgets.QTreeView(self)
        layout.addWidget(self._treeView)

        self._model = Model()
        self._proxyModel = ProxyModel()
        self._proxyModel.setSourceModel(self._model)
        self._treeView.setModel(self._proxyModel)

        # self._proxyModel.setFilterFixedString("bar1")

        button = QtWidgets.QPushButton("Add")
        layout.addWidget(button)

        button.clicked.connect(self._Clicked)

    def _Clicked(self):
        self._model.newData()
        self._treeView.expandAll()


def main():
    app = QtWidgets.QApplication(sys.argv)

    mainWindow = MainWindow()
    mainWindow.show()
    app.exec()


if __name__ == "__main__":
    main()

Solution

  • There are various issues with your code:

    • the most important one is that you're not returning the ItemIsUserCheckable flag;
    • you're overriding the flag() in the proxy model (uselessly, by the way, since you're just returning the default flags of QAbstractItemModel);
    • data() always returns Checked no matter its value;
    • setData() should always emit dataChanged (as the documentation points out);
    • it's not really clear why you return a QModelIndex() if the top level item has row or column greater than 0;

    Change the flag() behavior of the source model, remove that of the proxy (or implement it properly, using the default implementation), and correct both data() and setData().

    class Model(QtCore.QAbstractItemModel):
        def __init__(self, parent=None):
            super().__init__(parent)
            self._root = DBObject("root", None)
            self.newData()
            self.checks = {}
    
        def newData(self):
            items = ["foo", "bar", "baz"]
            for x in items:
                child = DBObject(x + "0", self._root)
                self._root.children.append(child)
                for y in items:
                    child.children.append(DBObject(y + "1", child, None, True))
    
        def columnCount(self, parent=QtCore.QModelIndex()):
            return 1
    
        def flags(self, index):
            flags = super().flags(index)
            item = index.internalPointer()
            if item and item.is_checkable:
                flags |= QtCore.Qt.ItemIsUserCheckable
            return flags
    
        def rowCount(self, parent=QtCore.QModelIndex()):
            if not parent.isValid():
                return 1
    
            return len(parent.internalPointer().children)
    
        def parent(self, index):
            if not index.isValid():
                return QtCore.QModelIndex()
    
            item = index.internalPointer()
            parentItem = item.parent
    
            if parentItem is None:
                return QtCore.QModelIndex()
    
            if parentItem.parent is None:
                return self.createIndex(0, 0, parentItem)
            else:
                return self.createIndex(
                    parentItem.parent.children.index(parentItem), 0, parentItem)
    
        def index(self, row, column, parent=QtCore.QModelIndex()):
            if not parent.isValid():
                if row != 0 or column != 0:
                    return QtCore.QModelIndex()
                else:
                    return self.createIndex(0, 0, self._root)
    
            parentItem = parent.internalPointer()
    
            if 0 <= row < len(parentItem.children):
                return self.createIndex(row, column, parentItem.children[row])
    
            return QtCore.QModelIndex()
    
        def data(self, index, role=QtCore.Qt.DisplayRole):
            if not index.isValid():
                return None
    
            item = index.internalPointer()
            col = index.column()
    
            if role == QtCore.Qt.CheckStateRole and col == 0 and item.is_checkable:
                return self.checks.get(QPersistentModelIndex(index), QtCore.Qt.Unchecked)
    
            if role == QtCore.Qt.DisplayRole:
                return item.name
    
        def setData(self, index, value, role=QtCore.Qt.EditRole):
            if not index.isValid():
                return False
    
            if role == QtCore.Qt.CheckStateRole:
                pIndex = QPersistentModelIndex(index)
                if self.checks.get(pIndex) != value:
                    self.checks[pIndex] = value
                    self.dataChanged.emit(index, index)
                    return True
    
            return False
    

    Also:

    • consider that models must be as fast as possible, so you should avoid unnecessary variable definitions (unless they really help coding and readability) and improve code logic;
    • avoid unnecessary else: blocks that are implicit in the function flow (especially if used to just return None at the end of a function);