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();
}
运行截图:
