Qt工具栏+图页,图元支持粘贴复制,撤销,剪切,移动,删除

graphicsview.h

复制代码
/**
 * @file graphicsview.h
 * @brief 自定义图形视图类声明,用于承载图形组态编辑。
 */

#ifndef GRAPHICSVIEW_H
#define GRAPHICSVIEW_H

#include <QGraphicsView>
#include <QList>
#include <QVariant>

class QGraphicsItem;
class QUndoStack;

/**
 * @brief 图形编辑视图。
 *
 * 支持从左侧工具栏拖拽图元到视图中创建对象,
 * 并提供选择、移动、删除、撤销、剪切、复制和粘贴等编辑操作。
 */
class GraphicsView : public QGraphicsView
{
    Q_OBJECT

public:
    /**
     * @brief 构造函数。
     * @param parent 父窗口对象。
     */
    explicit GraphicsView(QWidget *parent = nullptr);

protected:
    /**
     * @brief 处理拖入事件,决定是否接受拖拽数据。
     */
    void dragEnterEvent(QDragEnterEvent *event) override;

    /**
     * @brief 处理拖动过程中的事件。
     */
    void dragMoveEvent(QDragMoveEvent *event) override;

    /**
     * @brief 处理放下拖拽数据的事件,在场景中创建对应图元。
     */
    void dropEvent(QDropEvent *event) override;

    /**
     * @brief 处理键盘事件,实现删除、撤销、复制、剪切、粘贴等快捷键。
     */
    void keyPressEvent(QKeyEvent *event) override;

private:
    /// 撤销/重做命令栈
    QUndoStack *m_undoStack = nullptr;

    /// 内部剪贴板,保存被复制/剪切的图元信息
    QList<QVariantMap> m_clipboardItems;

    /**
     * @brief 删除当前选中的图元。
     * @param storeToClipboard 是否将删除的图元写入内部剪贴板。
     */
    void deleteSelectedItems(bool storeToClipboard = false);

    /**
     * @brief 剪切当前选中的图元(删除并写入内部剪贴板)。
     */
    void cutSelectedItems();

    /**
     * @brief 复制当前选中的图元到内部剪贴板。
     */
    void copySelectedItems();

    /**
     * @brief 从内部剪贴板粘贴图元到场景中。
     */
    void pasteItems();
};

#endif // GRAPHICSVIEW_H

graphicsview.cpp

复制代码
/**
 * @file graphicsview.cpp
 * @brief 自定义图形视图实现。
 */

#include "graphicsview.h"

#include <QDragEnterEvent>
#include <QDropEvent>
#include <QMimeData>
#include <QGraphicsScene>
#include <QGraphicsTextItem>
#include <QGraphicsItem>
#include <QGraphicsRectItem>
#include <QGraphicsEllipseItem>
#include <QDataStream>
#include <QMap>
#include <QKeyEvent>
#include <QPen>
#include <QBrush>
#include <QUndoStack>
#include <QUndoCommand>

/**
 * @brief 添加图元的命令,实现单个图元的可撤销/重做。
 *
 * 使用 QVariantMap 描述图元的类型与属性(矩形 / 圆形 / 文本),
 * redo() 负责在场景中创建或重新添加该图元,undo() 负责从场景中移除。
 */
class AddItemCommand : public QUndoCommand
{
public:
    /**
     * @brief 构造函数。
     * @param scene 目标场景。
     * @param data  图元的序列化数据(类型、位置、画笔、画刷、字体等)。
     * @param parent 父命令,用于命令分组(可选)。
     */
    AddItemCommand(QGraphicsScene *scene, const QVariantMap &data, QUndoCommand *parent = nullptr)
        : QUndoCommand(parent)
        , m_scene(scene)
        , m_data(data)
        , m_item(nullptr)
    {
    }

    /// 执行命令:在场景中添加图元(首次执行时创建,后续 redo 仅重新挂回场景)。
    void redo() override;

    /// 撤销命令:将图元从场景中移除,但保留指针以便后续 redo 复用。
    void undo() override;

private:
    QGraphicsScene *m_scene;  ///< 目标场景
    QVariantMap m_data;       ///< 图元数据
    QGraphicsItem *m_item;    ///< 实际场景图元指针
};

