Qt中实现两个TreeWidget之间拖拽节点

目录

[双 QTreeWidget 拖拽节点完整可运行代码](#双 QTreeWidget 拖拽节点完整可运行代码)

DragTree.h

DragTree.cpp

main.cpp

[双 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. 两个树形完全独立,支持节点互相拖拽交互;
  3. 内置演示测试数据:左侧多层分组父子节点,右侧预设空白分组,开箱即可测试;
  4. 全中文界面:窗口标题、按钮、表头、弹窗提示、右键菜单全部中文显示。

二、拖拽核心功能(无崩溃、数据不丢失)

1. 两种拖拽模式一键切换

窗口顶部提供「移动模式」「复制模式」按钮,全局切换两棵树拖拽行为:

  1. 移动模式(默认)
    • 跨树拖拽:源树原始节点删除,完整迁移到目标树;
    • 同树内部拖拽:仅调整层级顺序,源节点保留不删除。
  2. 复制模式
    • 拖拽生成节点副本,原树形节点完整保留;
    • 拖动父节点时整套多层子树同步复制,子节点不会丢失。

2. 跨树互相拖拽(左 ↔ 右)

  1. 任意顶层分组、任意深度子节点均可拖拽至另一棵树形;
  2. 拖拽父节点时,其下所有嵌套子节点完整跟随迁移 / 复制;
  3. 自动保留节点自定义业务数据 Qt::UserRole(ID、备注等自定义字段)。

3. 同树内部层级调整

  1. 拖拽到树形空白区域:节点提升为顶层根节点;
  2. 拖拽到已有节点上方:变为目标节点的子节点,父节点自动展开;
  3. 拖拽指示器显示横线,直观预览放置位置。

4. 拖拽安全防护

  1. 过滤无文字空白节点,禁止无效拖拽操作;
  2. 自动区分顶层 / 子节点,删除节点不会下标越界、程序闪退;
  3. 拖拽完成自动清空缓存,不会出现二次拖动误删其他节点;
  4. 仅接收本程序生成的树形拖拽数据,屏蔽外部无效拖拽内容。

三、右键上下文菜单功能(新增 / 删除节点)

在树形空白处、任意节点右键弹出中文菜单,共三项操作:

  1. 新增同级节点 右键已有节点:在当前节点下方插入同层级条目;右键空白区域:直接创建顶层根节点;弹窗输入自定义节点名称,空名称自动取消创建。
  2. 新增子节点 为选中节点添加子条目,父节点自动展开,支持无限多层嵌套树形。
  3. 删除当前节点 弹出确认弹窗,确认后删除当前节点以及其下属全部子节点;顶层节点、深层子节点均可安全删除。

四、树形数据序列化特性

  1. 结构化递归打包节点:节点显示文字、自定义 UserRole 数据、全部子节点统一封装;
  2. 二进制数据流传输拖拽数据,修复多层子节点截断丢失、树形错乱 bug;
  3. 精准反序列化还原完整嵌套树形,无递归死循环风险。

五、编译与环境适配优化

  1. 适配 Qt5.9.9 MSVC2015 64 位,彻底解决虚函数 override 签名不匹配编译报错;
  2. 修复 Qt5 QMouseEventbuttonDownPos接口兼容问题;
  3. pro 配置 MSVC 强制 UTF8 编译,解决中文字符串C2001常量换行报错;
  4. 括号、函数作用域严格成对匹配,无语法、标识符缺失报错;
  5. 纯 Qt 基础控件实现,无第三方依赖,可直接集成至其他 Qt 项目。

六、当前局限(不影响常规业务使用)

  1. 仅支持树形单列文本展示,多列树形需拓展序列化代码;
  2. 仅单选拖拽,不支持多选批量拖动;
  3. 不自动保存树形到文件,重启后数据重置(可拓展 JSON / 文件持久化);
  4. 不序列化节点图标、复选框状态,可自行拓展数据包字段实现。

七、可拓展方向

  1. 右键增加节点重命名功能;
  2. 支持多选批量拖拽;
  3. 树形结构导出 JSON、本地文件持久化加载;
  4. 增加限制逻辑:禁止将父节点拖入自身子节点,避免循环嵌套;
  5. 增加节点图标、复选框状态同步序列化。