加载新的数据集时跟踪QTreeWidget中的已检查项

2024-04-19 13:47:42 发布

您现在位置:Python中文网/ 问答频道 /正文

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

最初,我想用我创建的derive_tree_items方法来跟踪它,在这个方法中它包含QTreeWidgetItem对象,但是一旦我试图加载一组新的数据,我存储的对象就会在删除时丢失(预期的)。。在

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

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

  • 点击按钮“Data-01”
  • 检查所有对象,例如,我检查了“c102”和“a102”
  • 点击按钮“Data-02”
  • 再次单击“Data-01”按钮
  • 希望看到“c102”,但已选中“a02”。。在
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_())

Tags: thenameselfchildtreefordefitems
1条回答
网友
1楼 · 发布于 2024-04-19 13:47:42

QTreeWidget(像QListWidget和QTableWidget)有它的内部模型;它是对数据模型的某种高级访问,它的实际模型不直接(如容易)访问,也不应该。它们是“简化的”模型视图界面,用于一般用途,不需要高级编辑,但最重要的是,它们只支持自己的、单一的和唯一的模型。如果你不想在其他地方使用更复杂的数据,那就意味着你不需要在其他地方更容易地使用它们的模型来存储数据,除非你想用更复杂的方式来存储数据,这正是你的情况。在

相反,那些QWidgetItemViews提供了一些标准模型和视图中缺少的功能,其中之一就是QTreeWidgets中项目的“自动检查”。
虽然这个特性非常有用,但当您需要在同一个视图上显示不同的数据模型时,它可能是一个seriusPITA;这意味着,为了避免对轮子的修辞改造,最好使用QTreeView/QStandardItemModel对,只实现三态机制,而不是使用可能与QTreeWidget的内部实现冲突的复杂方法。在

分离QStandardItemModel子类实例,支持父/子三态

这里最重要的一点是,您将为每个数据集使用一个单个数据模型类实例(而不是多个dict+view的模型对),通过简单的setModel()在它们之间切换变得更加容易。
缺点是前面提到的缺少父/子状态支持,这必须实现;一旦解决了这个逻辑,您将得到多个持久的、唯一的和一致的模型,不管您实际需要多少。在

除了实际的模型内容初始化之外,您只需要将QStandardItemModel的两个方法子类化:

  • setData(index, value, role)被重写以将检查状态应用于子索引:如果角色是Qt.CheckState,并且索引有任何子级,则对它们应用[unchecked]状态;如果索引有父级,则索引向模型发出dataChanged信号,确保其视图需要更新(否则复选框可见状态不会更新)正确,直到重新绘制视图)[1]
  • data(index, role)重写需要“显示”父级的checkstate;不管模型的索引数据是什么:如果它有任何子级,它的状态完全依赖于它们(all/any/none checked),否则它基于默认模型索引的checkstate

一旦解决了这个问题,您只需关心将新选择的模型设置为视图,并且所有状态都将保持在切换到另一个模型(如果有的话)之前的状态。在

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

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

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

当任何子项的状态发生变化时。这不是一个大问题,但是如果您真的需要避免不必要的dataChanged通知,那么您可能需要添加一个QtCore.QTimer.singleshot延迟的dataChanged信号发射,如果父状态已经改变的话。这不是很难实现,但我没有我不认为这个例子真的有必要。在

相关问题 更多 >