/**
 * @brief 批量删除图元的命令,实现成组删除的撤销/重做。
 *
 * 该命令保存一组图元指针,redo() 时将它们全部从场景移除,
 * undo() 时再逐个添加回场景,从而实现一次性撤销/恢复一批删除操作。
 */
class DeleteItemsCommand : public QUndoCommand
{
public:
    /**
     * @brief 构造函数。
     * @param scene 目标场景。
     * @param items 本次要删除的图元列表。
     * @param parent 父命令。
     */
    DeleteItemsCommand(QGraphicsScene *scene, const QList<QGraphicsItem*> &items,
                       QUndoCommand *parent = nullptr)
        : QUndoCommand(parent)
        , m_scene(scene)
        , m_items(items)
    {
    }

    /// 执行命令:从场景中移除所有记录的图元。
    void redo() override;

    /// 撤销命令:将之前移除的图元重新添加回场景。
    void undo() override;

private:
    QGraphicsScene *m_scene;        ///< 目标场景
    QList<QGraphicsItem*> m_items;  ///< 被删除的图元集合
};

void AddItemCommand::redo()
{
    if (!m_scene)
        return;

    // 首次执行:根据数据创建对应类型的图元
    if (!m_item) {
        const QString type = m_data.value(QStringLiteral("type")).toString();

        if (type == QStringLiteral("rect")) {
            QRectF r = m_data.value(QStringLiteral("rect")).toRectF();
            QPen pen = qvariant_cast<QPen>(m_data.value(QStringLiteral("pen")));
            QBrush brush = qvariant_cast<QBrush>(m_data.value(QStringLiteral("brush")));

            auto *item = m_scene->addRect(r, pen, brush);
            item->setFlags(QGraphicsItem::ItemIsSelectable |
                           QGraphicsItem::ItemIsMovable);
            m_item = item;
        } else if (type == QStringLiteral("ellipse")) {
            QRectF r = m_data.value(QStringLiteral("rect")).toRectF();
            QPen pen = qvariant_cast<QPen>(m_data.value(QStringLiteral("pen")));
            QBrush brush = qvariant_cast<QBrush>(m_data.value(QStringLiteral("brush")));

            auto *item = m_scene->addEllipse(r, pen, brush);
            item->setFlags(QGraphicsItem::ItemIsSelectable |
                           QGraphicsItem::ItemIsMovable);
            m_item = item;
        } else if (type == QStringLiteral("text")) {
            QPointF pos = m_data.value(QStringLiteral("pos")).toPointF();
            const QString text = m_data.value(QStringLiteral("text")).toString();
            QFont font = qvariant_cast<QFont>(m_data.value(QStringLiteral("font")));

            auto *item = m_scene->addText(text, font);
            item->setPos(pos);
            item->setFlags(QGraphicsItem::ItemIsSelectable |
                           QGraphicsItem::ItemIsMovable);
            m_item = item;
        }
    } else {
        // 之前已经创建过,redo 时只需要重新加入场景
        if (!m_item->scene()) {
            m_scene->addItem(m_item);
        }
    }
}

void AddItemCommand::undo()
{
    if (m_scene && m_item && m_item->scene() == m_scene) {
        m_scene->removeItem(m_item);
    }
}

void DeleteItemsCommand::redo()
{
    if (!m_scene)
        return;

    for (QGraphicsItem *item : qAsConst(m_items)) {
        if (item && item->scene() == m_scene) {
            m_scene->removeItem(item);
        }
    }
}

void DeleteItemsCommand::undo()
{
    if (!m_scene)
        return;

    for (QGraphicsItem *item : qAsConst(m_items)) {
        if (item && !item->scene()) {
            m_scene->addItem(item);
        }
    }
}

