如何在具有不同层次列数的模型的 QTreeView 中显示正确的列数

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

在下面的代码示例中,我使用一组顶级项目填充项目模型,这些项目包含我可以使用 QTreeView 查看和编辑的键值属性。

查看 QAbstractItemModel::columnCount 的 Qt 文档,它说这应该 return 给定父级的子级的列数,这意味着这应该是层次相关的 属性.

但是,使用下面的代码,如果我 return 列计数为层次依赖 属性(在这种情况下 root->children 有 1 列,root->child->children 有2 列)那么视图将只显示 1 列。

打印 node.columnCount()(参见代码)实际上会显示 Item class 节点在展开其中一项后实际上 return columnCount = 2 .

如果我对 model.columnCount 函数总是 return 2,那么视图将正确显示两列。

这是否需要始终 return 视图中所需的列数,而不管层次结构如何,或者我只是做错了什么,如果是这样怎么办?为父项返回多个列,其子项具有不同列数只是为了使视图正常工作感觉一定是错误的。

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

文档似乎不清楚,与实现不完全匹配,在实现中视图中的列数取决于水平QHeaderView, horizo​​ntal QHeaderView使用不可见项的根的列数,即列数应由Root()给出,并且因为 Root() 不会覆盖 columnCount() 它默认值为 1(尽管对我来说 columnCount() of Node 必须是 0 而 children() 必须 return 一个空列表),所以在 Root columnCount().

中将解决方案设置为 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_())

正如@eyllanesc 回答的那样,视图中的列数仅受根项的影响。但是,columnCount() 将对给出的数字小于此数字的行产生影响,因为不会填充小于此数字的列。

采取上面的代码:

class Root(Node):
    ...
    def columnCount():
        return 2

class Property(Node):
    ...
    def columnCount():
        return 1

在这种情况下,即使显示了两列,Property 节点也不会显示第二列的数据,因为 columnCount() 告诉视图只有 1 列