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

Mac端PySide2 QTreeWidget拖拽项消失、内嵌控件丢失问题(Python2.7)

问题背景

运行环境及现象:

  • 系统环境:Mac Monterey 12.4、Python 2.7 + PySide2、Maya软件内嵌UI开发场景
  • 异常表现:
    • QTreeWidget开启InternalMove拖拽模式后,拖拽项会导致其他树节点意外消失,同逻辑此前在Windows平台可正常运行
    • 拖拽完成后,通过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)


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()
问题原因
  1. 拖拽后项消失:Mac端Qt对InternalMove模式的默认拖拽实现存在平台兼容问题,当项设置了~Qt.ItemIsDropEnabled禁止子项拖入的flag时,drop位置的坐标判定逻辑与Windows不一致,默认处理流程会错误删除非拖拽目标的相邻节点,导致树结构项丢失。
  2. 拖拽后控件丢失: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 02:24:34