您如何只允许用户通过拖放更改 QtreeWidget 中的项目顺序而不创建子项目?

How do you only allow user to change the item order in a QtreeWidget by drag and drop without creating child items?

我有一个使用 QtDesigner 设计的 GUI 和一个 QTreeWidget,同时使用 PySide2 对其背后的逻辑进行编程。现在我希望用户能够通过拖放来交换 QTreeWidget 中的元素,但不更改层次结构。所以基本上我不希望他能够将一个项目作为子项目插入另一个项目或使子项目成为顶级项目。

这是我的 QtreeWidget:

parent1
 |child1
 |child2
parent2
parent3

他应该只能更改父项目的顺序或子项目的顺序,但不能通过拖放使一个项目成为一个项目的子项目或使一个项目成为一个项目的父项目。我已经尝试使用 QtDesigner 中的设置进行试验,并更改我的 QTreeWidget 项目的代码中的一些值,但没有任何效果。如果有人能指导我走上正确的道路,我会很高兴。

编辑:答案已更新,请确保全部阅读

Qt Designer 不允许设置此类行为,虽然项目编辑器提供了 per item 标志,但它并未“完全”实现:它 确实ItemIsDropEnabled 提供标志,但默认情况下未选中,甚至 checking/unchecking 也不允许“取消设置”该标志。

结果是树小部件将使用默认的 QTreeWidgetItemFlags 创建,它会自动设置该标志。

最简单的解决方案是创建一个迭代顶级项目并禁用该标志的函数,同时调用一个递归函数来禁用 ItemIsDragEnabled for child 项。

如果结构已经有项,则必须在创建树小部件后立即调用该函数,并且还连接到模型的 rowsInserted 信号,以便每次添加新行时都会更新,包括child 项。

注意:这仅在顶级项目中需要手动排序时才有效,请参阅下文以了解允许对 child 项目进行排序的实现。

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        # ...
        self.checkTreeParents()
        self.treeWidget.model().rowsInserted.connect(self.checkTreeParents)

    def checkTreeParents(self):
        disabledFlags = QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled
        def checkChildren(parent):
            for row in range(parent.childCount()):
                child = parent.child(row)
                child.setFlags(child.flags() & ~disabledFlags)
                checkChildren(child)
        root = self.treeWidget.invisibleRootItem()
        for row in range(root.childCount()):
            child = root.child(row)
            child.setFlags(child.flags() & ~QtCore.Qt.ItemIsDropEnabled)
            checkChildren(child)

更新

如前所述,上述实现之所以有效,是因为很容易区分顶级项和 child 项:前者始终具有无效的 QModelIndex。如果需要在 child 项之间进行排序,则必须采用不同的路径。

虽然以下 可以 在不使用 subclassing(使用“猴子补丁”)的情况下实现,但通常不建议使用该路径,因为它通常会导致静默错误和难以跟踪的错误。

要求是使用 promoted 小部件(我建议阅读 my related answer 并对该主题进行一些研究),以便树形小部件可以正确已实施。

“技巧”是覆盖 startDrag 函数,获取整个树索引的列表,将所有项目与其当前标志配对,禁用所有项目的 ItemIsDropEnabled 标志 except 用于拖动项目的 parent;然后在拖动操作后立即恢复标志。由于 startDrag 处于阻塞状态(它启动自己的“事件循环”并在退出后 returns),在调用默认实现后恢复标志是足够安全的。
这确保拖动事件仅在悬停在与所选项目相同的 parent 或 between 上时才会被接受,而不是 on他们或 on/between 任何其他项目或 parent(包括 children)。

这可能是 最好的 方法,因为尝试通过覆盖 dragEnterEventdragMoveEventdropEvent 来实现相同的方法实际上是更复杂(因此,容易出现错误),并且可能还需要覆盖 paintEvent 才能正确显示放置指示器。通过临时更改项目的放置标志,我们让 QTreeView 处理所有这些。

注意:以下假设您使用 TreeView 作为 class 名称推广树形小部件;请确保您了解小部件推广的工作原理。

class TreeView(QtWidgets.QTreeWidget):
    def iterItems(self, parent=None):
        # iter through **all** items in the tree model, recursively, and
        # yield each item individually
        if parent is None:
            parent = self.invisibleRootItem()
            # the root item *must* be yield! If not, the result is that the
            # root will not have the ItemIsDropEnabled flag set, so it 
            # will accept drops even from child items 
            yield parent
        for row in range(parent.childCount()):
            childItem = parent.child(row)
            yield childItem
            for grandChild in self.iterItems(childItem):
                # yield children recursively, including grandchildren
                yield grandChild
        

    def startDrag(self, actions):
        selected = [i for i in self.selectedIndexes() 
            if i.flags() & QtCore.Qt.ItemIsDragEnabled]
        parents = list(set(i.parent() for i in selected))
        # we only accept drags from children of a single item
        if len(parents) == 1:
            parent = self.itemFromIndex(parents[0])
            if not parent:
                # required since itemFromIndex on the root *index* returns None
                parent = self.invisibleRootItem()
        else:
            # no item will accept drops!
            parent = None
        itemFlags = []
        for item in self.iterItems():
            if item != parent:
                # store all flags and disable the drop flag if set, UNLESS the 
                # item is the parent
                flags = item.flags()
                itemFlags.append((item, flags))
                item.setFlags(flags & ~QtCore.Qt.ItemIsDropEnabled)

        # call the default implementation and let the tree widget
        # do all of its stuff
        super().startDrag(actions)

        # finally, restore the original flags
        for item, flags in itemFlags:
            item.setFlags(flags)

备注:

  1. 以上代码没有考虑尝试拖动具有不同 parent 项目的项目的可能性(如评论中所述);这样做 可能的,但是需要更复杂的实现 iterItems() 和检查选择中每个项目的 parent 罩;
  2. 这里显然不考虑从外部来源掉落;
  3. setDragDropMode(InternalMove) 仍然是必需的;无论如何,它可以在设计器中设置;