Python – How to display the correct number of columns in QTreeView for models with different hierarchical column counts

How to display the correct number of columns in QTreeView for models with different hierarchical column counts… here is a solution to the problem.

How to display the correct number of columns in QTreeView for models with different hierarchical column counts

In the following code example, I populate the project model with a set of top-level items that contain key-value properties that I can view and edit using QTreeView.

Look at the Qt documentation for QAbstractItemModel::columnCount which says this should return the number of columns for the children of a given parent, This means that this should be a hierarchically related property.

However, using the following code, if I return the column count as a hierarchical correlation property (in this case, root->children has 1 column and root>child->children has 2 columns) then View will only display 1 column.

enter image description here

Printing node.columnCount() (see code) actually shows that the Item class node, when one of the items is expanded, actually returns columnCount = 2

If I always return 2 for the model.columnCount function, then View will display two columns correctly.

enter image description here

Does this always return the required number of columns in View regardless of hierarchy, or am I just doing something wrong, and what if so? Returning multiple columns for a parent whose children have different numbers of columns just for the View to work must feel 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

The documentation seems ambiguous and does not exactly match the implementation, where the number of columns in the View depends on the horizontal QHeaderView. , and the number of columns that horizontal QHeaderView uses as the root of the invisible item, i.e. the number of columns should be given by root(). , since root() does not overwrite columnCount(), by default it has a value of 1 (although for me Node’s columnCount() must be 0 and children() must return an empty list), so the solution is in Root columnCount () is set to 2

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_())

Related Problems and Solutions