
简介本资源是一个基于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%的初学者卡在“松手就消失”上你试过用QGraphicsViewQPainter实现鼠标绘制矩形吗不是静态显示一个框而是——按住左键拖动时实时预览轮廓松开后永久留下一个可选中、可移动、可删除的矩形对象。很多同学写完发现拖动时能画一松手矩形就没了或者画出来的是 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作为唯一状态中心维护QListQGraphicsRectItem* 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确保 leftright, topbottom QRectF rect QRectF(m_startPos, endPos).normalized(); // 3. 通知 scene 添加正式图元 static_castDrawingScene*(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); // 获取所有可操作矩形供外部查询、导出、批量操作 QListQGraphicsRectItem* allDrawableRects() const; signals: void rectAdded(QGraphicsRectItem* item); // 供 UI 更新状态栏如已绘制3个矩形 private: QListQGraphicsRectItem* 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); } QListQGraphicsRectItem* 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_castDrawingScene*(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()后的deleteQGraphicsScene析构时会自动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编译时加-fsanitizeaddress参数运行后执行绘制→删除→再绘制循环 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基本就点头了。希望帮到你。本文还有配套的精品资源点击获取