【问题标题】:Tracking checked items in QTreeWidget when loading in new set of data加载新数据集时跟踪 QTreeWidget 中的检查项目
【发布时间】:2019-08-23 19:11:37
【问题描述】:

我的 gui 中有一个 QTreeWidget,每当它加载到不同的数据集中时,它的内容都会被清除,并且我试图跟踪用户在不同数据集中加载时检查的内容。

最初,我想使用我创建的包含 QTreeWidgetItem 对象的derive_tree_items 方法来跟踪它,但是一旦我尝试加载一组新数据,我存储的对象将丢失为它们被删除(预期)..

目前迷失了“跟踪”这些可检查项目的更好方法是什么? (我可能还需要将它们填充到 QMenu + QAction 中,因此可以进行跟踪检查,但这将是下一次)

在我的代码中,您可以通过以下方式进行复制:

  • 点击按钮“Data-01”
  • 检查任何对象,例如。我检查了“c102”和“a102”
  • 点击“Data-02”按钮
  • 再次点击按钮“Data-01”
  • 期待看到'c102','a102'被选中..
IsNewItemRole = QtCore.Qt.UserRole + 1000

class CustomTreeWidgetItem(QtGui.QTreeWidgetItem):
    """Initialization class for QTreeWidgetItem creation.

    Args:
        widget (QtGui.QTreeWidget): To append items into.
        text (str): Input name for QTreeWidgetItem.
        is_tristate (bool): Should it be a tri-state checkbox. False by default.
    """
    def __init__(self, parent=None, text=None, is_tristate=False, is_new_item=False):
        super(CustomTreeWidgetItem, self).__init__(parent)

        self.setText(0, text)
        # flags = QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable

        if is_tristate:
            # flags |= QtCore.Qt.ItemIsTristate

            # Solely for the Parent item
            self.setFlags(
                self.flags()
                | QtCore.Qt.ItemIsTristate
                | QtCore.Qt.ItemIsEditable
                | QtCore.Qt.ItemIsUserCheckable
            )
        else:
            self.setFlags(
                self.flags()
                | QtCore.Qt.ItemIsEditable
                | QtCore.Qt.ItemIsUserCheckable
            )
            self.setCheckState(0, QtCore.Qt.Unchecked)

        self.setData(0, IsNewItemRole, is_new_item)

    def setData(self, column, role, value):
        """Override QTreeWidgetItem setData function.

        QTreeWidget does not have a signal that defines when an item has been
        checked/ unchecked. And so, this method will emits the signal as a
        means to handle this.

        Args:
            column (int): Column value of item.
            role (int): Value of Qt.ItemDataRole. It will be Qt.DisplayRole or
                Qt.CheckStateRole
            value (int or unicode): 
        """
        state = self.checkState(column)
        QtGui.QTreeWidgetItem.setData(self, column, role, value)
        if (role == QtCore.Qt.CheckStateRole and
                state != self.checkState(column)):
            tree_widget = self.treeWidget()
            if isinstance(tree_widget, CustomTreeWidget):
                tree_widget.itemToggled.emit(self, column)


class CustomTreeWidget(QtGui.QTreeWidget):
    """Initialization class for QTreeWidget creation.

    Args:
        widget ():
    """
    # itemToggled = QtCore.pyqtSignal(QtGui.QTreeWidgetItem, bool)
    itemToggled = QtCore.Signal(QtGui.QTreeWidgetItem, bool)

    contentUpdates = QtCore.Signal()

    def __init__(self, widget=None):
        super(CustomTreeWidget, self).__init__(widget)

        self.rename_counter = False

        # self.itemToggled.connect(self.handleItemToggled)
        self.currentItemChanged.connect(self.selection_item_changed)
        self.itemChanged.connect(self.tree_item_changed)
        self.itemDoubleClicked.connect(self.tree_item_double_clicked)

    def selection_item_changed(self, current, previous):
        """Overrides widget's default signal.

        Emiited when current item selection is changed. This will also toggles
        the state of `self.add_child_btn`.
        If a child item is selected, the "Add Child" button will be disabled.

        Args:
            current (CustomTreeWidgetItem): Currently selected item.
            previous (CustomTreeWidgetItem or None): Previous selected item.
        """
        state = True
        if not current or current.parent():
            state = False

    def tree_item_changed(self, item, column):
        """Overrides widget's default signal.

        Emitted when the contents of the selected item in the column changes.

        Args:
            item (CustomTreeWidgetItem): Selected item.
            column (int): Column value of the selected item.
        """
        if self.rename_counter and self.prev_name != item.text(column):
            self.rename_counter = False
            item.setData(0, IsNewItemRole, True)

            self.contentUpdates.emit()

        elif item.checkState(column) == QtCore.Qt.Checked:
            print('Item Checked')

        elif item.checkState(column) == QtCore.Qt.Unchecked:
            print('Item Unchecked')

    def tree_item_double_clicked(self, item, column):
        """Overrides widget's default signal.

        Emitted when User performs double clicks inside the widget.

        Args:
            item (CustomTreeWidgetItem): Selected item.
            column (int): Column value of the selected item.
        """
        self.prev_name = item.text(column)
        self.rename_counter = True

    def derive_tree_items(self, mode="all"):
        all_items = OrderedDict()

        root_item = self.invisibleRootItem()
        top_level_count = root_item.childCount()

        for i in range(top_level_count):
            top_level_item = root_item.child(i)
            top_level_item_name = str(top_level_item.text(0))
            child_num = top_level_item.childCount()

            all_items[top_level_item_name] = []

            for n in range(child_num):
                child_item = top_level_item.child(n)
                child_item_name = str(child_item.text(0)) or ""

                all_items[top_level_item_name].append(child_item)

        return all_items


