Python ItemIsAutoTristate标志未按预期工作

Python ItemIsAutoTristate标志未按预期工作,python,checkbox,pyqt,qtreeview,qstandarditemmodel,Python,Checkbox,Pyqt,Qtreeview,Qstandarditemmodel,考虑一下这个小片段: 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 i

考虑一下这个小片段:

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_())
我在这里试图实现的是,当用户选择父对象的所有子对象时,父对象状态变为选中状态;如果取消选择所有子级,则父级状态变为取消选择;最后,如果选择了一些子项,则父项将部分选中。(反之亦然,因此如果用户取消选择父项,则其所有子项都将取消选择,如果用户选择父项,则其所有子项都将被选择)

理论上,这种行为应该通过使用flag来实现,flag表示:

项目的状态取决于其子项的状态。这使得 自动管理QTreeWidget中父项的状态 (如果选中了所有子项,则选中;如果选中了所有子项,则未选中。) 未选中,或仅选中某些子项时部分选中)

但是,如果您运行上面的代码,您将看到,在阅读文档之后,这种行为并不是您所期望的。我已经看到了这一点,尽管我不确定它是否与此相关,或者我的代码片段是否遗漏了一些东西

例如,上面的代码段允许您执行以下操作:


无论如何,问题是,您如何修复此小部件,使其与任何软件包安装程序一样,您可以一次选择/取消选择/部分选择所有具有公共父级的子包?

目前看来,
ItemIsAutoTristate
仅为
QTreeWidget
类实现。下面的
QStandardItem
子类为使用
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 docs中所解释的,似乎
ItemIsAutoTristate
只针对QTreeWidget类实现,只是为了完整起见,这里有一个小片段显示了如何在
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_())

这些文档具体指的是一个QTreeWidget。因为你没有使用,所以行为完全符合预期。@Ekhumaro的确,你完全正确,我没有从文档中读到这一部分。让我们看看QTreeWidget是否可以开箱即用。在任何情况下,我想要获得这样的QTreeView行为,您需要手动实现这样的行为?看起来是这样的。我只是想看看QTreeWidget的源代码,看看它有多复杂。回答得很好,我想不出任何情况下你的代码会失败,所以谢谢!如果他们在未来的Qt/PyQt5版本中添加了类似的内容,那就太好了,但不知道为什么还没有实现:/。顺便说一句,添加了一个小片段,展示了QTreeWidget如何开箱即用