ItemIsAutoTristate 标志未按预期工作

ItemIsAutoTristate flag not working as expected

考虑这个小片段:

import sys

from PyQt5 import QtWidgets
from PyQt5 import QtWidgets
from PyQt5.QtGui import QStandardItemModel
from PyQt5.QtGui import QStandardItem
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QGridLayout
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QWidget
from PyQt5.QtWidgets import QTreeView
from PyQt5.QtWidgets import QAbstractItemView


packages = {
    'tree': {
        'parent1': ['child1', 'child2', 'child3'],
        'parent2': ['child4', 'child5'],
        'parent3': ['child6']
    },
    'metadata': {
        'child1': {'description': 'child1 description', 'enabled': True},
        'child2': {'description': 'child2 description', 'enabled': False},
        'child3': {'description': 'child3 description', 'enabled': True},
        'child4': {'description': 'child4 description', 'enabled': False},
        'child5': {'description': 'child5 description', 'enabled': True},
        'child6': {'description': 'child6 description', 'enabled': True}
    }
}


class McveDialog(QWidget):

    def __init__(self, parent=None):
        super().__init__(parent)

        self.treeview = QTreeView()
        # self.treeview.setHeaderHidden(True)
        self.treeview.setUniformRowHeights(True)
        # self.treeview.setEditTriggers(QAbstractItemView.NoEditTriggers)
        # self.treeview.setSelectionMode(QAbstractItemView.ExtendedSelection)

        self.model = QStandardItemModel()
        self.model.setHorizontalHeaderLabels(['Package', 'Description'])

        metadata = packages['metadata']
        tree = packages['tree']
        for parent, childs in tree.items():
            parent_item = QStandardItem(f'{parent}')
            parent_item.setCheckState(True)
            parent_item.setCheckable(True)
            parent_item.setFlags(parent_item.flags() | Qt.ItemIsAutoTristate)
            # parent_item.setFlags(parent_item.flags() | Qt.ItemIsUserTristate)
            self.model.appendRow(parent_item)

            for child in childs:
                description = metadata[child]['description']
                checked = metadata[child]['enabled']
                child_item = QStandardItem(f'{child}')
                check = Qt.Checked if checked else Qt.Unchecked
                child_item.setCheckState(check)
                child_item.setCheckable(True)
                # child_item.setFlags(child_item.flags() |Qt.ItemIsAutoTristate)
                parent_item.appendRow(child_item)

        self.treeview.setModel(self.model)
        self.model.itemChanged.connect(self.on_itemChanged)

        layout = QGridLayout()
        row = 0
        layout.addWidget(self.treeview, row, 0, 1, 3)

        row += 1
        self.but_ok = QPushButton("OK")
        layout.addWidget(self.but_ok, row, 1)
        self.but_ok.clicked.connect(self.on_ok)

        self.but_cancel = QPushButton("Cancel")
        layout.addWidget(self.but_cancel, row, 2)
        self.but_cancel.clicked.connect(self.on_cancel)

        self.setLayout(layout)
        self.setGeometry(300, 200, 460, 350)

    def on_itemChanged(self, item):
        pass

    def on_ok(self):
        pass

    def on_cancel(self):
        self.close()


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    dialog = McveDialog()
    dialog.setWindowTitle('Mcve dialog')
    dialog.show()
    sys.exit(app.exec_())

我在这里想要实现的是,当用户选择 parent 的所有 children 时,parent 状态变为选中状态;如果所有 children 都取消选择,则 parent 状态变为取消选择;最后,如果选择了某些 children,则 parent 会被部分选中。 (而 vice-versa,因此如果用户取消选择 parent,则其所有 children 都将被取消选择,如果用户选择 parent,其所有 children将被选中)。

理论上,这种行为应该通过使用 Qt::ItemIsAutoTristate 标志来实现,它表示:

The item's state depends on the state of its children. This enables automatic management of the state of parent items in QTreeWidget (checked if all children are checked, unchecked if all children are unchecked, or partially checked if only some children are checked).

但是,如果您 运行 上面的代码,您会在阅读文档后发现行为不是您所期望的。我看到有这个 bugreport,虽然我不确定它是否与此有关,或者我的代码片段是否只是缺少一些东西。

例如,上面的代码片段允许您这样做:

无论如何,问题是,您将如何修复这个小部件,使其像任何包安装程序一样运行,您可以在其中 select/unselect/partially-select 所有子包同时具有一个共同的 parent?

看来,目前,ItemIsAutoTristate 仅针对 QTreeWidget class 实施。下面的 QStandardItem subclass 为 item-views 提供了与使用 QStandardItemModel 相同的功能。这是 QTreeWidget 实现的或多或少的忠实移植。它似乎与示例代码一起工作正常,但我还没有测试它:

class StandardItem(QStandardItem):
    def data(self, role = Qt.UserRole + 1):
        if (role == Qt.CheckStateRole and self.hasChildren() and
            self.flags() & Qt.ItemIsAutoTristate):
            return self._childrenCheckState()
        return super().data(role)

    def setData(self, value, role=Qt.UserRole + 1):
        if role == Qt.CheckStateRole:
            if (self.flags() & Qt.ItemIsAutoTristate and
                value != Qt.PartiallyChecked):
                for row in range(self.rowCount()):
                    for column in range(self.columnCount()):
                        child = self.child(row, column)
                        if child.data(role) is not None:
                            flags = self.flags()
                            self.setFlags(flags & ~Qt.ItemIsAutoTristate)
                            child.setData(value, role)
                            self.setFlags(flags)
            model = self.model()
            if model is not None:
                parent = self
                while True:
                    parent = parent.parent()
                    if (parent is not None and
                        parent.flags() & Qt.ItemIsAutoTristate):
                        model.dataChanged.emit(
                            parent.index(), parent.index(),
                            [Qt.CheckStateRole])
                    else:
                        break
        super().setData(value, role)

    def _childrenCheckState(self):
        checked = unchecked = False
        for row in range(self.rowCount()):
            for column in range(self.columnCount()):
                child = self.child(row, column)
                value = child.data(Qt.CheckStateRole)
                if value is None:
                    return
                elif value == Qt.Unchecked:
                    unchecked = True
                elif value == Qt.Checked:
                    checked = True
                else:
                    return Qt.PartiallyChecked
                if unchecked and checked:
                    return Qt.PartiallyChecked
        if unchecked:
            return Qt.Unchecked
        elif checked:
            return Qt.Checked

如@ekhumoro answer 和 Qt 文档中所述,似乎 ItemIsAutoTristate 仅针对 QTreeWidget class 实现,只是为了完整起见,这里有一个小片段展示了如何在QTreeWidget 开箱即用:

import sys

from PyQt5 import QtWidgets
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QGridLayout
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QWidget
from PyQt5.QtWidgets import QTreeWidget
from PyQt5.QtWidgets import QTreeWidgetItem

packages = {
    'tree': {
        'parent1': ['child1', 'child2', 'child3'],
        'parent2': ['child4', 'child5'],
        'parent3': ['child6']
    },
    'metadata': {
        'child1': {'description': 'child1 description', 'enabled': True},
        'child2': {'description': 'child2 description', 'enabled': False},
        'child3': {'description': 'child3 description', 'enabled': True},
        'child4': {'description': 'child4 description', 'enabled': False},
        'child5': {'description': 'child5 description', 'enabled': True},
        'child6': {'description': 'child6 description', 'enabled': True}
    }
}


class McveDialog(QWidget):

    def __init__(self, parent=None):
        super().__init__(parent)

        self.treewidget = QTreeWidget()
        self.treewidget.setHeaderLabels(['Package', 'Description'])

        metadata = packages['metadata']
        tree = packages['tree']
        for parent, childs in tree.items():
            parent_item = QTreeWidgetItem(self.treewidget)
            parent_item.setText(0, parent)
            parent_item.setFlags(parent_item.flags() |
                                 Qt.ItemIsAutoTristate | Qt.ItemIsUserCheckable)
            parent_item.setCheckState(0, Qt.Checked)

            for child in childs:
                description = metadata[child]['description']
                checked = metadata[child]['enabled']
                child_item = QTreeWidgetItem(parent_item)
                child_item.setText(0, child)
                child_item.setText(
                    1, packages['metadata'][child]['description'])
                check = Qt.Checked if checked else Qt.Unchecked
                child_item.setFlags(child_item.flags() |
                                    Qt.ItemIsUserCheckable)
                child_item.setCheckState(0, check)

        layout = QGridLayout()
        row = 0
        layout.addWidget(self.treewidget, row, 0, 1, 3)

        row += 1
        self.but_ok = QPushButton("OK")
        layout.addWidget(self.but_ok, row, 1)
        self.but_ok.clicked.connect(self.on_ok)

        self.but_cancel = QPushButton("Cancel")
        layout.addWidget(self.but_cancel, row, 2)
        self.but_cancel.clicked.connect(self.on_cancel)

        self.setLayout(layout)
        self.setGeometry(300, 200, 460, 350)

    def on_ok(self):
        self.close()

    def on_cancel(self):
        self.close()


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    dialog = McveDialog()
    dialog.setWindowTitle('Mcve dialog')
    dialog.show()
    sys.exit(app.exec_())