Search code examples
pythonpyqtpyqt5qtreeviewqabstractitemmodel

how to show the proper number of columns in a QTreeView for a model with different hierarchical column counts


In the code example below I populate an item model with a set of top level items which contain key-value properties that I can view and edit with a QTreeView.

Looking at the Qt Documentation for QAbstractItemModel::columnCount it says that this should return the number of columns for the children of the given parent, meaning that this should be a hierarchical dependent property.

However, using the code below, if I return the column count as a hierarchical dependent property (in this case root->children have 1 column, root->child->children have 2 columns) then the view will only display 1 column.

enter image description here

Printing node.columnCount() (see the code) will actually show that the Item class nodes do in fact return columnCount = 2 after you expand one of the items.

If I just always return 2 for the model.columnCount function, then the view will properly display both columns.

enter image description here

Is this required to always return the desired number of columns in a view no matter the hierarchy or am I just doing something wrong and if so what? Returning a number of columns for a parent whose children have a different number of columns just to make the view work properly feels like it must be wrong.

import sys
import typing
from PyQt5 import QtCore, QtWidgets


class Node:
    def __init__(self, parent=None):
        self.parent = parent  # type: Node
        self.name : str

    def children(self) -> list:
        return None

    def hasChildren(self):
        return bool(self.children())

    def getData(self, index: QtCore.QModelIndex):
        if index.column() == 0:
            return self.name

    def setData(self, val, index: QtCore.QModelIndex):
        if index.column() == 0:
            self.name = val

    def columnCount(self):
        return 1

    def rowCount(self):
        children = self.children()
        return 0 if not children else len(children)

    def flags(self, index: QtCore.QModelIndex):
        if index.column() == 0:
            return (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable)
        else:
            return QtCore.Qt.NoItemFlags


class Property(Node):
    def __init__(self, parent, label, value):
        super().__init__(parent)
        self.label = label
        self.value = value

    def getData(self, index: QtCore.QModelIndex):
        col = index.column()
        if col == 0:
            return self.label
        elif col == 1:
            return self.value

    def setData(self, val, index: QtCore.QModelIndex):
        if index.column() == 1:
            self.value = val

    def flags(self, index: QtCore.QModelIndex):
        col = index.column()
        if col == 0:
            return QtCore.Qt.ItemIsEnabled
        elif col == 1:
            return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEditable


class Item(Node):
    def __init__(self, parent):
        super().__init__(parent)
        self.name = 'Item'
        self.p1 = Property(self, 'string', 'text')
        self.p2 = Property(self, 'float', 1.2)

    def children(self):
        return [self.p1, self.p2]

    def columnCount(self):
        return 2


class Root(Node):
    def __init__(self):
        super().__init__(parent=None)
        self._children = list()

    def children(self):
        return self._children


