加载新数据集时跟踪 QTreeWidget 中的选中项

2024-02-15

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

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

目前不知道什么是“跟踪”这些可检查项目的更好方法? (我可能还需要将它们填充到 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(如 QListWidget 和 QTableWidget)有其内部模型;它是对数据模型的某种高级访问,并且其实际模型不是直接的(如easily)可以访问,也不应该。它们是“简化的”模型视图界面,​​旨在用于不需要高级编辑的一般用途,但最重要的是它们只支持他们自己的,single和独特的模型。除了 Q[viewType]WidgetItem 接口之外,没有简单的方法可以更改它,除非您完全重置模型,这意味着如果您想在同一视图中使用多个模型,则需要将数据“存储”在其他地方,使整个事情比需要的更加复杂,并且很容易出现错误和问题,这正是您的情况所发生的情况。

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

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

这里最重要的方面是您将使用single数据模型类实例对于每个数据集(而不是多个字典+视图的模型对),只需简单地轻按一下即可在它们之间切换变得更加容易setModel().
缺点是前面提到的缺乏父母/孩子国家的支持,这是必须实施的;一旦这个逻辑得到解决,你就会得到多个持久、独特、一致模型,无论您实际需要多少个。

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

  • setData(index, value, role) is overridden to apply the check state to the children indexes: if the role is Qt.CheckState and the index has any children, the [un]checked state is applied to them; if the index has a parent, the index emits the dataChanged signal to the model, ensuring that its view[s] requires updates (otherwise the checkbox visible state won't be updated correctly until the view is repainted)[1];
  • data(index, role)需要覆盖"show"父级的检查状态;模型的索引数据是什么并不重要:如果它有任何子项,则其状态完全取决于它们(全部/任何/无检查),否则它基于默认模型索引的 checkState;

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

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

因为我已经在那里了,所以我还添加了一种机制来存储每个索引的扩展状态,以实现更好的视图/模型一致性;这不是必需的,但它确实有助于用户体验:-) 请记住,这只是为了演示目的:显然,如果您添加/删除项目而不考虑内部 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]: In this example the dataChanged signal is emitted whenever any child item check state changes. This isn't a big issue, but if you really need to avoid unnecessary dataChanged notifications you might need to add a QtCore.QTimer.singleshot delayed dataChanged signal emission only if the parent state has changed. It's not that hard to achieve, but I didn't think it was really necessary for this example.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

加载新数据集时跟踪 QTreeWidget 中的选中项 的相关文章

随机推荐