GraphicsView::GraphicsView(QWidget *parent)
    : QGraphicsView(parent)
{
    setAcceptDrops(true);                 // 允许接收拖拽
    setScene(new QGraphicsScene(this));   // 创建默认场景
    setFocusPolicy(Qt::StrongFocus);      // 接收键盘焦点,用于快捷键编辑
    m_undoStack = new QUndoStack(this);   // 撤销/重做命令栈
}

void GraphicsView::dragEnterEvent(QDragEnterEvent *event)
{
    // 接受来自工具栏(QListWidget)或文本的拖拽数据
    if (event->mimeData()->hasText()
            || event->mimeData()->hasFormat("application/x-qabstractitemmodeldatalist")) {
        event->acceptProposedAction();
    }
}

void GraphicsView::dragMoveEvent(QDragMoveEvent *event)
{
    // 拖拽移动时保持接受状态,保证拖拽光标样式
    if (event->mimeData()->hasText()
            || event->mimeData()->hasFormat("application/x-qabstractitemmodeldatalist")) {
        event->acceptProposedAction();
    }
}

void GraphicsView::dropEvent(QDropEvent *event)
{
    if (!scene() || !m_undoStack) {
        event->ignore();
        return;
    }

    QString type;

    // 1) 直接用 text(如果有的话)
    if (event->mimeData()->hasText()) {
        type = event->mimeData()->text();
    }
    // 2) 从 QListWidget 的拖拽数据中解析出显示文本
    else if (event->mimeData()->hasFormat("application/x-qabstractitemmodeldatalist")) {
        const QByteArray encoded =
                event->mimeData()->data("application/x-qabstractitemmodeldatalist");
        QDataStream stream(encoded);

        while (!stream.atEnd()) {
            int row, col;
            QMap<int, QVariant> roleDataMap;
            stream >> row >> col >> roleDataMap;
            type = roleDataMap.value(Qt::DisplayRole).toString();
            if (!type.isEmpty())
                break;
        }
    }

    if (type.isEmpty()) {
        event->ignore();
        return;
    }

    // 将鼠标坐标转换为场景坐标,用于放置新建图元
    const QPointF pos = mapToScene(event->pos());

    // 根据拖拽类型构造一份图元数据交给命令对象创建
    QVariantMap data;

    if (type == QObject::tr("矩形")) {
        data["type"] = QStringLiteral("rect");
        data["rect"] = QRectF(pos.x() - 40, pos.y() - 25, 80, 50);
        data["pen"] = QPen(Qt::black);
        data["brush"] = QBrush(Qt::lightGray);
    } else if (type == QObject::tr("圆形")) {
        data["type"] = QStringLiteral("ellipse");
        data["rect"] = QRectF(pos.x() - 25, pos.y() - 25, 50, 50);
        data["pen"] = QPen(Qt::blue);
        data["brush"] = QBrush(Qt::cyan);
    } else if (type == QObject::tr("文本")) {
        data["type"] = QStringLiteral("text");
        data["pos"] = pos;
        data["text"] = QObject::tr("文本");
        data["font"] = QFont(QStringLiteral("Microsoft YaHei"), 10);
    }

    if (!data.isEmpty()) {
        m_undoStack->push(new AddItemCommand(scene(), data));
        event->acceptProposedAction();
    } else {
        event->ignore();
    }
}

void GraphicsView::keyPressEvent(QKeyEvent *event)
{
    if (!scene()) {
        QGraphicsView::keyPressEvent(event);
        return;
    }

    const bool ctrl = event->modifiers() & Qt::ControlModifier;

    // Delete:删除选中图元(不写入剪贴板,可撤销)
    if (event->key() == Qt::Key_Delete) {
        deleteSelectedItems(false);
        event->accept();
        return;
    // Ctrl + X:剪切选中图元(删除并写入内部剪贴板)
    } else if (ctrl && event->key() == Qt::Key_X) {
        cutSelectedItems();
        event->accept();
        return;
    // Ctrl + C:复制选中图元
    } else if (ctrl && event->key() == Qt::Key_C) {
        copySelectedItems();
        event->accept();
        return;
    // Ctrl + V:粘贴图元
    } else if (ctrl && event->key() == Qt::Key_V) {
        pasteItems();
        event->accept();
        return;
    // Ctrl + Z:撤销最近一次操作(由 QUndoStack 管理)
    } else if (ctrl && event->key() == Qt::Key_Z) {
        if (m_undoStack)
            m_undoStack->undo();
        event->accept();
        return;
    // Ctrl + Y:重做最近一次被撤销的操作
    } else if (ctrl && event->key() == Qt::Key_Y) {
        if (m_undoStack)
            m_undoStack->redo();
        event->accept();
        return;
    }

    QGraphicsView::keyPressEvent(event);
}

