Skip to content
正常

TreeDragMove 成品导览

sourcemodel/01-mv-pattern/tree-drag-move/ related拖放系统(入门)QTreeWidget(入门)

TreeDragMove 是个 QTreeWidget 子类——把节点拖来拖去改层级,看着像「文件管理器那一坨」,但它的魂在重写 Qt 拖放协议的四个虚函数。很多人只会在 setDragDropMode(InternalMove) 一开了事,拖是能拖,可一旦想「拖到节点上变子项 / 拖到空白变同级」分不清、或想跨树转移,默认行为就不够用了。这件成品把协议四件套全部接管,把「拖放到底是怎么一回事」讲透。

本篇是「成品导览」

想直接用成品 → 看这里(架构 / 决策 / 踩坑 / 怎么读)。 想自己从零搓出来 → 转 手搓手册

1. 它做什么

一个 AwesomeQt::TreeDragMove 控件(QTreeWidget 子类):

  • 重写拖放协议四件套mimeTypes() / mimeData() / supportedDropActions() / dropMimeData()——完全接管「拖出时序列化什么、拖入时怎么落地」
  • 内部移动语义supportedDropActions() 返回 Qt::MoveActionInternalMove 模式下拖完源位置真正删除(不是隐藏)
  • 落点判定dropEventitemAt(pos) 区分「落在节点上(成为其子节点)」与「落在空白(同级插入)」
  • 选中态 / 展开态维护:拖完自动展开目标父节点、把选中态切到新落点
  • 跨树转移:demo 配两棵树,左拖右 = 节点从源树到目标树

跑起来看一眼:

Shell
cmake -S model/01-mv-pattern/tree-drag-move -B build -DCMAKE_PREFIX_PATH=/usr/lib/cmake/Qt6
cmake --build build -j"$(nproc)"
./build/demo/tree-drag-move_demo

demo 里左树是「项目结构」(src / tests / docs),右树是「回收区」。单树内拖 = 改层级;左拖到右 = 转移。Ctrl+拖 = 复制保留源。

2. 架构总览

拖放协议时序

一次内部拖拽,框架会按固定顺序回调这几个虚函数。看懂这张图就懂了「协议」二字:

关键:mimeData(拖出)和 dropMimeData(拖入)是一对,中间靠 QMimeData 传话。自定义 MIME 类型让两端对得上。

文件职责

文件职责
include/tree-drag-move.h接口:TreeDragMove 声明 + kMimeType 常量 + 四个 protected 虚函数
src/tree-drag-move.cpp实现:协议四件套 + dropEvent 落点判定 / 删源 / 状态维护 + cloneItem 深拷贝
demo/tree-drag-move_window.cpp演示:拖放标志位四件套(setDragEnabled 等)+ 两棵树 + 种子数据 + 状态栏说明

协议四件套各自的活

