首页 > 解决方案 > 如何在 QTreeView 中为具有不同层次列数的模型显示正确的列数

问题描述

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

查看QAbstractItemModel::columnCount的 Qt 文档,它说这应该返回给定父级的子级的列数,这意味着这应该是一个分层的依赖属性。

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

在此处输入图像描述

打印node.columnCount()(参见代码)实际上将显示Item类节点在展开其中一项后实际上返回 columnCount = 2。

如果我总是为model.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 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_())

标签: pythonpyqtpyqt5qtreeviewqabstractitemmodel

解决方案


似乎文档不清楚并且与实现不完全匹配,在实现中视图中的列数取决于水平QHeaderView水平QHeaderView使用作为不可见项的根的列数,即列数应该由 给出Root(),并且由于Root()不会覆盖columnCount()它,因此默认情况下它的值为 1(尽管对我来说columnCount()Node 必须为 0 并且children()必须返回一个空列表),因此解决方案在根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_())

推荐阅读