void GraphicsView::deleteSelectedItems(bool storeToClipboard)
{
    if (!scene() || !m_undoStack)
        return;

    if (storeToClipboard) {
        m_clipboardItems.clear();
    }

    const QList<QGraphicsItem*> selected = scene()->selectedItems();
    if (selected.isEmpty())
        return;

    // 根据需要,将删除的图元属性保存到内部剪贴板
    for (QGraphicsItem *item : selected) {
        if (storeToClipboard) {
            QVariantMap data;

            if (auto *rectItem = qgraphicsitem_cast<QGraphicsRectItem*>(item)) {
                data["type"] = QStringLiteral("rect");
                // 位置 = 图元自身 rect 加上场景位置
                QRectF r = rectItem->rect();
                r.translate(rectItem->pos());
                data["rect"] = r;
                data["pen"] = rectItem->pen();
                data["brush"] = rectItem->brush();
            } else if (auto *ellipseItem = qgraphicsitem_cast<QGraphicsEllipseItem*>(item)) {
                data["type"] = QStringLiteral("ellipse");
                QRectF r = ellipseItem->rect();
                r.translate(ellipseItem->pos());
                data["rect"] = r;
                data["pen"] = ellipseItem->pen();
                data["brush"] = ellipseItem->brush();
            } else if (auto *textItem = qgraphicsitem_cast<QGraphicsTextItem*>(item)) {
                data["type"] = QStringLiteral("text");
                data["pos"] = textItem->pos();
                data["text"] = textItem->toPlainText();
                data["font"] = textItem->font();
            }

            if (!data.isEmpty()) {
                m_clipboardItems.append(data);
            }
        }
    }

    // 删除操作交给 QUndoCommand 管理
    m_undoStack->push(new DeleteItemsCommand(scene(), selected));
}

void GraphicsView::cutSelectedItems()
{
    // 删除并把数据记录到剪贴板
    deleteSelectedItems(true);
}

void GraphicsView::copySelectedItems()
{
    if (!scene())
        return;

    m_clipboardItems.clear();

    const QList<QGraphicsItem*> selected = scene()->selectedItems();
    if (selected.isEmpty())
        return;

    // 将当前选中的图元属性拷贝到内部剪贴板
    for (QGraphicsItem *item : selected) {
        QVariantMap data;

        if (auto *rectItem = qgraphicsitem_cast<QGraphicsRectItem*>(item)) {
            data["type"] = QStringLiteral("rect");
            QRectF r = rectItem->rect();
            r.translate(rectItem->pos());
            data["rect"] = r;
            data["pen"] = rectItem->pen();
            data["brush"] = rectItem->brush();
        } else if (auto *ellipseItem = qgraphicsitem_cast<QGraphicsEllipseItem*>(item)) {
            data["type"] = QStringLiteral("ellipse");
            QRectF r = ellipseItem->rect();
            r.translate(ellipseItem->pos());
            data["rect"] = r;
            data["pen"] = ellipseItem->pen();
            data["brush"] = ellipseItem->brush();
        } else if (auto *textItem = qgraphicsitem_cast<QGraphicsTextItem*>(item)) {
            data["type"] = QStringLiteral("text");
            data["pos"] = textItem->pos();
            data["text"] = textItem->toPlainText();
            data["font"] = textItem->font();
        }

        if (!data.isEmpty()) {
            m_clipboardItems.append(data);
        }
    }
}