class MainApp(QtGui.QWidget):
    def __init__(self, parent=None):
        super(MainApp, self).__init__(parent)

        self._diff_highlight = False
        self._tree = CustomTreeWidget()
        self._tree.header().hide()

        # QTreeWidget default signals override
        self._tree.contentUpdates.connect(self.update_dictionary)

        tree_layout = QtGui.QVBoxLayout()
        self.btn1 = QtGui.QPushButton("Data-01")
        self.btn2 = QtGui.QPushButton("Data-02")
        tree_layout.addWidget(self._tree)
        tree_layout.addWidget(self.btn1)
        tree_layout.addWidget(self.btn2)

        main_layout = QtGui.QHBoxLayout()
        main_layout.addLayout(tree_layout)
        self.setLayout(main_layout)

        self.setup_connections()

    def setup_connections(self):
        self.btn1.clicked.connect(self.show_data_01)
        self.btn2.clicked.connect(self.show_data_02)

    def update_dictionary(self):
        print '>>> update: ', self._tree.derive_tree_items()

    def show_data_01(self):
        print '>>> Button1 test'

        self._tree.clear()

        test_dict1 = {
            "itemA" :{
                "menuA": ["a101", "a102"],
            },
            "itemBC": {
                "menuC": ["c101", "c102", "c103"],
                "menuB": ["b101"]
            },
        }

        for page_name, page_contents in test_dict1.items():
            # page_item = PageHeaderItem(self._tree, page_name)
            for pk, pv in page_contents.items():
                parent = CustomTreeWidgetItem(self._tree, pk, is_tristate=True)
                for c in pv:
                    child = CustomTreeWidgetItem(parent, c)

        self._tree.expandAll()



    def show_data_02(self):
        print '>>> Button2 test'

        self._tree.clear()

        test_dict2 = {
            "itemD" :{
                "menuD": ["d100"],
            },
        }

        for page_name, page_contents in test_dict2.items():
            # page_item = PageHeaderItem(self._tree, page_name)
            for pk, pv in page_contents.items():
                parent = CustomTreeWidgetItem(self._tree, pk, is_tristate=True)
                for c in pv:
                    child = CustomTreeWidgetItem(parent, c)

        self._tree.expandAll()



if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    w = MainApp()
    w.show()
    sys.exit(app.exec_())

【问题讨论】:

  • 使用 QTreeWidget 而不是 QTreeView 是否有特定原因?由于您必须在两个数据集之间“切换”,您可能应该使用 QTreeView 并将您的数据设置为您需要的每个数据集的 QStandardItemModel,然后只需使用 setModel 在它们之间切换。
  • @musicamante 我应该提到它可能超过 2 个数据集。出于说明目的,我在示例中使用了 2 个数据集。至于 QTreeWidget 或 QTreeView 的使用,老实说我不确定哪个是最好的。我用的是前者,可能是我第一次遇到的那个
  • 虽然我对QTreeView 的使用一无所知,但只是尝试使用它并意识到对于我要实现的复选框,它不像检查顶级项目那样直观不检查里面的子项(可能是我做错了)
  • 使用 QTreeView 和 QStandardItemModel 的缺点是它不支持自动三态检查状态。我正在添加一个答案,该答案也可以解决此功能不足的问题。