class Model(QtCore.QAbstractItemModel):
    def __init__(self):
        super().__init__()
        self.root = Root()

    def index(self, row: int, column: int, parent: QtCore.QModelIndex = ...) -> QtCore.QModelIndex:
        if not self.hasIndex(row, column, parent):
            return QtCore.QModelIndex()

        node = parent.internalPointer() if parent.isValid() else self.root
        if node.children:
            return self.createIndex(row, column, node.children()[row])
        else:
            return QtCore.QModelIndex()

    def parent(self, child: QtCore.QModelIndex) -> QtCore.QModelIndex:
        if not child.isValid():
            return QtCore.QModelIndex()

        node = child.internalPointer()  # type: Node

        if node.parent and node.parent.parent:
            row = node.parent.parent.children().index(node.parent)
            return self.createIndex(row, 0, node.parent)
        else:
            return QtCore.QModelIndex()

    def rowCount(self, parent: QtCore.QModelIndex = ...) -> int:
        node = parent.internalPointer() if parent.isValid() else self.root
        children = node.children()
        return len(children) if children else 0

    def columnCount(self, parent: QtCore.QModelIndex = ...) -> int:
        node = parent.internalPointer() if parent.isValid() else self.root
        print(f'{node.__class__.__name__} column count: ', node.columnCount())  # shows that column count 2 is returned, when items are expanded
        # return 2  # 2nd column only shows up if I just always return 2
        return node.columnCount()  # view only shows 1 columns

    def hasChildren(self, parent: QtCore.QModelIndex = ...) -> bool:
        node = parent.internalPointer() if parent.isValid() else self.root
        return node.hasChildren()

    def data(self, index: QtCore.QModelIndex, role: int = ...):
        if index.isValid() and role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
            node = index.internalPointer()  # type: Node
            return node.getData(index)
        else:
            return None

    def setData(self, index: QtCore.QModelIndex, value: typing.Any, role: int = ...) -> bool:
        if role in (QtCore.Qt.EditRole,):
            node = index.internalPointer()  # type: Node
            node.setData(value, index)
            self.dataChanged.emit(index, index)
            return True
        else:
            return False

    def flags(self, index: QtCore.QModelIndex):
        node = index.internalPointer() if index.isValid() else self.root
        return node.flags(index)

    def appendRow(self, item):
        row = len(self.root.children())
        self.beginInsertRows(QtCore.QModelIndex(), row, row)
        self.root.children().append(item)
        self.endInsertRows()


class TreeView(QtWidgets.QTreeView):
    def __init__(self, parent=None):
        super(TreeView, self).__init__(parent)
        self._model = Model()
        self.setModel(self._model)
        self.setSelectionMode(self.ExtendedSelection)
        # self.setDropIndicatorShown(False)
        self.setEditTriggers(self.DoubleClicked | self.SelectedClicked | self.EditKeyPressed)

    def model(self) -> Model:
        return self._model

sys.excepthook = sys.__excepthook__
app = QtWidgets.QApplication(sys.argv)
widget = TreeView()
model = widget.model()
for i in range(2):
    model.appendRow(Item(model.root))
widget.show()
widget.setAttribute(QtCore.Qt.WA_DeleteOnClose)
sys.exit(app.exec_())