void GraphicsView::pasteItems()
{
    if (!scene() || !m_undoStack || m_clipboardItems.isEmpty())
        return;

    // 粘贴时整体偏移一点,避免完全重叠在原位置
    const QPointF offset(10.0, 10.0);

    for (const QVariantMap &data : qAsConst(m_clipboardItems)) {
        QVariantMap d = data;
        const QString type = d.value("type").toString();

        if (type == QStringLiteral("rect") || type == QStringLiteral("ellipse")) {
            QRectF r = d.value("rect").toRectF();
            r.translate(offset);
            d["rect"] = r;
        } else if (type == QStringLiteral("text")) {
            QPointF pos = d.value("pos").toPointF();
            pos += offset;
            d["pos"] = pos;
        }

        m_undoStack->push(new AddItemCommand(scene(), d));
    }
}

mainwindow.h

复制代码
/**
 * @file mainwindow.h                                        ///< 文件名:mainwindow.h
 * @brief 主窗口类声明,负责整体界面框架:工具栏 Dock + 图形编辑 Tab 页。 ///< 描述主窗口的作用
 */

#ifndef MAINWINDOW_H                                        ///< 头文件防重包含宏开始
#define MAINWINDOW_H                                        ///< 定义防重包含宏

#include <QMainWindow>                                      ///< 引入 QMainWindow 基类

class QTabWidget;                                           ///< 前向声明 QTabWidget,避免在头文件中包含过多头文件

QT_BEGIN_NAMESPACE                                          ///< Qt 命名空间宏开始
namespace Ui {                                              ///< Qt Designer 生成的 UI 命名空间
class MainWindow;                                           ///< 前向声明 UI::MainWindow 类
}                                                           ///< 结束 Ui 命名空间
QT_END_NAMESPACE                                            ///< Qt 命名空间宏结束

/**
 * @brief 应用程序主窗口。                                  ///< 整个程序的主界面类
 *
 * 左侧提供拖拽工具栏,中央为带有多个图页的 Tab 视图,    ///< 简要说明布局结构
 * 每个图页包含一个可拖拽、编辑图元的自定义视图。        ///< 每个图页里都有一个 GraphicsView
 */
class MainWindow : public QMainWindow
{
    Q_OBJECT

public:                                                     ///< 公共接口区域
    /**
     * @brief 构造函数。                                    ///< 创建 MainWindow 对象
     * @param parent 父窗口指针。                          ///< 指定父窗口,一般为 nullptr
     */
    MainWindow(QWidget *parent = nullptr);                  ///< 构造函数声明

    /**
     * @brief 析构函数。                                    ///< 销毁 MainWindow 对象
     */
    ~MainWindow();                                          ///< 析构函数声明

private:                                                    ///< 私有成员区域
    Ui::MainWindow *ui;     ///< Qt Designer 生成的 UI 对象指针
    QTabWidget *m_tabWidget;///< 中央区域的 Tab 控件,承载多个图形页面的指针

    /**
     * @brief 创建并初始化左侧工具栏 Dock。                ///< 设置左侧可停靠工具栏
     *
     * 工具栏中包含可拖拽的图元项(矩形、圆形、文本等)。 ///< 工具栏的主要内容说明
     */
    void setupToolDock();                                   ///< 工具栏初始化函数声明

    /**
     * @brief 创建并初始化图形编辑 Tab 页。                ///< 设置中央 Tab 页
     *
     * 当前实现中,默认创建一个图页并放置一个 GraphicsView。///< 说明默认只创建一个页面
     */
    void setupTabPages();                                   ///< 图形编辑 Tab 页初始化函数声明
};

#endif // MAINWINDOW_H                                     ///< 头文件防重包含宏结束

mainwindow.cpp

复制代码
/**
 * @file mainwindow.cpp                              ///< 文件名:mainwindow.cpp
 * @brief 主窗口类实现。                             ///< 提供 MainWindow 的具体实现
 */

#include "mainwindow.h"                              ///< 引入 MainWindow 类声明
#include "ui_mainwindow.h"                           ///< 引入由 Qt Designer 生成的 UI 头文件
#include "graphicsview.h"                            ///< 引入自定义图形视图类声明