标签: python pyqt pyqt4 qtreewidget


【解决方案1】:

一个QTreeWidget(如QListWidget和QTableWidget)有它的内部模型;这是对数据模型的某种高级访问,它的实际模型不能直接访问(如 容易),也不应该直接访问。它们是“简化的”模型视图界面,​​旨在用于不需要高级编辑的一般用途,但 - 最重要的是 - 它们仅支持自己的、单一和独特的模型。除了从他们的 Q[viewType]WidgetItem 接口之外,没有简单的方法可以更改它,除非您完全重置模型,这意味着如果您想在同一视图中使用多个模型,则需要将数据“存储”到其他地方,使整个事情变得比它需要的复杂得多,并且很容易出现错误和问题,这正是您的情况。

另一方面,那些 QWidgetItemViews 提供了标准模型和视图中缺少的一些功能,其中之一是 QTreeWidgets 中项目的“自动检查”。
虽然该功能非常有用,但当您需要在同一视图上显示不同的数据模型时,它可能是一个 serius PITA;这意味着,为了避免花言巧语地重新发明轮子,最好坚持使用 QTreeView/QStandardItemModel 对并只实现三态机制,而不是使用可能与 QTreeWidget 的内部实现发生冲突的复杂方法。

独立的QStandardItemModel 子类实例,支持父/子三态

这里最重要的方面是您将为每个数据集使用一个单个数据模型类实例(而不是多个dict + view的模型对),使只需轻弹setModel(),就可以更轻松地在它们之间切换。
缺点是前面提到的缺乏父母/子女国家支持,必须实施;一旦解决了该逻辑,您将获得多个持久、独特且一致的模型,无论您实际需要多少模型。

除了实际的模型内容初始化,只需要继承QStandardItemModel的两个方法:

  • setData(index, value, role) 被覆盖以将检查状态应用于子索引:如果角色是 Qt.CheckState 并且索引有任何子索引,则将 [un]checked 状态应用于它们;如果索引有父索引,则索引向模型发出dataChanged 信号,确保其视图[s] 需要更新(否则在重新绘制视图之前,复选框可见状态不会正确更新)[ 1];
  • data(index, role) 需要覆盖以“显示”父级的检查状态;模型的索引数据是什么无关紧要:如果它有任何子节点,则其状态完全取决于它们(所有/任何/未检查),否则它基于默认模型索引的 checkState;

一旦解决了这个问题,您只需要关心将新选择的模型设置为视图,所有状态都会像切换到另一个模型之前一样存在。

为了与您的示例保持一致,我使用了您的基于 dict 的模型数据创建逻辑,但我建议您使用递归方法来添加子子项。

由于我已经在那里,我还添加了一种机制来存储每个索引的展开状态,以实现更好的视图/模型一致性;这不是必需的,但它确实有助于用户体验:-) 请记住,这只是为了演示目的:显然,如果您在不关心内部 expandState 字典的情况下添加/删除项目,这将无法正常工作(或获胜根本不工作!)。

import sys
from PyQt5 import QtCore, QtGui, QtWidgets

dataSets = [
    {
        "itemA" :{
            "menuA": ["a101", "a102"],
        },
        "itemBC": {
            "menuC": ["c101", "c102", "c103"],
            "menuB": ["b101"]
        },
    }, 
    {
        "itemD" :{
            "menuD": ["d100"],
        },
    }

]

