Qt工具栏+图页,图元支持粘贴复制,撤销,剪切,移动,删除
2026/5/23 23:55:48 网站建设 项目流程

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(); }

运行截图:

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询