Mac端PySide2 QTreeWidget拖拽项消失、内嵌控件丢失问题(Python2.7)
问题背景
运行环境及现象:
- 系统环境:Mac Monterey 12.4、Python 2.7 + PySide2、Maya软件内嵌UI开发场景
- 异常表现:
- QTreeWidget开启
InternalMove拖拽模式后,拖拽项会导致其他树节点意外消失,同逻辑此前在Windows平台可正常运行 - 拖拽完成后,通过
setItemWidget绑定到项上的内嵌控件全部丢失
- QTreeWidget开启
- 原问题复现代码:
import sys from PySide2 import QtCore, QtWidgets def main(): try: app = QtWidgets.QApplication(sys.argv) ui = TreeUI() ui.show() app.exec_() except RuntimeError: from maya import OpenMayaUI as omui try: import shiboken2 as shiboken except ImportError: import shiboken pointer = omui.MQtUtil.mainWindow() win = shiboken.wrapInstance(long(pointer), QtWidgets.QWidget) ui = TreeUI(parent=win) ui.show() class Tree(QtWidgets.QTreeWidget): def __init__(self, parent=None): super(Tree, self).__init__(parent) self.setHeaderLabels(('name', 'widget')) self.setSelectionMode(self.SingleSelection) self.setDragEnabled(True) self.setDropIndicatorShown(True) self.setDragDropMode(self.InternalMove) class TreeUI(QtWidgets.QMainWindow): def __init__(self, parent=None): super(TreeUI, self).__init__(parent) widget = QtWidgets.QWidget(self) self.setCentralWidget(widget) tree = Tree() for x in range(0, 6): item = QtWidgets.QTreeWidgetItem(tree, ('item{}'.format(x), None)) item.setFlags(item.flags() & ~QtCore.Qt.ItemIsDropEnabled) button = QtWidgets.QPushButton('Button{}'.format(x)) tree.setItemWidget(item, 1, button) layout = QtWidgets.QVBoxLayout(widget) layout.addWidget(tree) main()
问题原因
- 拖拽后项消失:Mac端Qt对
InternalMove模式的默认拖拽实现存在平台兼容问题,当项设置了~Qt.ItemIsDropEnabled禁止子项拖入的flag时,drop位置的坐标判定逻辑与Windows不一致,默认处理流程会错误删除非拖拽目标的相邻节点,导致树结构项丢失。 - 拖拽后控件丢失:Qt默认的
InternalMove拖拽逻辑并非直接移动原有QTreeWidgetItem对象,而是在落点位置创建新的项副本、删除原始项。通过setItemWidget绑定的控件与原始项对象强关联,原始项被回收时控件会同步销毁,不会自动迁移到新生成的项副本上;Mac端Qt对象回收触发时机更早,因此该问题比Windows平台更易复现。
修复方案
- 重写QTreeWidget子类的
dropEvent方法,完全接管拖拽移动逻辑,不调用Qt默认的drop处理流程,规避Mac平台下默认逻辑的位置判定bug,避免项被误删 - 执行项移动操作前,先暂存项上通过
setItemWidget绑定的自定义控件,项插入到新位置后重新绑定,避免控件随原始项销毁丢失
修复后可运行代码
import sys from PySide2 import QtCore, QtWidgets def main(): try: app = QtWidgets.QApplication(sys.argv) ui = TreeUI() ui.show() app.exec_() except RuntimeError: from maya import OpenMayaUI as omui try: import shiboken2 as shiboken except ImportError: import shiboken pointer = omui.MQtUtil.mainWindow() win = shiboken.wrapInstance(long(pointer), QtWidgets.QWidget) ui = TreeUI(parent=win) ui.show() class Tree(QtWidgets.QTreeWidget): def __init__(self, parent=None): super(Tree, self).__init__(parent) self.setHeaderLabels(('name', 'widget')) self.setSelectionMode(self.SingleSelection) self.setDragEnabled(True) self.setDropIndicatorShown(True) self.setDragDropMode(self.InternalMove) def dropEvent(self, event): dragged_item = self.currentItem() if not dragged_item: event.ignore() return # 暂存绑定的自定义控件 widget_column = 1 bound_widget = self.itemWidget(dragged_item, widget_column) if bound_widget: self.removeItemWidget(dragged_item, widget_column) # 获取落点信息 drop_pos = event.pos() target_item = self.itemAt(drop_pos) drop_position = self.dropIndicatorPosition() # 从原位置移除拖拽项 source_parent = dragged_item.parent() if source_parent: source_index = source_parent.indexOfChild(dragged_item) source_parent.takeChild(source_index) else: source_index = self.indexOfTopLevelItem(dragged_item) self.takeTopLevelItem(source_index) # 计算新位置并插入 if not target_item: # 拖到空白区域,追加到顶级项末尾 self.addTopLevelItem(dragged_item) else: target_parent = target_item.parent() if target_item.parent() else self target_index = target_parent.indexOfChild(target_item) if target_item.parent() else self.indexOfTopLevelItem(target_item) if drop_position == self.AboveItem: insert_index = target_index elif drop_position == self.BelowItem: insert_index = target_index + 1 else: # 落点在项上,因项已禁止Drop,统一插到项下方 insert_index = target_index + 1 if target_parent == self: self.insertTopLevelItem(insert_index, dragged_item) else: target_parent.insertChild(insert_index, dragged_item) # 重新绑定控件 if bound_widget: self.setItemWidget(dragged_item, widget_column, bound_widget) event.accept() class TreeUI(QtWidgets.QMainWindow): def __init__(self, parent=None): super(TreeUI, self).__init__(parent) widget = QtWidgets.QWidget(self) self.setCentralWidget(widget) tree = Tree() for x in range(0, 6): item = QtWidgets.QTreeWidgetItem(tree, ('item{}'.format(x), None)) item.setFlags(item.flags() & ~QtCore.Qt.ItemIsDropEnabled) button = QtWidgets.QPushButton('Button{}'.format(x)) tree.setItemWidget(item, 1, button) layout = QtWidgets.QVBoxLayout(widget) layout.addWidget(tree) main()
内容的提问来源于stack exchange,提问作者Jesse
相关产品推荐
相关产品推荐

