python - 您如何只允许用户通过拖放更改 QtreeWidget 中的项目顺序而不创建子项目?
问题描述
我有一个使用 QtDesigner 和 QTreeWidget 设计的 GUI,同时使用 PySide2 对其背后的逻辑进行编程。现在我希望用户能够通过拖放来交换 QTreeWidget 中的元素,但不改变层次结构。所以基本上我不希望他能够将一个项目作为一个孩子插入另一个项目或使一个子项目成为顶级项目。
这是我的 QtreeWidget:
parent1
|child1
|child2
parent2
parent3
他应该只能更改父项的顺序或子项的顺序,但不能通过拖放将其中一项设为项的子项或将一项设为项的父项。我已经尝试过使用 QtDesigner 中的设置并为我的 QTreeWidget 项目更改代码中的一些值,但没有任何效果。如果有人能引导我走上正确的道路,我会非常高兴。
解决方案
编辑:答案已更新,请确保您已阅读所有内容
Qt Designer 不允许设置这种行为,虽然项目编辑器提供了每个项目的标志,但它并没有“完全”实现:它确实为 提供了标志ItemIsDropEnabled
,但默认情况下未选中,甚至检查/取消选中它也不允许“取消设置”该标志。
结果是树小部件将使用默认的 QTreeWidgetItemFlags 创建,它会自动设置该标志。
最简单的解决方案是创建一个函数来迭代顶级项目并禁用该标志,但也调用一个禁用ItemIsDragEnabled
子项目的递归函数。
如果结构已经有项目,则必须在创建树小部件后立即调用该函数,并且还连接到模型的rowsInserted
信号,以便每次添加新行时都会更新它,包括子项目。
注意:这仅在顶级项目中需要手动排序时才有效,请参阅下面的允许对子项目进行排序的实现。
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)
更新
如前所述,上述实现之所以有效,只是因为很容易区分顶级项目和子项目:前者总是有一个无效的 QModelIndex。如果需要在子项之间进行排序,则必须采用不同的路线。
虽然在没有子类化(使用“猴子补丁”)的情况下可以实现以下目标,但通常不建议使用该路径,因为它通常会导致难以跟踪的静默错误和错误。
要求是使用提升的小部件(我建议阅读我的相关答案并对该主题进行一些研究),以便可以正确实现树小部件。
“诀窍”是覆盖该startDrag
函数,获取整个树索引的列表,将所有项目与其当前标志配对,禁用除拖动项目的父项之外ItemIsDropEnabled
的所有项目的标志;然后在拖动操作后立即恢复标志。由于是阻塞的(它启动自己的“事件循环”并在退出后返回),调用默认实现后恢复标志是足够安全的。
这确保了拖动事件仅在悬停在与所选项目相同的父项上或它们之间时才被接受,而不是在它们上或在任何其他项目或父项(包括子项)上/之间。startDrag
这可能是最好的方法,因为尝试通过覆盖来做同样的事情dragEnterEvent
,实际上会更复杂(因此,容易出现错误),dragMoveEvent
并且dropEvent
可能还需要覆盖paintEvent
才能正确显示放置指示器。通过临时更改项目的丢弃标志,我们让 QTreeView 处理所有这些。
注意:以下假设您使用TreeView
作为类名来提升树小部件;请确保您已了解小部件促销的工作原理。
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)
笔记:
- 上面的代码没有考虑尝试拖动具有不同父项的项目的可能性(如注释中所述);这样做是可能的,但需要对两者进行更复杂的实现
iterItems()
并检查选择中每个项目的父母身份; - 显然这里不考虑来自外部来源的下降;
setDragDropMode(InternalMove)
仍然需要;无论如何,它可以在 Designer 中设置;