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())
关键实现说明
- 数据结构:用
TreeItem类维护树形结构,包含父项、子项列表和数据,确保模型能正确遍历和修改。 - 拖拽基础配置:
supportedDragActions指定支持移动操作;flags给项添加拖拽和放置权限;mimeTypes自定义MIME类型,用于传输项数据。
- MIME数据处理:
mimeData将源项的路径(从根到项的行号序列)编码到MIME数据中,确保能准确找到源项;dropMimeData解码MIME数据,获取源项,然后根据放置位置判断操作类型:- 拖到空白区域或顶级项的上下:作为顶级项插入;
- 拖到同级项的上下:插入到同级位置;
- 拖到项上:作为该项的子项添加。
- 模型更新:使用
beginMoveRows和endMoveRows通知视图数据变化,确保界面正确刷新,同时手动修改TreeItem的父子关系和列表顺序。
注意事项
- 必须给模型设置关联的
QTreeView(通过setView方法),才能在dropMimeData中获取放置指示器位置; - 移动时要检查源项和目标项的层级关系,避免拖到自身或子项中,防止循环引用;
setDragDropMode设置为InternalMove,确保只允许内部拖拽移动。
内容的提问来源于stack exchange,提问作者Thomas.C
相关产品推荐
相关产品推荐

