简介:本资源是一个基于Qt 5.15.2的图形交互式绘图Demo,面向C++ GUI开发初学者与Qt进阶实践者,聚焦QGraphicsView框架下的鼠标交互逻辑实现,解决矩形绘制、拖拽、缩放、边缘调整及删除保存等核心UI操作问题。压缩包共16个文件,含5个头文件(如GraphicsViewTool.h、DrawROI.h)定义图形工具类与ROI绘制逻辑,5个源文件(如DrawROI.cpp、GraphicsView.cpp)实现事件响应与场景管理,辅以vcxproj工程配置、ui界面文件、qrc资源文件及filters过滤器,结构完整,便于理解Qt图形视图架构分层设计。资源包仅18KB,轻量易读,已有2348人学习下载。读者可直接编译运行,掌握QPainter绘图基础、QGraphicsRectItem动态控制、鼠标事件(press/move/release)与键盘修饰键(Ctrl+滚轮)协同处理等关键技能,并复用代码逻辑扩展多边形绘制、选中高亮、序列化保存等功能。
1. 鼠标拖拽画矩形这件事,为什么90%的初学者卡在“松手就消失”上?
你试过用QGraphicsView+QPainter实现鼠标绘制矩形吗?不是静态显示一个框,而是——按住左键拖动时实时预览轮廓,松开后永久留下一个可选中、可移动、可删除的矩形对象。很多同学写完发现:拖动时能画,一松手,矩形就没了;或者画出来的是 QPainter 在 viewport 上临时涂的“影子”,根本没进场景(scene);更常见的是,多个矩形叠加后选中错乱、缩放失真、坐标偏移几十像素……这不是代码没写完,是根本没理清 QGraphics 框架里「视图-场景-图元」三层职责的边界。这套方案专为需要交付完整交互功能的课程设计、毕业设计或小型图形工具模块而生:它不依赖 Qt Quick,纯 QWidget 路线,用QGraphicsRectItem做真实图元,用QGraphicsScene管理生命周期,用QPainter只负责极简的临时反馈——所有逻辑可调试、可扩展、可答辩。适合正在啃 Qt 图形模块、被事件分发和坐标系绕晕的 C++ 学生,也适合要快速嵌入已有 UI 的工程师。
2. 总体架构设计:三层分离与四类事件的精准拦截
Qt 图形绘制最易混淆的,就是把paintEvent()、mousePressEvent()、QGraphicsItem::paint()全塞进同一个类里搅成一锅粥。我们先立住骨架:View 负责接收原始输入,Scene 负责状态调度,Item 负责自身渲染。整个流程不经过任何自定义绘图事件,全部走 QGraphics 框架原生管线。这意味着:你永远不需要重写QWidget::paintEvent,也不需要手动调用update()触发重绘——框架会自动根据图元变化刷新视图。
2.1 核心类职责划分:谁该管什么,绝不越界
DrawingView(继承QGraphicsView):只做三件事——捕获鼠标按下/移动/释放事件;将视口坐标转为场景坐标;在拖拽过程中临时创建/更新一个PreviewRectItem(仅用于视觉反馈)。它不持有任何业务数据,不管理矩形列表,不决定是否保存。DrawingScene(继承QGraphicsScene):作为唯一状态中心,维护QList<QGraphicsRectItem*> m_rectItems;提供addFinishedRect(QRectF)接口供 View 调用;重写mousePressEvent做全局点击拦截(比如点空白处取消当前预览);所有持久化矩形的创建、删除、ZOrder 排序都在这里完成。PreviewRectItem(继承QGraphicsRectItem):一个轻量级临时图元,仅在拖拽中存在。它重写paint()方法,用QPainter::setPen(Qt::DashLine)画虚线框,并禁用所有交互(setFlag(ItemIsMovable, false)),避免干扰主场景操作。DrawableRectItem(继承QGraphicsRectItem):最终落地的正式图元。启用ItemIsMovable | ItemIsSelectable | ItemIsFocusable,支持键盘删除、双击编辑尺寸、右键菜单等扩展能力——本方案已预留接口,后续加功能不用改架构。
提示:不要在
DrawingView中直接 newQGraphicsRectItem后 add 到 scene。必须通过DrawingScene::addFinishedRect()统一入口,否则 scene 无法感知新增图元,会导致items()查询为空、信号连接失败等玄学问题。
2.2 坐标系转换:为什么你算的坐标总差 20 像素?
Qt 中存在三套坐标系:Widget 坐标(以 widget 左上角为原点)、Viewport 坐标(以 view 视口左上角为原点)、Scene 坐标(以 scene 逻辑坐标系原点为原点)。鼠标事件给的是 Viewport 坐标,但QGraphicsScene::addItem()要求 Scene 坐标。错误做法是手动加减horizontalScrollBar()->value()—— 这在有缩放、平移时必然翻车。
正确解法是使用框架提供的转换函数:
// 在 DrawingView::mousePressEvent 中 void DrawingView::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { // 将 viewport 坐标 → scene 坐标(自动处理缩放/滚动) QPointF scenePos = mapToScene(event->pos()); m_startPos = scenePos; m_previewItem = new PreviewRectItem(); m_previewItem->setPen(QPen(Qt::blue, 2, Qt::DashLine)); scene()->addItem(m_previewItem); m_previewItem->setRect(QRectF(scenePos, scenePos)); // 初始宽高为0 event->accept(); } else { QGraphicsView::mousePressEvent(event); // 让父类处理右键等其他事件 } }关键点:mapToScene(event->pos())是唯一可靠方式。它内部已封装了QTransform矩阵运算,无论你是否调用scale()或translate(),结果都精准。我带过的某高校课程设计里,A同学硬写scenePos.x() + horizontalOffset(),结果在 125% 缩放屏下偏移整整 37 像素——从那以后我每次教坐标转换,都强制让学员先删掉所有手算代码,只留这一行。
2.3 拖拽状态机:四个状态,三处校验,零内存泄漏
整个绘制过程本质是一个有限状态机:
| 状态 | 触发条件 | View 行为 | Scene 行为 |
|---|---|---|---|
| Idle(空闲) | 未按下左键 | 忽略所有 move/release | 无 |
| DragStart(起始) | mousePressEvent左键 | 记录起点,创建PreviewRectItem | 不添加到 items 列表 |
| Dragging(拖拽中) | mouseMoveEvent | 更新PreviewRectItem的setRect() | PreviewRectItem始终在 scene 中,但不计入正式列表 |
| DragEnd(结束) | mouseReleaseEvent | 删除PreviewRectItem,调用scene()->addFinishedRect() | 创建DrawableRectItem并加入m_rectItems |
状态切换必须严格闭环。特别注意DragEnd时的资源清理:
// 在 DrawingView::mouseReleaseEvent 中 void DrawingView::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton && m_previewItem) { // 1. 获取终点 scene 坐标 QPointF endPos = mapToScene(event->pos()); // 2. 构造标准 QRectF(确保 left<=right, top<=bottom) QRectF rect = QRectF(m_startPos, endPos).normalized(); // 3. 通知 scene 添加正式图元 static_cast<DrawingScene*>(scene())->addFinishedRect(rect); // 4. 彻底销毁预览项(必须!否则内存泄漏) scene()->removeItem(m_previewItem); delete m_previewItem; m_previewItem = nullptr; event->accept(); } else { QGraphicsView::mouseReleaseEvent(event); } }normalized()是血泪经验:当用户从右向左拖拽时,QRectF(m_startPos, endPos)会产生负宽高,导致QGraphicsRectItem渲染异常甚至崩溃。normalized()自动交换坐标,保证 rect 合法。另外,removeItem()和delete必须成对出现——Qt 文档明确要求:从 scene 移除后,若不再使用,必须手动 delete,否则对象残留内存。
3. 关键模块实现:从预览虚线到可操作矩形的完整链路
现在进入真正要抄作业的部分。下面给出DrawingScene和DrawableRectItem的核心实现,每段代码后都附带参数说明和设计意图,不是贴完就走。
3.1 DrawingScene:状态容器与业务中枢
// drawing_scene.h class DrawingScene : public QGraphicsScene { Q_OBJECT public: explicit DrawingScene(QObject *parent = nullptr); // 对外唯一添加正式矩形的入口 void addFinishedRect(const QRectF &rect); // 获取所有可操作矩形(供外部查询、导出、批量操作) QList<QGraphicsRectItem*> allDrawableRects() const; signals: void rectAdded(QGraphicsRectItem* item); // 供 UI 更新状态栏,如"已绘制3个矩形" private: QList<QGraphicsRectItem*> m_rectItems; }; // drawing_scene.cpp DrawingScene::DrawingScene(QObject *parent) : QGraphicsScene(parent) { // 设置默认画布背景为浅灰,便于看清白色矩形边框 setBackgroundBrush(Qt::lightGray); } void DrawingScene::addFinishedRect(const QRectF &rect) { DrawableRectItem *item = new DrawableRectItem(); item->setRect(rect); item->setPen(QPen(Qt::black, 1.5)); // 正式图元用实线 item->setBrush(QColor(200, 220, 255, 100)); // 半透明蓝底 addItem(item); m_rectItems.append(item); // 发射信号,上层可连接槽函数更新按钮状态或计数器 emit rectAdded(item); } QList<QGraphicsRectItem*> DrawingScene::allDrawableRects() const { return m_rectItems; }参数说明与设计点:
setBackgroundBrush(Qt::lightGray):不是为了美观,而是解决一个经典问题——当用户画出纯白矩形时,在默认白底上完全不可见。设为浅灰后,即使setBrush(Qt::white)也能看清边框。QColor(200, 220, 255, 100)中的100是 alpha 通道(0~255),取值 100 约等于 40% 透明度。这样既能看到矩形覆盖区域,又不会遮挡底层内容,符合工程中“可叠加、可识别”的交互原则。allDrawableRects()返回QList而非QVector,因为QList在 Qt 中对指针类型做了优化,插入/遍历性能更好,且与QGraphicsScene::items()返回类型一致,减少类型转换。
3.2 DrawableRectItem:可交互图元的最小完备实现
// drawable_rect_item.h class DrawableRectItem : public QGraphicsRectItem { public: explicit DrawableRectItem(QGraphicsItem *parent = nullptr); // 重写 boundingRect 以支持精确点击检测(非必须,但推荐) QRectF boundingRect() const override; // 重写 paint,控制外观细节 void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; protected: // 支持键盘删除 void keyPressEvent(QKeyEvent *event) override; // 支持双击进入编辑模式(预留扩展点) void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; }; // drawable_rect_item.cpp DrawableRectItem::DrawableRectItem(QGraphicsItem *parent) : QGraphicsRectItem(parent) { // 启用所有基础交互能力 setFlag(ItemIsMovable, true); setFlag(ItemIsSelectable, true); setFlag(ItemIsFocusable, true); // 设置选中时的虚线边框(Qt 默认样式) setPen(QPen(Qt::darkBlue, 2, Qt::DotLine)); } QRectF DrawableRectItem::boundingRect() const { // 扩展 2px 边界,让鼠标在边框外 2px 内也能选中(提升体验) QRectF base = QGraphicsRectItem::boundingRect(); return base.adjusted(-2, -2, 2, 2); } void DrawableRectItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { // 先调用父类绘制基础矩形(含选中高亮) QGraphicsRectItem::paint(painter, option, widget); // 如果当前被选中,叠加一个半透明遮罩层(增强视觉反馈) if (isSelected()) { painter->save(); painter->setPen(Qt::NoPen); painter->setBrush(QColor(255, 215, 0, 80)); // 金色半透明 painter->drawRect(rect().adjusted(2, 2, -2, -2)); painter->restore(); } } void DrawableRectItem::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace) { // 安全删除:先从 scene 移除,再 delete if (scene()) { scene()->removeItem(this); } delete this; // 注意:this 指针在此后失效 event->accept(); } else { QGraphicsRectItem::keyPressEvent(event); } } void DrawableRectItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { // 预留:此处可弹出尺寸编辑对话框 qDebug() << "Double clicked on rect:" << rect(); event->accept(); }设计说明:
boundingRect().adjusted(-2,-2,2,2)是典型的人机工程技巧。人眼定位精度有限,直接点击像素级边框极易失败。扩展 2px 后,用户点击边框外侧也能触发选中,体验提升显著。keyPressEvent中的delete this是 Qt 官方认可的安全写法(见 Qt Docs: Object Trees & Ownership ),前提是确保该对象不在栈上创建,且没有其他强引用。本方案中所有DrawableRectItem均由new创建,符合前提。paint()中的qDebug()是调试钩子,实际交付时应替换为信号发射(如emit sizeEdited(rect())),供主窗口更新属性面板。
3.3 主窗口集成:三行代码启动绘制模式
最后,如何把这套模块接入你的主 UI?假设你用 Qt Designer 拖了一个QGraphicsView,命名为graphicsView:
// main_window.cpp #include "drawing_view.h" #include "drawing_scene.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); // 1. 替换默认 view 为自定义 DrawingView DrawingView *view = new DrawingView(this); ui->graphicsView->close(); // 移除 Designer 创建的默认 view ui->verticalLayout->addWidget(view); // 假设布局名为 verticalLayout // 2. 创建自定义 scene 并设置给 view DrawingScene *scene = new DrawingScene(this); view->setScene(scene); // 3. 连接添加信号,更新 UI 状态(例如按钮文字、计数器) connect(scene, &DrawingScene::rectAdded, this, &MainWindow::onRectAdded); } void MainWindow::onRectAdded(QGraphicsRectItem *item) { int count = static_cast<DrawingScene*>(ui->graphicsView->scene()) ->allDrawableRects().size(); ui->statusBar->showMessage(QString("已绘制 %1 个矩形").arg(count)); }关键点:不要试图在 Designer 中“提升为”自定义类——QGraphicsView提升后无法在.ui文件中修改其scene属性,会导致编译期绑定失败。必须在代码中new并setScene(),这是 Qt 官方推荐的动态集成方式。
4. 避坑指南:五个必踩的“松手就消失”型问题排查清单
刚写完代码却看不到矩形?或者画出来位置错乱?别急着重写,先对照这份按发生频率排序的避坑清单逐条排查。这些问题我在三个不同高校的毕设答辩中反复见到,90% 的“画不出来”都源于其中某一条。
4.1 现象:鼠标拖拽时有虚线框,松手后矩形彻底消失
原因:mouseReleaseEvent中调用了scene()->addItem(item),但item是栈对象或局部变量,作用域结束即销毁;或addItem()后未调用item->show()(虽然QGraphicsItem默认可见,但某些 Qt 版本在特定编译选项下需显式调用)。
解决:确认DrawableRectItem* item = new DrawableRectItem()在堆上创建;检查addItem()后是否误加了item->hide();用qDebug() << scene()->items().size()在添加前后打印数量验证。
4.2 现象:矩形画出来了,但位置严重偏移(比如总在左上角)
原因:在mousePressEvent中用了event->pos()直接构造QRectF,而event->pos()是 Viewport 坐标,不是 Scene 坐标。
解决:必须统一使用mapToScene(event->pos())转换;在addFinishedRect()中打印qDebug() << "start:" << m_startPos << "end:" << endPos验证坐标值是否合理(正常应在 0~1000 范围内,而非 0~100)。
4.3 现象:画多个矩形后,只能选中最上面一个,下面的点不中
原因:QGraphicsRectItem默认zValue()为 0,新添加的图元会覆盖旧图元,但QGraphicsScene::items()返回顺序不保证 ZOrder;更常见的是setFlag(ItemIsSelectable, true)没在构造函数中调用,导致图元不可选。
解决:在DrawableRectItem构造函数中强制setZValue(0);检查setFlag(ItemIsSelectable, true)是否执行;用scene()->sendEvent(item, new QMouseEvent(...))模拟点击测试单个图元响应。
4.4 现象:缩放(view->scale(2,2))后,拖拽预览框严重变形或抖动
原因:PreviewRectItem::setRect()传入的是未归一化的QRectF(m_startPos, endPos),缩放时QRectF的宽高计算受变换矩阵影响,产生浮点误差累积。
解决:始终使用QRectF::normalized()构造;在PreviewRectItem::paint()中用painter->setRenderHint(QPainter::Antialiasing)抗锯齿;避免在mouseMoveEvent中高频调用setRect(),改为每 16ms(约 60fps)更新一次。
4.5 现象:程序退出时崩溃,报QGraphicsScene: deleting items while scene is being destroyed
原因:DrawingScene析构时,m_rectItems中的DrawableRectItem*仍被 scene 持有,而你在析构函数中又手动delete了它们,造成双重释放。
解决:DrawingScene析构函数中只清空m_rectItems列表,不 delete 任何指针;因为QGraphicsScene析构时会自动 delete 所有addItem()过的图元。正确写法:
DrawingScene::~DrawingScene() { m_rectItems.clear(); // 仅清空列表,指针由 scene 自动管理 }注意:Qt 的内存管理规则是——谁
addItem(),谁负责removeItem()后的delete;QGraphicsScene析构时会自动removeItem()并delete所有子项。因此,你只需在运行时动态删除(如 Delete 键),无需在析构时干预。
5. 验收与答辩:用三张表+一个动作,让老师当场点头
毕设/课设答辩最怕什么?不是代码写得不够炫,而是老师问“这个功能怎么验证?”你支吾半天,最后打开程序随便点两下,效果还不稳定。验收不是演示,是用可复现的步骤、可量化的指标、可回溯的日志,证明你真的做完了、做对了、能用了。下面这套方法,我在某高校连续三年指导学生答辩,通过率 100%,核心就一句话:把“我实现了”变成“你按这步做,立刻看到结果”。
5.1 功能验收表:五项核心能力,逐条打钩
拿出一张纸(或新建文本文件),按顺序执行以下操作,每完成一项打一个 ✓。只要有一项失败,说明模块未闭环,必须返工。
| 序号 | 验证动作 | 预期结果 | 失败表现 | 定位方向 |
|---|---|---|---|---|
| 1 | 启动程序,鼠标悬停在 view 区域 | 状态栏显示“就绪”或无提示 | 显示“未初始化”或崩溃 | 检查DrawingScene是否成功setScene() |
| 2 | 按住左键,从 (100,100) 拖拽到 (300,200) 后松手 | 场景中出现一个蓝色虚线预览框 → 松手后变为黑色实线矩形 | 预览框不出现 / 松手后消失 / 位置偏移 | 查mousePressEvent是否调用mapToScene();查addFinishedRect()是否执行 |
| 3 | 用鼠标点击刚画的矩形 | 矩形边框变为深蓝色虚线,状态栏显示“已选中1个” | 无反应 / 选中其他矩形 | 检查DrawableRectItem是否启用ItemIsSelectable;查boundingRect()是否正确 |
| 4 | 选中矩形后按 Delete 键 | 矩形立即消失,状态栏计数减 1 | 无反应 / 崩溃 / 闪退 | 查keyPressEvent中delete this是否执行;用qDebug()打印scene()->items().size() |
| 5 | 连续绘制 5 个矩形,分别拖拽到不同位置 | 所有矩形独立可移动、可选中、不互相遮挡 | 某个矩形无法拖动 / 选中后其他矩形也移动 | 检查每个DrawableRectItem的setFlag(ItemIsMovable)是否单独调用;确认未误设setParentItem() |
这张表的价值在于:它把模糊的“功能正常”拆解成原子操作,每个动作都有明确输入、确定输出、失败特征和排查路径。答辩时,你可以直接说:“老师,请看第3项——我现在点击这个矩形……您看,边框变色了,状态栏更新了,这证明选中逻辑是通的。” 老师不需要懂代码,也能判断你做对了。
5.2 性能与健壮性验收:两个命令行指令定生死
很多同学忽略非功能需求,直到答辩被问“如果用户疯狂拖拽怎么办?”才慌神。其实只需两条终端命令,就能暴露所有隐患:
命令1:压力测试(模拟高频拖拽)
在mouseMoveEvent开头加一行:
qDebug() << "MouseMove at" << QTime::currentTime().msec();然后快速左右拖拽 10 秒。观察 Qt Creator 的 Application Output 面板:如果出现大量重复日志(如MouseMove at 123,MouseMove at 124,MouseMove at 125…),说明事件未节流,可能卡顿。解决方案:在DrawingView中加时间戳过滤:
// class member QElapsedTimer m_moveTimer; // in mouseMoveEvent if (m_moveTimer.elapsed() < 16) return; // 限制 60fps m_moveTimer.restart();命令2:内存泄漏扫描(Linux/macOS)
编译时加-fsanitize=address参数,运行后执行绘制→删除→再绘制循环 10 次,退出时查看终端输出。若出现AddressSanitizer: heap-use-after-free,说明delete this时机错误;若出现leak summary: 5 bytes in 1 blocks,说明某个new没配对delete。Windows 下可用 Visual Studio 的诊断工具,原理相同。
5.3 答辩话术:用“问题-解法-证据”三段式征服老师
别一上来就说“我用了 QGraphicsView 和 QPainter”。老师想听的是:你遇到了什么具体问题,怎么想到这个解法,以及怎么证明它有效。我教学生的固定话术模板:
“老师,这个模块最大的挑战是‘松手瞬间矩形丢失’(问题)。我最初以为是
addItem()没调用,后来发现是坐标系混乱——鼠标给的是 viewport 坐标,但 scene 要 scene 坐标(解法)。所以我强制所有坐标转换都走mapToScene(),并在addFinishedRect()里加了qDebug()打印转换前后的值(证据)。您看,这是日志截图:起点 (102.3, 98.7) → 终点 (298.1, 199.4),完全在合理范围内。”
说完立刻切到程序,现场演示从拖拽到松手的全过程,同步念出日志内容。这种“问题导向+证据链”式表达,比背诵技术名词有力十倍。
从那以后我每次带学生做 Qt 图形项目,都强制他们在main()开头加一行qInstallMessageHandler(customMessageHandler),把所有qDebug()输出重定向到文件。答辩前夜,他们把生成的debug.log打印出来,夹在报告书里——老师翻到那页,看到密密麻麻的坐标日志和✓ rectAdded,基本就点头了。希望帮到你。
本文还有配套的精品资源,点击获取