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

PySide6中实现QTreeView拖拽重排功能的技术求助

PySide6 QTreeView 完整拖拽实现方案

以下是满足你三个需求的完整实现代码,基于QAbstractItemModel,支持项重排、拖拽为子项、子项转为顶级项:

from PySide6.QtCore import (Qt, QModelIndex, QAbstractItemModel,
                            QMimeData, QByteArray, QDataStream)
from PySide6.QtWidgets import (QApplication, QTreeView, QWidget,
                               QVBoxLayout)

class TreeItem:
    def __init__(self, data, parent=None):
        self.parent_item = parent
        self.item_data = data
        self.child_items = []

    def append_child(self, item):
        self.child_items.append(item)

    def child(self, row):
        return self.child_items[row] if 0 <= row < len(self.child_items) else None

    def child_count(self):
        return len(self.child_items)

    def column_count(self):
        return len(self.item_data)

    def data(self, column):
        return self.item_data[column] if 0 <= column < len(self.item_data) else None

    def parent(self):
        return self.parent_item

    def row(self):
        if self.parent_item:
            return self.parent_item.child_items.index(self)
        return 0

class TreeModel(QAbstractItemModel):
    def __init__(self, data, parent=None):
        super().__init__(parent)
        self.root_item = TreeItem(["Item"])
        self.setup_model_data(data, self.root_item)

    def setup_model_data(self, data, parent):
        for item_data in data:
            child = TreeItem(item_data, parent)
            parent.append_child(child)
            if isinstance(item_data, list) and len(item_data) > 1:
                self.setup_model_data(item_data[1], child)

    def index(self, row, column, parent=QModelIndex()):
        if not self.hasIndex(row, column, parent):
            return QModelIndex()

        parent_item = self.root_item if not parent.isValid() else parent.internalPointer()
        child_item = parent_item.child(row)
        return self.createIndex(row, column, child_item) if child_item else QModelIndex()

    def parent(self, index):
        if not index.isValid():
            return QModelIndex()

        child_item = index.internalPointer()
        parent_item = child_item.parent()

        return self.createIndex(parent_item.row(), 0, parent_item) if parent_item != self.root_item else QModelIndex()

    def rowCount(self, parent=QModelIndex()):
        parent_item = self.root_item if not parent.isValid() else parent.internalPointer()
        return parent_item.child_count()

    def columnCount(self, parent=QModelIndex()):
        return self.root_item.column_count()

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None

        if role != Qt.DisplayRole:
            return None

        item = index.internalPointer()
        return item.data(index.column())

    # 拖拽相关重写
    def supportedDragActions(self):
        return Qt.MoveAction

    def flags(self, index):
        if not index.isValid():
            return Qt.ItemIsEnabled
        return super().flags(index) | Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled

    def mimeTypes(self):
        return ["application/x-treeitem"]

    def mimeData(self, indexes):
        mime_data = QMimeData()
        encoded_data = QByteArray()
        stream = QDataStream(encoded_data, QDataStream.WriteOnly)

        for index in indexes:
            if index.isValid():
                item = index.internalPointer()
                # 记录项的完整路径(从根到当前项的行号)
                path = []
                current = item
                while current != self.root_item:
                    path.append(current.row())
                    current = current.parent()
                path.reverse()
                stream.writeInt32(len(path))
                for row in path:
                    stream.writeInt32(row)

        mime_data.setData("application/x-treeitem", encoded_data)
        return mime_data

    def dropMimeData(self, data, action, row, column, parent):
        if action != Qt.MoveAction:
            return False

        if not data.hasFormat("application/x-treeitem"):
            return False

        encoded_data = data.data("application/x-treeitem")
        stream = QDataStream(encoded_data, QDataStream.ReadOnly)

        # 读取源项路径
        path_length = stream.readInt32()
        source_path = [stream.readInt32() for _ in range(path_length)]
        # 获取源项索引
        source_index = QModelIndex()
        current_parent = self.root_item
        for row_num in source_path:
            source_index = self.index(row_num, 0, self.createIndex(current_parent.row(), 0, current_parent) if current_parent != self.root_item else QModelIndex())
            current_parent = source_index.internalPointer()

        if not source_index.isValid():
            return False

        # 处理放置位置
        source_item = source_index.internalPointer()
        source_parent_item = source_item.parent()

        # 情况1: 拖到顶级位置(parent无效,row为指定位置)
        if not parent.isValid():
            target_parent_item = self.root_item
            target_row = row if row != -1 else self.rowCount(parent)
        else:
            target_parent_item = parent.internalPointer()
            # 判断放置指示器位置
            drop_pos = self.view.dropIndicatorPosition()
            if drop_pos == QTreeView.AboveItem or drop_pos == QTreeView.BelowItem:
                # 情况2: 拖到同级项的上下,作为同级项插入
                target_parent_item = target_parent_item.parent()
                target_row = parent.row() + (1 if drop_pos == QTreeView.BelowItem else 0)
            else:
                # 情况3: 拖到项上,作为子项
                target_row = self.rowCount(parent)

        # 避免拖到自身或自身的子项
        ancestor = target_parent_item
        while ancestor:
            if ancestor == source_item:
                return False
            ancestor = ancestor.parent()

        # 开始移动
        self.beginMoveRows(
            self.createIndex(source_parent_item.row(), 0, source_parent_item) if source_parent_item != self.root_item else QModelIndex(),
            source_item.row(),
            source_item.row(),
            self.createIndex(target_parent_item.row(), 0, target_parent_item) if target_parent_item != self.root_item else QModelIndex(),
            target_row
        )

        # 更新数据结构
        source_parent_item.child_items.remove(source_item)
        target_parent_item.child_items.insert(target_row, source_item)
        source_item.parent_item = target_parent_item

        self.endMoveRows()
        return True

    def setView(self, view):
        self.view = view