Solution

  • It seems that the docs is not clear and does not exactly match the implementation, in the implementation the number of columns in the view is dependent on the horizontal QHeaderView, and the horizontal QHeaderView uses the number of columns of the root that is the invisible item, ie the number of columns should be given by Root(), and since Root() does not overwrite the columnCount() it will have the value of 1 by default (although for me the columnCount() of Node must be 0 and children() must return a empty list), so the solution is set to 2 in Root columnCount().

    import sys
    import typing
    from PyQt5 import QtCore, QtWidgets
    
    
    class Node:
        def __init__(self, parent=None):
            self.parent = parent  # type: Node
            self.name : str
    
        def children(self) -> list:
            return list()
    
        def hasChildren(self):
            return bool(self.children())
    
        def getData(self, index: QtCore.QModelIndex):
            if index.column() == 0:
                return self.name
    
        def setData(self, val, index: QtCore.QModelIndex):
            if index.column() == 0:
                self.name = val
    
        def columnCount(self):
            return 0
    
        def rowCount(self):
            children = self.children()
            return len(children)
    
        def flags(self, index: QtCore.QModelIndex):
            if index.column() == 0:
                return (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable)
            else:
                return QtCore.Qt.NoItemFlags
    
    
    class Property(Node):
        def __init__(self, parent, label, value):
            super().__init__(parent)
            self.label = label
            self.value = value
    
        def getData(self, index: QtCore.QModelIndex):
            col = index.column()
            if col == 0:
                return self.label
            elif col == 1:
                return self.value
    
        def setData(self, val, index: QtCore.QModelIndex):
            if index.column() == 1:
                self.value = val
    
        def flags(self, index: QtCore.QModelIndex):
            col = index.column()
            if col == 0:
                return QtCore.Qt.ItemIsEnabled
            elif col == 1:
                return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEditable
    
        def columnCount(self):
            return 1
    
    
    class Item(Node):
        def __init__(self, parent):
            super().__init__(parent)
            self.name = 'Item'
            self.p1 = Property(self, 'string', 'text')
            self.p2 = Property(self, 'float', 1.2)
    
        def children(self):
            return [self.p1, self.p2]
    
        def columnCount(self):
            return 2
    
    
    class Root(Node):
        def __init__(self):
            super().__init__(parent=None)
            self._children = list()
    
        def children(self):
            return self._children
    
        def columnCount(self):
            return 2
    
    
    class Model(QtCore.QAbstractItemModel):
        def __init__(self):
            super().__init__()
            self.root = Root()
    
        def index(self, row: int, column: int, parent: QtCore.QModelIndex = ...) -> QtCore.QModelIndex:
            if not self.hasIndex(row, column, parent):
                return QtCore.QModelIndex()
    
            node = parent.internalPointer() if parent.isValid() else self.root
            if node.children:
                return self.createIndex(row, column, node.children()[row])
            else:
                return QtCore.QModelIndex()
    
        def parent(self, child: QtCore.QModelIndex) -> QtCore.QModelIndex:
            if not child.isValid():
                return QtCore.QModelIndex()
    
            node = child.internalPointer()  # type: Node
    
            if node.parent and node.parent.parent:
                row = node.parent.parent.children().index(node.parent)
                return self.createIndex(row, 0, node.parent)
            else:
                return QtCore.QModelIndex()
    
        def rowCount(self, parent: QtCore.QModelIndex = ...) -> int:
            node = parent.internalPointer() if parent.isValid() else self.root
            children = node.children()
            return len(children) if children else 0
    
        def columnCount(self, parent: QtCore.QModelIndex = ...) -> int:
            node = parent.internalPointer() if parent.isValid() else self.root
            print(f'{node.__class__.__name__} column count: ', node.columnCount())  # shows that column count 2 is returned, when items are expanded
            # return 2  # 2nd column only shows up if I just always return 2
            return node.columnCount()  # view only shows 1 columns
    
        def hasChildren(self, parent: QtCore.QModelIndex = ...) -> bool:
            node = parent.internalPointer() if parent.isValid() else self.root
            return node.hasChildren()
    
        def data(self, index: QtCore.QModelIndex, role: int = ...):
            if index.isValid() and role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
                node = index.internalPointer()  # type: Node
                return node.getData(index)
            else:
                return None
    
        def setData(self, index: QtCore.QModelIndex, value: typing.Any, role: int = ...) -> bool:
            if role in (QtCore.Qt.EditRole,):
                node = index.internalPointer()  # type: Node
                node.setData(value, index)
                self.dataChanged.emit(index, index)
                return True
            else:
                return False
    
        def flags(self, index: QtCore.QModelIndex):
            node = index.internalPointer() if index.isValid() else self.root
            return node.flags(index)
    
        def appendRow(self, item):
            row = len(self.root.children())
            self.beginInsertRows(QtCore.QModelIndex(), row, row)
            self.root.children().append(item)
            self.endInsertRows()
    
    
    class TreeView(QtWidgets.QTreeView):
        def __init__(self, parent=None):
            super(TreeView, self).__init__(parent)
            self._model = Model()
            self.setModel(self._model)
            self.setSelectionMode(self.ExtendedSelection)
            # self.setDropIndicatorShown(False)
            self.setEditTriggers(self.DoubleClicked | self.SelectedClicked | self.EditKeyPressed)
    
        def model(self) -> Model:
            return self._model
    
    sys.excepthook = sys.__excepthook__
    app = QtWidgets.QApplication(sys.argv)
    widget = TreeView()
    model = widget.model()
    for i in range(2):
        model.appendRow(Item(model.root))
    widget.show()
    widget.setAttribute(QtCore.Qt.WA_DeleteOnClose)
    sys.exit(app.exec_())