You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PySide2中QTreeWidget拖拽后如何保留QTreeWidgetItem关联控件

QTreeWidget内部拖拽后setItemWidget绑定控件丢失修复方案

问题现象

使用自定义QTreeWidget实现内部拖拽排序功能时,拖拽放置完成后,QTreeWidgetItem通过setItemWidget绑定的关联控件会直接丢失。
已知该现象的触发逻辑:Qt默认内部拖拽机制会在拖拽过程中销毁被拖拽的原Item,在放置位置重新生成新的Item对象,绑定在原Item上的控件会随原对象销毁被回收。
代码同时兼容Maya平台运行:Mac端Maya存在已知bug,默认拖拽逻辑会直接删除被拖拽Item,必须重写dropEvent修复该问题,修复后即可复现控件丢失问题——例如下方示例中拖拽item5重排序后,其绑定的按钮控件会消失:
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)
        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自定义,通过控件临时重设父对象+拖拽后匹配重绑即可实现,步骤如下:

  1. 重写startDrag方法:拖拽启动前遍历所有选中Item,将Item的唯一标识(示例中用第一列文本做标识,实际业务可替换为自定义数据角色存储的唯一ID)、对应列的控件存入缓存字典,同时将控件的父对象临时设置为Tree控件本身,避免原Item销毁时控件被回收。
  2. 执行原有拖拽逻辑(保留Maya Mac端的dropEvent兼容修复),等待新Item创建完成。
  3. 拖拽操作完成后,遍历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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 12:31:01