PySide2中QTreeWidget拖拽后如何保留QTreeWidgetItem关联控件
QTreeWidget内部拖拽后setItemWidget绑定控件丢失修复方案
问题现象
使用自定义QTreeWidget实现内部拖拽排序功能时,拖拽放置完成后,QTreeWidgetItem通过setItemWidget绑定的关联控件会直接丢失。
已知该现象的触发逻辑:Qt默认内部拖拽机制会在拖拽过程中销毁被拖拽的原Item,在放置位置重新生成新的Item对象,绑定在原Item上的控件会随原对象销毁被回收。
代码同时兼容Maya平台运行:Mac端Maya存在已知bug,默认拖拽逻辑会直接删除被拖拽Item,必须重写dropEvent修复该问题,修复后即可复现控件丢失问题——例如下方示例中拖拽item5重排序后,其绑定的按钮控件会消失:
初始复现代码如下:
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) self.reparented_widgets = {} def dropEvent(self, event): index = self.indexAt(event.pos()) parent = index.parent() self.model().dropMimeData( event.mimeData(), event.dropAction(), index.row(), index.column(), parent ) 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()
之前尝试方案的崩溃原因
之前在startDrag中暂存控件、移除原绑定后重新挂载的方案会触发崩溃,核心原因有两点:
- 调用
removeItemWidget后没有临时修改控件的父对象为Tree控件本身,原Item被销毁时,Qt对象树机制会连带析构子控件,后续访问已经被析构的控件指针属于野指针操作,直接触发崩溃。 - 拖拽完成后通过
self.itemAt(event.pos())获取目标Item的逻辑不可靠:当拖拽位置落在两个Item的间隙时,该方法返回的是相邻Item而非拖拽生成的新Item,会出现控件绑错位置的问题。
修复实现逻辑
不需要复杂的QMimeType自定义,通过控件临时重设父对象+拖拽后匹配重绑即可实现,步骤如下:
- 重写
startDrag方法:拖拽启动前遍历所有选中Item,将Item的唯一标识(示例中用第一列文本做标识,实际业务可替换为自定义数据角色存储的唯一ID)、对应列的控件存入缓存字典,同时将控件的父对象临时设置为Tree控件本身,避免原Item销毁时控件被回收。 - 执行原有拖拽逻辑(保留Maya Mac端的dropEvent兼容修复),等待新Item创建完成。
- 拖拽操作完成后,遍历Tree中所有Item,通过唯一标识匹配缓存中的控件,调用
setItemWidget重新绑定到对应新Item的对应列,绑定完成后清空缓存字典。
修正后可运行代码
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) # 缓存结构:{item唯一标识: {列索引: 控件对象}} self._drag_temp_widgets = {} def startDrag(self, actions): self._drag_temp_widgets.clear() selected_items = self.selectedItems() for item in selected_items: # 取第一列文本作为item唯一标识,业务中可替换为Qt.UserRole存储的自定义ID item_id = item.text(0) self._drag_temp_widgets[item_id] = {} # 遍历所有列,收集绑定的控件 for col in range(self.columnCount()): widget = self.itemWidget(item, col) if widget: # 临时把控件父对象设为树本身,避免原item销毁时控件被回收 widget.setParent(self) self._drag_temp_widgets[item_id][col] = widget super(Tree, self).startDrag(actions) # 拖拽完成后执行控件重绑定 self._rebind_dragged_widgets() def dropEvent(self, event): # 保留Maya Mac端拖拽bug修复逻辑 index = self.indexAt(event.pos()) parent = index.parent() self.model().dropMimeData( event.mimeData(), event.dropAction(), index.row(), index.column(), parent ) event.accept() def _rebind_dragged_widgets(self): """遍历所有item,匹配缓存的控件重新绑定""" if not self._drag_temp_widgets: return # 遍历所有顶层item(如果有层级嵌套需要加递归遍历子item逻辑) for row in range(self.topLevelItemCount()): item = self.topLevelItem(row) item_id = item.text(0) if item_id in self._drag_temp_widgets: for col, widget in self._drag_temp_widgets[item_id].items(): self.setItemWidget(item, col, widget) # 清空缓存 self._drag_temp_widgets.clear() 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()
注意:如果Tree存在多层级嵌套Item,需要在
_rebind_dragged_widgets方法中增加递归遍历子Item的逻辑,确保所有层级的新Item都能被匹配到。如果存在同名Item,不要用文本做唯一标识,改用Qt.UserRole角色存储自定义唯一ID匹配即可。
内容的提问来源于stack exchange,提问作者Jesse
相关产品推荐
相关产品推荐