class TreeModel(QtGui.QStandardItemModel):
    checkStateChange = QtCore.pyqtSignal(QtCore.QModelIndex, bool)
    def __init__(self, dataSet):
        super(TreeModel, self).__init__()

        # unserialize data, as per your original code; you might want to use a
        # recursive function instead, to allow multiple levels of items
        for page_name, page_contents in dataSet.items():
            for pk, pv in page_contents.items():
                parent = QtGui.QStandardItem(pk)
                parent.setCheckable(True)
                self.appendRow(parent)
                if pv:
                    parent.setTristate(True)
                    for c in pv:
                        child = QtGui.QStandardItem(c)
                        child.setCheckable(True)
                        parent.appendRow(child)

        self.dataChanged.connect(self.checkStateChange)

    def setData(self, index, value, role=QtCore.Qt.EditRole):
        if role == QtCore.Qt.CheckStateRole:
            childState = QtCore.Qt.Checked if value else QtCore.Qt.Unchecked
            # set all children states according to this parent item
            for row in range(self.rowCount(index)):
                for col in range(self.columnCount(index)):
                    childIndex = self.index(row, col, index)
                    self.setData(childIndex, childState, QtCore.Qt.CheckStateRole)
            # if the item has a parent, emit the dataChanged signal to ensure
            # that the parent state is painted correctly according to what data()
            # will return; note that this will emit the dataChanged signal whatever
            # the "new" parent state is, meaning that it might still be the same
            parent = self.parent(index)
            if parent.isValid():
                self.dataChanged.emit(parent, parent)
        return super(TreeModel, self).setData(index, value, role)

    def data(self, index, role=QtCore.Qt.DisplayRole):
        # QStandardItemModel doesn't support auto tristate based on its children 
        # as it does for QTreeWidget's internal model; we have to implement that
        if role == QtCore.Qt.CheckStateRole and self.flags(index) & QtCore.Qt.ItemIsTristate:
            childStates = []
            # collect all child check states
            for row in range(self.rowCount(index)):
                for col in range(self.columnCount(index)):
                    childIndex = self.index(row, col, index)
                    childState = self.data(childIndex, QtCore.Qt.CheckStateRole)
                    # if the state of a children is partially checked we can
                    # stop here and return a partially checked state
                    if childState == QtCore.Qt.PartiallyChecked:
                        return QtCore.Qt.PartiallyChecked
                    childStates.append(childState)
            if all(childStates):
                # all children are checked, yay!
                return QtCore.Qt.Checked
            elif any(childStates):
                # only some children are checked...
                return QtCore.Qt.PartiallyChecked
            # no item is checked, so bad :-(
            return QtCore.Qt.Unchecked
        return super(TreeModel, self).data(index, role)

    def checkStateChange(self, topLeft, bottomRight):
        # if you need some control back to your data outside the model, here is
        # the right place to do it; note that *usually* the topLeft and 
        # bottomRight indexes are the same, expecially with QStandardItemModels
        # but that would not be the same in some special cases
        pass


class Window(QtWidgets.QWidget):
    def __init__(self):
        QtWidgets.QWidget.__init__(self)
        layout = QtWidgets.QGridLayout()
        self.setLayout(layout)

        self.treeView = QtWidgets.QTreeView()
        layout.addWidget(self.treeView)

        self.models = []
        self.expandStates = {}

        for i, dataSet in enumerate(dataSets):
            model = TreeModel(dataSet)
            button = QtWidgets.QPushButton('Data-{:02}'.format(i + 1))
            layout.addWidget(button)
            button.clicked.connect(lambda _, model=model: self.setModel(model))

    def getExpandState(self, expDict, model, index=QtCore.QModelIndex()):
        # set the index expanded state, if it's not the root index:
        # the root index is not a valid index!
        if index.isValid():
            expDict[index] = self.treeView.isExpanded(index)
        # if the index (or root index) has children, set their states
        for row in range(model.rowCount(index)):
            for col in range(model.columnCount(index)):
                childIndex = model.index(row, col, index)
                # if the current index has children, set their expand state
                # using this function, which is recursive
                for childRow in range(model.rowCount(childIndex)):
                    self.getExpandState(expDict, model, childIndex)

    def setModel(self, model):
        if self.treeView.model():
            if self.treeView.model() == model:
                # the model is the same, no need to update anything
                return
            # save the expand states of the current model before changing it
            prevModel = self.treeView.model()
            self.expandStates[prevModel] = expDict = {}
            self.getExpandState(expDict, prevModel)
        self.treeView.setModel(model)
        if model in self.expandStates:
            # if the new model has expand states saved, restore them
            for index, expanded in self.expandStates.get(model, {}).items():
                self.treeView.setExpanded(index, expanded)
        else:
            self.treeView.expandAll()

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    w = Window()
    w.show()
    sys.exit(app.exec_())

[1]:在本例中,只要 any 子项检查状态发生变化,就会发出 dataChanged 信号。这不是一个大问题,但如果您确实需要避免不必要的 dataChanged 通知,您可能需要添加 QtCore.QTimer.singleshot 延迟 dataChanged 信号发射如果父状态已更改。实现起来并不难,但我认为对于这个例子来说真的没有必要。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-08-15
    • 1970-01-01
    • 2017-06-19
    • 1970-01-01
    • 1970-01-01
    • 2023-01-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多