#include <QDockWidget>                               ///< 引入 QDockWidget,用于创建可停靠窗口
#include <QListWidget>                               ///< 引入 QListWidget,用于显示工具项列表
#include <QTabWidget>                                ///< 引入 QTabWidget,用于中央 Tab 页

/**
 * @brief MainWindow 构造函数,初始化界面和布局。    ///< 负责搭建主界面结构
 * @param parent 父窗口指针。                        ///< 可以指定父窗口,一般为 nullptr
 */
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)                           ///< 调用基类 QMainWindow 的构造函数
    , ui(new Ui::MainWindow)                        ///< 创建 UI 对象实例
    , m_tabWidget(nullptr)                          ///< 初始化 Tab 控件指针为空
{
    ui->setupUi(this);                              ///< 通过 UI 对象构建界面基础部件

    setupToolDock();                                ///< 创建左侧工具栏 Dock
    setupTabPages();                                ///< 创建图形编辑 Tab 页
}

MainWindow::~MainWindow()
{
    delete ui;                                      ///< 释放 UI 对象,防止内存泄漏
}

void MainWindow::setupToolDock()
{
    // 创建可停靠的工具栏窗口
    auto *dock = new QDockWidget(tr("工具栏"), this); ///< 新建 Dock 窗口,标题为"工具栏"
    dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); ///< 允许左右两侧停靠

    // 使用 QListWidget 承载可拖拽的图元条目
    auto *list = new QListWidget(dock);             ///< 在 Dock 中创建列表控件
    list->setViewMode(QListView::IconMode);         ///< 使用图标视图模式显示条目
    list->setMovement(QListView::Static);           ///< 禁止在列表内部拖动改变顺序
    list->setDragEnabled(true);                     ///< 启用拖拽功能,允许拖到图页
    list->setSpacing(8);                            ///< 设置条目之间的间距
    list->setResizeMode(QListView::Adjust);         ///< 自动调整布局适应大小

    // 添加基本图元类型条目
    new QListWidgetItem(tr("矩形"), list);          ///< 添加"矩形"工具条目
    new QListWidgetItem(tr("圆形"), list);          ///< 添加"圆形"工具条目
    new QListWidgetItem(tr("文本"), list);          ///< 添加"文本"工具条目

    dock->setWidget(list);                          ///< 将列表设置为 Dock 的内部控件
    addDockWidget(Qt::LeftDockWidgetArea, dock);    ///< 把 Dock 加到主窗口左侧
}

void MainWindow::setupTabPages()
{
    // 中央区域使用 Tab 控件,今后可扩展为多图页
    m_tabWidget = new QTabWidget(this);             ///< 创建 Tab 控件作为中央小部件
    setCentralWidget(m_tabWidget);                  ///< 将 Tab 控件设为主窗口中心部件

    // 创建默认的第一个图形视图页面
    auto *view = new GraphicsView(this);            ///< 创建一个自定义图形视图
    m_tabWidget->addTab(view, tr("图页1"));         ///< 将视图作为一个 Tab 页添加进来
}

main.cpp

复制代码
#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

运行截图:

相关推荐
爱吃大芒果4 小时前
Flutter 本地存储方案:SharedPreferences、SQFlite 与 Hive
开发语言·javascript·hive·hadoop·flutter·华为·harmonyos
Kelvin_Ngan4 小时前
Qt包含QtCharts/QValueAxis时编译报错
开发语言·qt
葱卤山猪4 小时前
【Qt】心跳检测与粘包处理:打造稳定可靠的TCP Socket通信
开发语言·数据库·qt
a程序小傲4 小时前
淘宝Java面试被问:Atomic原子类的实现原理
java·开发语言·后端·面试
laocooon5238578864 小时前
C++中的安全指针(智能指针)
开发语言·c++
咸鱼加辣4 小时前
【python面试题】LRUCache
开发语言·python
LitchiCheng4 小时前
WSL2 中 pynput 无法捕获按键输入?
开发语言·python
中年程序员一枚4 小时前
Python 中处理视频添加 / 替换音频
开发语言·python·音视频
yuuki2332334 小时前
【C++】模板初阶
java·开发语言·c++