if __name__ == "__main__":
    import sys
    app = QApplication(sys.argv)

    # 测试数据
    data = [
        ["Item 1"],
        ["Item 2", [
            ["Subitem 2-1"],
            ["Subitem 2-2"]
        ]],
        ["Item 3"],
        ["Item 4"]
    ]

    window = QWidget()
    layout = QVBoxLayout(window)
    tree_view = QTreeView()
    model = TreeModel(data)
    model.setView(tree_view)
    tree_view.setModel(model)
    tree_view.setDragDropMode(QTreeView.InternalMove)
    tree_view.setDefaultDropAction(Qt.MoveAction)
    tree_view.expandAll()

    layout.addWidget(tree_view)
    window.resize(400, 300)
    window.show()
    sys.exit(app.exec())

关键实现说明

  1. 数据结构:用TreeItem类维护树形结构,包含父项、子项列表和数据,确保模型能正确遍历和修改。
  2. 拖拽基础配置:
    • supportedDragActions指定支持移动操作;
    • flags给项添加拖拽和放置权限;
    • mimeTypes自定义MIME类型,用于传输项数据。
  3. MIME数据处理:
    • mimeData将源项的路径(从根到项的行号序列)编码到MIME数据中,确保能准确找到源项;
    • dropMimeData解码MIME数据,获取源项,然后根据放置位置判断操作类型:
      • 拖到空白区域或顶级项的上下:作为顶级项插入;
      • 拖到同级项的上下:插入到同级位置;
      • 拖到项上:作为该项的子项添加。
  4. 模型更新:使用beginMoveRows和endMoveRows通知视图数据变化,确保界面正确刷新,同时手动修改TreeItem的父子关系和列表顺序。

注意事项

  • 必须给模型设置关联的QTreeView(通过setView方法),才能在dropMimeData中获取放置指示器位置;
  • 移动时要检查源项和目标项的层级关系,避免拖到自身或子项中,防止循环引用;
  • setDragDropMode设置为InternalMove,确保只允许内部拖拽移动。

内容的提问来源于stack exchange,提问作者Thomas.C

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 15:43:12