虚函数何时被调干什么
mimeTypes()拖出前声明本树能产出的 MIME 类型(src/tree-drag-move.cpp:36
mimeData(items)拖出时把源节点序列化进 QMimeData(src/tree-drag-move.cpp:42
supportedDropActions()拖拽中告诉框架本树接受哪些动作(这里只 MoveAction,src/tree-drag-move.cpp:31
dropMimeData(parent,index,data,action)拖入时反序列化 + 把节点插到 (parent,index)(src/tree-drag-move.cpp:69

3. 关键设计决策

① 自定义 MIME 类型 + 指针编码,不靠 text/plain 传节点。 进程内拖拽,用 application/x-awesomeqt-treedragmove 装源节点指针(QDataStreamquintptr),drop 端原样取回,零歧义定位源。text/plain 只是给跨进程/日志留的可读通道。若只靠文本传,同名节点会分不清谁是谁。(src/tree-drag-move.cpp:42-66

supportedDropActions() 只返回 MoveAction,InternalMove 语义收窄。 默认 QAbstractItemView 会返回 Move|Copy,导致按 Ctrl 变复制、松手变移动,行为飘。这里收窄成纯 MoveAction,单树内拖必删源,行为可预测。跨树转移在 demo 里靠两棵树各自的 AcceptDrops 完成,不是靠 CopyAction。(src/tree-drag-move.cpp:31-34

③ 删源放在 dropEvent,不放 dropMimeData——防时序坑。dropMimeData 里若先删源,一旦基类 dropEvent 默认实现也在删(InternalMove 下它会),就 double-free 或取空。所以协议层只负责「克隆+插入」,删源集中到 dropEvent 的 MoveAction 分支,且先调基类 dropEvent(完成复制)再删源,顺序锁死。(src/tree-drag-move.cpp:163-185

④ 落点判定用 itemAt(pos),不靠已弃用的 target 精确语义。dropEventQTreeWidgetItem* target_parent = itemAt(event->position().toPoint()):返回非 null = 落在某节点上(成为子节点),null = 落空白(同级插入)。这比纠结 index 的精确含义稳,且和用户视觉一致。(src/tree-drag-move.cpp:144

⑤ 深拷贝 cloneItem 递归复制整棵子树。 拖一个有子节点的父节点,子树必须跟着走。cloneItem 递归复制 text/icon/toolTip/UserRole/checkState + 所有 child,保证克隆和源结构一致。MoveAction 下源连同子树一起删,不会漏。(src/tree-drag-move.cpp:201-222

4. 怎么读这份 code

按这个顺序读,最快建立心智:

  1. include/tree-drag-move.h 的类注释include/tree-drag-move.h:17-32)——先看「协议四件套各干什么 + 教学点对应」
  2. kMimeType 常量src/tree-drag-move.cpp:24)——自定义 MIME 的名字
  3. mimeTypes + mimeDatasrc/tree-drag-move.cpp:36-66)——拖出端:声明类型 + 编码源
  4. dropMimeDatasrc/tree-drag-move.cpp:69-119)——拖入端:反序列化 + 克隆 + 插入(注意这里故意不删源)
  5. dropEventsrc/tree-drag-move.cpp:122-198)——落点判定 + 删源 + 状态维护,整件最重头的逻辑
  6. cloneItemsrc/tree-drag-move.cpp:201-222)——深拷贝子树
  7. demo 的拖放标志位demo/tree-drag-move_window.cpp:40-49)——setDragEnabled/setAcceptDrops/setDragDropMode/setDefaultDropAction 四件套

入口:demo/main.cppdemo/tree-drag-move_window.cpp 跑起来,对照读。

5. 踩坑

#现象原因后果解法
只开 setDragDropMode(InternalMove),拖动没反应忘了 setDragEnabled(true)setAcceptDrops(true)——前者是拖出总开关,后者是拖入总开关拖不动 / 拖到别处不接收四个标志位都开(demo/tree-drag-move_window.cpp:40-42
InternalMove 下源没删,节点出现两份dropMimeData 只复制没删源,又没在 dropEvent 补删数据重复MoveAction 分支手动 takeChild+delete 源(src/tree-drag-move.cpp:166-185
dropMimeData 里删源,偶发崩溃 / double-free基类 QTreeWidget::dropEvent 在 InternalMove 下也会删源,和你的删源撞车segfault协议层只复制,删源集中到 dropEvent,且先调基类再删(src/tree-drag-move.cpp:163-167
跨树拖拽后源树节点没清干净indexOfTopLevelItem/indexOfChild 返回 -1 没判,或父节点是另一棵树的静默漏删删源前判 i >= 0,分 parent!=null 和 null 两路(src/tree-drag-move.cpp:172-186
拖个带子节点的父节点,子节点丢一半克隆只复制了顶层项,没递归 child子树残缺cloneItem 递归深拷贝(src/tree-drag-move.cpp:213-219
拖完选中态停在已被删除的源上QTreeWidget dropEvent 后默认选中没刷新选中幽灵项 → 操作崩溃dropEvent 末尾手动 setCurrentItem 切到新落点(src/tree-drag-move.cpp:190-198
落在某节点上但新子节点看不见目标父节点是折叠的以为没拖成功落点是节点就 setExpanded(true)src/tree-drag-move.cpp:194
用 SIGNAL/SLOT 宏连 expand 动作老式宏语法编译期不查类型,改名就崩一律函数指针语法 connect(act,&QAction::triggered,...)demo/tree-drag-move_window.cpp:72

6. 官方文档


这套「重写协议四件套 + dropEvent 落点判定」不是 TreeDragMove 专属——它是「任何想精细控制拖放的 Item View」的标准范式。换成 QListView / QTableView 同一套骨架。想自己搓?手搓手册带你从能拖动的 QTreeWidget 一行行搓到协议全接管。

AwesomeQt v0.3.0-13-g31f4d47 · 31f4d47 · 2026-09-22 · CHECKED-BY: CI