目录
[双 QTreeWidget 拖拽节点完整可运行代码](#双 QTreeWidget 拖拽节点完整可运行代码)
[双 QTreeWidget 树形拖拽程序 完整功能介绍(Qt5.9.9 MSVC2015 64 位,全中文界面)](#双 QTreeWidget 树形拖拽程序 完整功能介绍(Qt5.9.9 MSVC2015 64 位,全中文界面))
[1. 两种拖拽模式一键切换](#1. 两种拖拽模式一键切换)
[2. 跨树互相拖拽(左 ↔ 右)](#2. 跨树互相拖拽(左 ↔ 右))
[3. 同树内部层级调整](#3. 同树内部层级调整)
[4. 拖拽安全防护](#4. 拖拽安全防护)
[三、右键上下文菜单功能(新增 / 删除节点)](#三、右键上下文菜单功能(新增 / 删除节点))
双 QTreeWidget 拖拽节点完整可运行代码
pro
QT += core gui widgets
CONFIG += c++11
# MSVC编译器全局开启UTF-8编码,解决中文常量换行报错
msvc {
QMAKE_CXXFLAGS += /utf-8
}
win32-msvc*{
QMAKE_CXXFLAGS += /utf-8
}
SOURCES += \
main.cpp \
DragTree.cpp
HEADERS += \
DragTree.h
DESTDIR = bin
MOC_DIR = tmp/moc
OBJECTS_DIR = tmp/obj
UI_DIR = tmp/ui
TARGET = TreeDragDemo
TEMPLATE = app
DragTree.h
运行
#ifndef DRAGTREE_H
#define DRAGTREE_H
#include <QTreeWidget>
#include <QMouseEvent>
#include <QDrag>
#include <QMimeData>
#include <QByteArray>
#include <QDataStream>
#include <QPoint>
#include <QList>
#include <Qt>
#include <QMenu>
#include <QAction>
/**
* @brief 树形节点序列化数据包结构体
* 用于拖拽时递归保存节点文本、自定义数据、所有子节点信息
*/
struct NodePacket
{
QString text; // 节点显示文本
QString userData; // 节点绑定的自定义业务数据(Qt::UserRole)
QList<NodePacket> childs;// 子节点数据包数组
};
/**
* @brief 自定义树形控件:支持跨树拖拽、右键增删节点
* 适配Qt5.9.9 MSVC2015 64位,修复所有override、鼠标接口编译报错
* 核心功能:
* 1. 双TreeWidget互相拖拽,支持移动/复制两种模式切换
* 2. 递归序列化多层子节点,拖拽不会丢失整棵子树
* 3. 右键上下文菜单:新增同级节点、新增子节点、删除节点
* 4. 安全删除任意层级节点,不会下标越界崩溃
*/
class DragTreeWidget : public QTreeWidget
{
Q_OBJECT
public:
explicit DragTreeWidget(QWidget *parent = nullptr);
/**
* @brief 设置拖拽行为模式
* @param isCopy true=复制模式(源节点保留) false=移动模式(跨树删除源节点)
*/
void setDragCopyMode(bool isCopy);
protected:
// 鼠标按下事件:记录拖拽起始坐标、当前点击的源节点
void mousePressEvent(QMouseEvent *event) override;
// 鼠标移动事件:判断拖动距离,启动拖拽流程
void mouseMoveEvent(QMouseEvent *event) override;
// 拖拽进入控件:校验拖拽数据格式是否合法
void dragEnterEvent(QDragEnterEvent *event) override;
// 拖拽在控件内移动:持续接收拖拽动作
void dragMoveEvent(QDragMoveEvent *event) override;
// 鼠标松开放下拖拽:反序列化生成新节点,处理源节点删除逻辑
void dropEvent(QDropEvent *event) override;
// 右键菜单触发入口
void contextMenuEvent(QContextMenuEvent *event) override;
private:
/**
* @brief 递归打包节点所有信息为数据包
* @param item 需要打包的树形节点
* @return 完整节点数据包(包含全部子节点)
*/
NodePacket packItem(QTreeWidgetItem* item) const;
/**
* @brief 节点数据包转为二进制字节流,用于拖拽传输
* @param pkt 节点数据包
* @return 序列化二进制数组
*/
QByteArray packetToBytes(const NodePacket& pkt) const;
/**
* @brief 二进制字节流还原节点数据包
* @param data 拖拽传来的二进制数据
* @return 还原后的结构化节点数据包
*/
NodePacket bytesToPacket(const QByteArray& data);
/**
* @brief 根据数据包递归重建完整树形节点
* @param pkt 节点数据包
* @return 新建完成的QTreeWidgetItem
*/
QTreeWidgetItem* buildItem(const NodePacket& pkt);
/**
* @brief 安全删除任意层级节点(顶层/子节点通用,不会下标越界崩溃)
* @param item 待删除树形节点
*/
void deleteTreeItem(QTreeWidgetItem* item);
/**
* @brief 构建右键弹出菜单
* @param item 当前右键点击的节点,nullptr代表空白区域
* @param globalPos 菜单弹出全局屏幕坐标
*/
void createContextMenu(QTreeWidgetItem* item, const QPoint& globalPos);
QPoint m_mousePressPos; // 鼠标左键按下时的本地坐标,用于计算拖拽距离
QTreeWidgetItem* m_dragSrcItem; // 本次拖拽的源节点缓存,防止删错其他节点
Qt::DropAction m_dragActionMode;// 拖拽模式:Move移动 / Copy复制
QTreeWidgetItem* m_rightClickItem;// 本次右键点击的节点
};
#endif // DRAGTREE_H
DragTree.cpp
cpp
运行
#include "DragTree.h"
#include <QApplication>
#include <QInputDialog>
#include <QMessageBox>
/**
* @brief 构造函数:初始化树形拖拽配置、右键菜单开关、成员变量
*/
DragTreeWidget::DragTreeWidget(QWidget *parent)
: QTreeWidget(parent),
m_dragSrcItem(nullptr),
m_dragActionMode(Qt::MoveAction),
m_rightClickItem(nullptr)
{
setDragEnabled(true); // 开启节点拖拽功能
setAcceptDrops(true); // 允许接收外部拖入的数据
setDropIndicatorShown(true); // 显示拖拽放置位置指示横线
setUniformRowHeights(true); // 统一行高,拖拽指示器渲染正常
setSelectionMode(QAbstractItemView::SingleSelection); // 单选模式
setHeaderLabel("节点"); // 表头中文
setContextMenuPolicy(Qt::DefaultContextMenu); // 启用默认右键菜单事件
}
/**
* @brief 外部接口:切换复制/移动拖拽模式
*/
void DragTreeWidget::setDragCopyMode(bool isCopy)
{
m_dragActionMode = isCopy ? Qt::CopyAction : Qt::MoveAction;
}
/**
* @brief 鼠标按下:缓存按下坐标与点击节点
*/
void DragTreeWidget::mousePressEvent(QMouseEvent *event)
{
QTreeWidget::mousePressEvent(event);
// 仅处理左键按下
if (event->button() == Qt::LeftButton)
{
m_mousePressPos = event->pos();
m_dragSrcItem = itemAt(event->pos());
}
}
/**
* @brief 鼠标移动:判断是否达到拖拽阈值,启动拖拽
*/
void DragTreeWidget::mouseMoveEvent(QMouseEvent *event)
{
QTreeWidget::mouseMoveEvent(event);
// 非左键拖动直接返回
if (!(event->buttons() & Qt::LeftButton))
return;
QTreeWidgetItem* curItem = m_dragSrcItem;
if (!curItem)
return;
if (curItem->text(0).trimmed().isEmpty())
return;
int dist = (event->pos() - m_mousePressPos).manhattanLength();
if (dist < QApplication::startDragDistance())
return;
// 1. 将选中节点打包为二进制数据包
NodePacket pkt = packItem(curItem);
QByteArray buf = packetToBytes(pkt);
// 2. 构造拖拽载体与mime数据
QDrag* drag = new QDrag(this);
QMimeData* mime = new QMimeData;
mime->setData("tree/itemdata", buf); // 自定义拖拽数据标识
drag->setMimeData(mime);
// 3. 执行拖拽,传入当前设置的移动/复制模式
drag->exec(m_dragActionMode);
}
/**
* @brief 拖拽进入控件:校验数据标识,只接收本程序树形拖拽数据
*/
void DragTreeWidget::dragEnterEvent(QDragEnterEvent *event)
{
if (event->mimeData()->hasFormat("tree/itemdata"))
{
event->acceptProposedAction();
}
}
/**
* @brief 拖拽在控件内持续移动,保持接收状态
*/
void DragTreeWidget::dragMoveEvent(QDragMoveEvent *event)
{
event->acceptProposedAction();
}
/**
* @brief 拖拽放下核心逻辑:反序列化生成节点、跨树删除源节点
*/
void DragTreeWidget::dropEvent(QDropEvent *event)
{
QByteArray data = event->mimeData()->data("tree/itemdata");
// 空数据直接中断
if (data.isEmpty())
{
m_dragSrcItem = nullptr; // 清空拖拽缓存
return;
}
// 二进制还原节点数据包
NodePacket pkt = bytesToPacket(data);
QTreeWidgetItem* newItem = buildItem(pkt);
if (!newItem)
{
m_dragSrcItem = nullptr;
return;
}
// 判断放置目标:拖到节点内变为子节点,拖空白为顶层节点
QTreeWidgetItem* targetItem = itemAt(event->pos());
if (targetItem)
{
targetItem->addChild(newItem);
targetItem->setExpanded(true); // 自动展开父节点
}
else
{
addTopLevelItem(newItem);
}
// 移动模式逻辑:仅跨两棵树时删除源节点,同树拖拽保留原节点
if (event->proposedAction() == Qt::MoveAction)
{
QTreeWidgetItem* srcItem = m_dragSrcItem;
if (srcItem)
{
DragTreeWidget* srcTree = qobject_cast<DragTreeWidget*>(srcItem->treeWidget());
DragTreeWidget* targetTree = this;
if (srcTree && srcTree != targetTree)
{
srcTree->deleteTreeItem(srcItem);
}
}
}
event->acceptProposedAction();
m_dragSrcItem = nullptr; // 拖拽完成清空缓存,防止下次误删
}
/**
* @brief 右键菜单触发,获取点击节点并创建菜单
*/
void DragTreeWidget::contextMenuEvent(QContextMenuEvent *event)
{
m_rightClickItem = itemAt(event->pos());
createContextMenu(m_rightClickItem, event->globalPos());
}
void DragTreeWidget::createContextMenu(QTreeWidgetItem* item, const QPoint& globalPos)
{
QMenu menu;
QAction* actAddSibling = new QAction(QString::fromUtf8("新增同级节点"), &menu);
QAction* actAddChild = new QAction(QString::fromUtf8("新增zi节点"), &menu);
QAction* actDelete = new QAction("删除当前节点", &menu);
menu.addAction(actAddSibling);
menu.addAction(actAddChild);
menu.addSeparator();
menu.addAction(actDelete);
// 新增同级节点槽函数
QObject::connect(actAddSibling, &QAction::triggered, [=](){
QString name = QInputDialog::getText(nullptr, "新增节点", "输入节点名称:");
if(name.isEmpty()) return;
QTreeWidgetItem* newItem = new QTreeWidgetItem(QStringList() << name);
newItem->setFlags(newItem->flags() | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
// 右键有节点:在该节点下方插入同级
if(item)
{
QTreeWidgetItem* parent = item->parent();
if(parent)
parent->insertChild(parent->indexOfChild(item)+1, newItem);
else
insertTopLevelItem(indexOfTopLevelItem(item)+1, newItem);
}
// 右键空白区域:新增顶层节点
else
{
addTopLevelItem(newItem);
}
});
QObject::connect(actAddChild, &QAction::triggered, [=](){
QString name = QInputDialog::getText(nullptr, "新增zi节点", "输入zi节点名称:");
if(name.isEmpty()) return;
QTreeWidgetItem* newItem = new QTreeWidgetItem(QStringList() << name);
newItem->setFlags(newItem->flags() | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
if(item)
{
item->addChild(newItem);
item->setExpanded(true);
}
else
{
addTopLevelItem(newItem);
}
});
// 删除节点槽函数,弹窗二次确认
QObject::connect(actDelete, &QAction::triggered, [=](){
if(!item) return;
int ret = QMessageBox::question(nullptr, "确认删除", "确定删除该节点及所有子节点?");
if(ret != QMessageBox::Yes) return;
deleteTreeItem(item);
});
// 在鼠标全局坐标弹出菜单
menu.exec(globalPos);
}
/**
* @brief 递归打包节点所有文本、自定义数据、全部子节点
*/
NodePacket DragTreeWidget::packItem(QTreeWidgetItem *item) const
{
NodePacket pkt;
pkt.text = item->text(0);
pkt.userData = item->data(0, Qt::UserRole).toString();
int childCnt = item->childCount();
// 递归遍历所有子节点打包
for (int i = 0; i < childCnt; i++)
{
pkt.childs.append(packItem(item->child(i)));
}
return pkt;
}
/**
* @brief 节点数据包序列化为二进制字节流
*/
QByteArray DragTreeWidget::packetToBytes(const NodePacket &pkt) const
{
QByteArray buf;
QDataStream stream(&buf, QIODevice::WriteOnly);
// 写入当前节点信息、子节点数量
stream << pkt.text << pkt.userData << pkt.childs.size();
// 递归拼接所有子节点二进制数据
for (const auto& child : pkt.childs)
{
buf.append(packetToBytes(child));
}
return buf;
}
/**
* @brief 二进制数据流反序列化,还原完整节点数据包(修复多层子节点截断丢失bug)
*/
NodePacket DragTreeWidget::bytesToPacket(const QByteArray &data)
{
NodePacket pkt;
QByteArray temp = data;
QDataStream stream(&temp, QIODevice::ReadOnly);
int childCount;
stream >> pkt.text >> pkt.userData >> childCount;
// 读取完当前节点后,剩余字节全部为子节点数据包
qint64 readPos = stream.device()->pos();
QByteArray remainBuf = temp.mid(readPos);
for (int i = 0; i < childCount; i++)
{
NodePacket childPkt = bytesToPacket(remainBuf);
pkt.childs.append(childPkt);
// 截断已读取的子包数据,避免重复解析
QByteArray childFullBuf = packetToBytes(childPkt);
remainBuf = remainBuf.mid(childFullBuf.size());
}
return pkt;
}
/**
* @brief 根据数据包递归生成完整树形节点(包含所有嵌套子节点)
*/
QTreeWidgetItem *DragTreeWidget::buildItem(const NodePacket &pkt)
{
QTreeWidgetItem* item = new QTreeWidgetItem(QStringList() << pkt.text);
item->setData(0, Qt::UserRole, pkt.userData);
// 开启节点可选中、可拖拽标记
item->setFlags(item->flags() | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
// 递归创建所有子节点
for (const auto& childPkt : pkt.childs)
{
item->addChild(buildItem(childPkt));
}
return item;
}
/**
* @brief 安全删除节点:自动区分顶层/子节点,不会下标越界崩溃
*/
void DragTreeWidget::deleteTreeItem(QTreeWidgetItem *item)
{
if (!item) return;
QTreeWidgetItem* parent = item->parent();
if (parent)
{
// 有父节点:从父节点移除子项
parent->removeChild(item);
}
else
{
// 无父节点:顶层节点,获取下标移除
int idx = indexOfTopLevelItem(item);
if (idx >= 0)
{
takeTopLevelItem(idx);
}
}
delete item; // 释放节点内存
}
main.cpp
cpp
运行
#include <QApplication>
#include <QMainWindow>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QPushButton>
#include <QObject>
#include "DragTree.h"
/**
* @brief 程序入口函数
* 主窗口布局:顶部切换按钮 + 左右双树形控件
*/
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QMainWindow w;
w.resize(1000, 600);
w.setWindowTitle("双树控件跨树拖拽演示");
// 中心部件与垂直总布局
QWidget* central = new QWidget(&w);
QVBoxLayout* mainVLayout = new QVBoxLayout(central);
mainVLayout->setSpacing(10);
mainVLayout->setContentsMargins(15,15,15,15);
w.setCentralWidget(central);
// 顶部按钮布局:切换移动/复制模式
QHBoxLayout* btnLayout = new QHBoxLayout;
QPushButton* btnMove = new QPushButton("移动模式");
QPushButton* btnCopy = new QPushButton("复制模式");
btnLayout->addWidget(btnMove);
btnLayout->addWidget(btnCopy);
btnLayout->addStretch();
mainVLayout->addLayout(btnLayout);
// 左右树形水平布局
QHBoxLayout* treeLayout = new QHBoxLayout;
treeLayout->setSpacing(20);
// 左侧树形控件 + 测试演示数据
DragTreeWidget* treeLeft = new DragTreeWidget;
treeLeft->setHeaderLabel("左侧树形");
QTreeWidgetItem* g1 = new QTreeWidgetItem(treeLeft, {"分组1"});
g1->setData(0, Qt::UserRole, "id_001");
g1->addChild(new QTreeWidgetItem({"子项1-1"}));
g1->addChild(new QTreeWidgetItem({"子项1-2"}));
treeLeft->addTopLevelItem(new QTreeWidgetItem({"分组2"}));
// 右侧树形控件 + 初始空白分组
DragTreeWidget* treeRight = new DragTreeWidget;
treeRight->setHeaderLabel("右侧树形");
treeRight->addTopLevelItem(new QTreeWidgetItem({"目标分组A"}));
treeLayout->addWidget(treeLeft);
treeLayout->addWidget(treeRight);
mainVLayout->addLayout(treeLayout);
// 绑定按钮槽函数,切换两棵树的拖拽模式
QObject::connect(btnMove, &QPushButton::clicked, [=](){
treeLeft->setDragCopyMode(false);
treeRight->setDragCopyMode(false);
});
QObject::connect(btnCopy, &QPushButton::clicked, [=](){
treeLeft->setDragCopyMode(true);
treeRight->setDragCopyMode(true);
});
w.show();
return a.exec();
}
双 QTreeWidget 树形拖拽程序 完整功能介绍(Qt5.9.9 MSVC2015 64 位,全中文界面)
一、整体界面布局
- 窗口垂直布局:顶部模式切换按钮 + 下方左右两个独立树形控件;
- 两个树形完全独立,支持节点互相拖拽交互;
- 内置演示测试数据:左侧多层分组父子节点,右侧预设空白分组,开箱即可测试;
- 全中文界面:窗口标题、按钮、表头、弹窗提示、右键菜单全部中文显示。
二、拖拽核心功能(无崩溃、数据不丢失)
1. 两种拖拽模式一键切换
窗口顶部提供「移动模式」「复制模式」按钮,全局切换两棵树拖拽行为:
- 移动模式(默认)
- 跨树拖拽:源树原始节点删除,完整迁移到目标树;
- 同树内部拖拽:仅调整层级顺序,源节点保留不删除。
- 复制模式
- 拖拽生成节点副本,原树形节点完整保留;
- 拖动父节点时整套多层子树同步复制,子节点不会丢失。
2. 跨树互相拖拽(左 ↔ 右)
- 任意顶层分组、任意深度子节点均可拖拽至另一棵树形;
- 拖拽父节点时,其下所有嵌套子节点完整跟随迁移 / 复制;
- 自动保留节点自定义业务数据
Qt::UserRole(ID、备注等自定义字段)。
3. 同树内部层级调整
- 拖拽到树形空白区域:节点提升为顶层根节点;
- 拖拽到已有节点上方:变为目标节点的子节点,父节点自动展开;
- 拖拽指示器显示横线,直观预览放置位置。
4. 拖拽安全防护
- 过滤无文字空白节点,禁止无效拖拽操作;
- 自动区分顶层 / 子节点,删除节点不会下标越界、程序闪退;
- 拖拽完成自动清空缓存,不会出现二次拖动误删其他节点;
- 仅接收本程序生成的树形拖拽数据,屏蔽外部无效拖拽内容。
三、右键上下文菜单功能(新增 / 删除节点)
在树形空白处、任意节点右键弹出中文菜单,共三项操作:
- 新增同级节点 右键已有节点:在当前节点下方插入同层级条目;右键空白区域:直接创建顶层根节点;弹窗输入自定义节点名称,空名称自动取消创建。
- 新增子节点 为选中节点添加子条目,父节点自动展开,支持无限多层嵌套树形。
- 删除当前节点 弹出确认弹窗,确认后删除当前节点以及其下属全部子节点;顶层节点、深层子节点均可安全删除。
四、树形数据序列化特性
- 结构化递归打包节点:节点显示文字、自定义 UserRole 数据、全部子节点统一封装;
- 二进制数据流传输拖拽数据,修复多层子节点截断丢失、树形错乱 bug;
- 精准反序列化还原完整嵌套树形,无递归死循环风险。
五、编译与环境适配优化
- 适配 Qt5.9.9 MSVC2015 64 位,彻底解决虚函数 override 签名不匹配编译报错;
- 修复 Qt5
QMouseEvent无buttonDownPos接口兼容问题; - pro 配置 MSVC 强制 UTF8 编译,解决中文字符串
C2001常量换行报错; - 括号、函数作用域严格成对匹配,无语法、标识符缺失报错;
- 纯 Qt 基础控件实现,无第三方依赖,可直接集成至其他 Qt 项目。
六、当前局限(不影响常规业务使用)
- 仅支持树形单列文本展示,多列树形需拓展序列化代码;
- 仅单选拖拽,不支持多选批量拖动;
- 不自动保存树形到文件,重启后数据重置(可拓展 JSON / 文件持久化);
- 不序列化节点图标、复选框状态,可自行拓展数据包字段实现。
七、可拓展方向
- 右键增加节点重命名功能;
- 支持多选批量拖拽;
- 树形结构导出 JSON、本地文件持久化加载;
- 增加限制逻辑:禁止将父节点拖入自身子节点,避免循环嵌套;
- 增加节点图标、复选框状态同步序列化。