简介:这是一份基于Qt与C++语言的图形演示工程,主要使用QGraphicsView、QPainter及QGraphicsRectItem等图形视图类,实现用鼠标绘制矩形的完整交互流程。工程在Qt 5.15.2与VS2019环境下开发,代码结构清晰,共包含十六个文件,其中有五个头文件与五个源文件,还配有用户界面文件、资源文件以及Visual Studio工程配置文件,压缩包体积仅十八KB,方便快速下载。功能上支持鼠标绘制一个或多个矩形,按住矩形即可拖动,拖拽矩形边缘能够调整宽高,同时支持按住Ctrl滚动滚轮进行缩放,右键单击矩形即可删除或保存。该项目适合正在学习Qt图形视图框架或需要编写图形交互工具的开发者,通过阅读完整工程可快速理解关键交互逻辑,并在此基础上扩展绘制多边形、标注区域等功能。目前已有两千三百四十九人浏览学习,作为演示样例具有较好的参考价值。
1. 用鼠标在 QGraphicsView 上拖出矩形,这个需求比你想的麻烦
做截图标注工具或者图纸勾画工具时,最基础也最刚需的功能就是在 QGraphicsView 画布上用鼠标拖出一个矩形。很多从 QWidget 转过来的同学第一反应是重写 paintEvent,在 mousePressEvent 里记起点、mouseMoveEvent 里画线,看着好像没问题,但一旦画布支持了缩放和平移,坐标就全乱了,画出来的矩形也没法选中、没法拖动。这个工程给出的是一个标准答案:用 Qt 的 QGraphicsView 管理画布、QPainter 在拖拽过程绘制虚线预览、松手后用 QGraphicsRectItem 把矩形正式提交到场景里。适合正在做 Qt/C++ 桌面绘图、标注工具、地图框选这类功能的开发者,照着跑一遍就能理解三层坐标怎么换算、事件该重写谁的、为什么橡皮筋不能直接画在 scene 上。
2. 架构基础:GraphicsView 三件套与最小可运行工程
2.1 为什么要用 QGraphicsView 而不是普通 QWidget
先明确一个结论:画矩形这个需求用普通 QWidget 也能实现,但只会让后面的维护成本越来越高。QWidget 方案里,你画上去的内容本质是一张像素图,缩放、平移、旋转全都要自己写矩阵运算,撤销要保存整块画布,选中一个矩形还得自己算碰撞。这些东西在 GraphicsView 框架里是现成的。
QGraphicsScene 负责管理所有图元对象,QGraphicsView 负责把场景渲染到屏幕上,QGraphicsItem(这里具体是 QGraphicsRectItem)负责单个图形的坐标、绘制、碰撞和交互。三者的关系一句话概括:Item 活在 Scene 的世界坐标里,View 是一个移动的摄像机,把世界坐标投影到屏幕像素上。这也是为什么在 view 上拿到鼠标坐标后不能直接拿去创建 item——鼠标给的是屏幕像素坐标,item 要的是场景世界坐标,中间隔着一层 view 变换。
这个分层带来的实际好处是:场景里有几百个矩形时,View 只重绘可见区域;item 自带 ItemIsMovable 和 ItemIsSelectable 标志,一行代码就能获得拖动和选中能力。代价是事件链路变长了,鼠标事件要先经过 viewport 再派发给 view,处理不好就容易在坐标换算上翻车。后面核心实现部分就是把这条链路完整走一遍。
2.2 目录结构:最小的标注画布工程由哪几个文件组成
这个工程刻意保持精简,没有引入任何第三方库,也没有拆出独立的 item 类文件,方便你在现有项目里把核心文件直接抄走。
RectDrawDemo/ ├── RectDrawDemo.pro # qmake 工程文件 ├── main.cpp # 程序入口 ├── MainWindow.h # 主窗口头文件 ├── MainWindow.cpp # 主窗口实现,只负责放一个 DrawView ├── DrawView.h # 自定义画布视图头文件 └── DrawView.cpp # 核心实现:鼠标事件、橡皮筋、图元创建核心代码全部集中在 DrawView 这两个文件里,MainWindow 只是个壳。这么拆的原因很简单:画矩形是一类"交互工具"行为,把它封装在 QGraphicsView 的子类里,后续想加画椭圆、画箭头、画折线,都是在这个类里扩展,不需要动主窗口。如果你要集成到现有项目,把 DrawView.h 和 DrawView.cpp 拷过去,再在需要的地方 new 一个 DrawView 放到布局里就行。
这种结构也方便你测试:DrawView 不依赖 MainWindow 的任何内部状态,给它一个空的 QGraphicsScene 就能独立工作,符合单一职责的思路。后面二次开发章节的图形工厂扩展,也是基于这个结构展开的。
2.3 从 main.cpp 到 MainWindow:把画布跑起来
先看工程文件。这里用 qmake,Qt 5.15 和 Qt 6.x 都能直接编译,不需要额外配置。
QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = RectDrawDemo TEMPLATE = app SOURCES += \ main.cpp \ MainWindow.cpp \ DrawView.cpp HEADERS += \ MainWindow.h \ DrawView.h这个 .pro 没什么特别的,加了 widgets 模块,列出三个源文件和两个头文件。在 Qt Creator 里打开这个 .pro 文件,选择合适的构建套件(MinGW 或 MSVC 都行),直接点运行就能看到窗口。
main.cpp 是标准的 QApplication 入口:
#include <QApplication> #include "MainWindow.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow window; window.show(); return app.exec(); }MainWindow 我故意写得极简,只创建一个 DrawView 并设置场景范围,没有工具栏也没有菜单,避免无关代码干扰你理解画矩形的核心逻辑:
#include "MainWindow.h" #include "DrawView.h" #include <QGraphicsScene> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { auto *view = new DrawView(this); // 场景范围 800x600,超出这个范围的图元不会被显示 view->setSceneRect(0, 0, 800, 600); view->setAlignment(Qt::AlignCenter); setCentralWidget(view); setWindowTitle(QStringLiteral("QGraphicsView 鼠标绘制矩形 Demo")); }setSceneRect 这一步容易被忽略。如果不设置,QGraphicsView 会根据场景内所有 item 的边界自动计算可见范围,空场景时会导致视图区域大小不确定,矩形画出来有时候看不到。固定场景范围是标注工具最常见的用法,也方便后续实现缩放时把坐标系锚定在固定区域上。
2.4 事件谁先收到:View 与 viewport 的事件分发链路
在写鼠标事件之前,得先搞清楚 QGraphicsView 的鼠标事件是怎么到达你的代码里的。这一步理解不透,后面排查问题时很容易在"为什么我重写了 mousePressEvent 却不生效"这种问题上卡半天。
QGraphicsView 继承自 QAbstractScrollArea,它的鼠标事件不是直接触发在 QGraphicsView 对象上,而是先发生在内部的 viewport() 控件上。事件流大概是:QApplication 把鼠标事件派发给 viewport,viewport 把事件交给 QGraphicsView::viewportEvent,这个函数内部会调用 QGraphicsView 的 mousePressEvent、mouseMoveEvent 等虚拟函数。当你重写 DrawView 的这些函数时,你就站在了场景 event 处理之前的位置,可以在 item 收到事件前先做自己的逻辑。
如果调用基类 QGraphicsView::mousePressEvent,基类会执行两件事:一是把事件派发给场景中命中的 item(比如触发 item 的 mousePressEvent),二是根据当前拖拽模式处理框选。这正是后面代码里"点击到已有图元时交给基类处理"的底层原因——item 的移动和选中都依赖基类的默认分发机制。
弄明白这条链路后,后面的核心实现就顺理成章了:我们在 DrawView 里拦截鼠标事件,把拖拽过程当作"创建新图元"的交互工具来处理,而不是把事件直接透传给场景。
3. 核心实现:从鼠标按下到矩形入 Scene 的完整链路
3.1 自定义 DrawView:重写三个鼠标事件
先看 DrawView 的头文件,成员变量不多,但每个都对应一个明确的职责:
#ifndef DRAWVIEW_H #define DRAWVIEW_H #include <QGraphicsView> #include <QPoint> class QGraphicsScene; class DrawView : public QGraphicsView { Q_OBJECT public: explicit DrawView(QWidget *parent = nullptr); protected: void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; private: bool m_drawing; // 是否正在拖拽画矩形 QPoint m_startPos; // 鼠标按下时的 viewport 坐标 QPoint m_lastPos; // 上一次鼠标移动时的 viewport 坐标 }; #endif // DRAWVIEW_H成员变量的作用分开说。m_drawing 是一个状态标志,标记当前是否处于"拖拽绘制"这个动作中,mouseMoveEvent 和 mouseReleaseEvent 里第一件事就是判断这个标志,避免鼠标悬停时反复执行绘制逻辑。m_startPos 记录按下的原点,m_lastPos 记录上一次移动的位置,这两个都是 viewport 坐标,也就是像素坐标,用于橡皮筋绘制时的擦除和重画。这里刻意没有存场景坐标,因为橡皮筋阶段要画在 viewport 上,用像素坐标最直接。
构造函数里要做的事不多:创建场景、设置场景大小、确保关闭 QGraphicsView 自带的橡皮筋拖拽框选,因为那个默认行为和我们自己的绘制逻辑冲突:
DrawView::DrawView(QWidget *parent) : QGraphicsView(parent) , m_drawing(false) { // 创建场景,这里用 new 之后交由 view 管理 setScene(new QGraphicsScene(this)); // 关闭 view 自带的橡皮筋框选,避免和我们自己的拖拽绘制冲突 setDragMode(QGraphicsView::NoDrag); // 按需开启抗锯齿,让矩形边框更平滑 setRenderHint(QPainter::Antialiasing, true); }setDragMode(QGraphicsView::NoDrag) 是必须的一步。QGraphicsView 默认在鼠标拖拽时会启动橡皮筋框选,用来一次性选中多个 item。我们要用左键拖拽来画新矩形,这个默认行为必须关掉,否则你会发现拖一下鼠标,场景里出现一个半透明的框选区域,自己的绘制逻辑完全被覆盖了。
3.2 橡皮筋预览:用 QPainter 在 viewport 上画虚线矩形
橡皮筋是这个功能里最体现 QPainter 功底的部分。我们的目标是拖拽过程中实时显示一个虚线矩形作为预览,但场景里不能真的创建一个临时 item 再删除——那样会产生大量增删操作和重绘。经典做法是直接用 QPainter 画在 viewport 上,因为这些像素不属于场景,下一次重绘就会消失,正好符合"临时预览"的语义。
mousePressEvent 的职责是判断是否开始绘制,并且不打扰已有图元的选中和移动:
void DrawView::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { // 如果点击位置已经有图元,把这个事件交给基类处理, // 让 item 自己响应选中、移动等交互 QPointF scenePos = mapToScene(event->pos()); if (scene()->itemAt(scenePos, transform())) { QGraphicsView::mousePressEvent(event); return; } // 没有命中任何图元,开始绘制新矩形 m_drawing = true; m_startPos = event->pos(); m_lastPos = event->pos(); event->accept(); return; } // 非左键事件按默认方式处理 QGraphicsView::mousePressEvent(event); }这里有个交互细节值得注意:如果鼠标按下时场景里刚好有已有矩形,我们会把事件透传给基类,让那个矩形响应 ItemIsMovable 的拖动逻辑。否则用户每点一次就会画出一个零宽矩形,已有图元也永远选不中、拖不动,整个画布就只剩"添加新矩形"这一个功能了。判断用 itemAt 配合 transform(),是因为缩放变换后,item 的命中测试需要知道当前 view 的变换矩阵。
mouseMoveEvent 是橡皮筋的核心,我的实现方式是"先擦除旧的,再画新的":
void DrawView::mouseMoveEvent(QMouseEvent *event) { if (!m_drawing) { QGraphicsView::mouseMoveEvent(event); return; } // 擦除上一次绘制的虚线矩形区域,让 viewport 重新绘制底层内容 viewport()->repaint(QRect(m_startPos, m_lastPos).normalized()); // 用 QPainter 在 viewport 上绘制新的虚线矩形 QPainter painter(viewport()); painter.setRenderHint(QPainter::Antialiasing, true); QPen pen(Qt::DashLine); pen.setColor(QColor(0, 160, 255)); pen.setWidth(2); painter.setPen(pen); painter.setBrush(Qt::NoBrush); QRect currentRect = QRect(m_startPos, event->pos()).normalized(); painter.drawRect(currentRect); m_lastPos = event->pos(); }代码里最关键的是 viewport()->repaint 这一行。为什么不用 update?update 是异步的,它会向事件队列投递一个重绘请求后立即返回,代码继续往下走,用 QPainter 画的矩形立刻又被后续的重绘覆盖掉,橡皮筋就闪没了。repaint 是同步刷新,执行完这一行,那个区域的底层内容(也就是场景里的已有图元)就被立即重绘出来,相当于把上一次的橡皮筋从屏幕上擦掉了。然后再用 QPainter 画新的矩形,屏幕上的效果就是虚线框跟着鼠标走。
normalized() 也是容易忽略的细节。从右下往左上拖拽时,QRect(m_startPos, event->pos()) 的 width 和 height 会是负数。repaint 接收负宽度区域时会拒绝刷新,drawRect 画出来的矩形方向也不对。normalized() 把矩形的左上角调整为实际左上角,宽高修正为正值,for 这个场景是必需的。
QPainter 开启抗锯齿后,虚线的断点过渡会平滑一些,不至于在缩放后的高分辨率屏幕上看着毛躁。虚线用 Qt::DashLine,画笔颜色是浅蓝色,不设置画刷,这样矩形内部是透明的,不会挡住底下的内容。你也可以把 pen.setWidth(2) 改成 1,在高 DPI 屏幕上更纤细,看你自己的视觉偏好。
3.3 提交图元:把 QGraphicsRectItem 加入 Scene
mouseReleaseEvent 的思路和 press 对应:如果此前没有 m_drawing 状态,就把事件交给基类处理;如果在绘制过程中,就完成矩形从"临时预览"到"正式图元"的转换:
void DrawView::mouseReleaseEvent(QMouseEvent *event) { if (!m_drawing) { QGraphicsView::mouseReleaseEvent(event); return; } // 松手后不需要手动擦除橡皮筋,addItem 触发的场景重绘会自然清掉 QRect releaseRect = QRect(m_startPos, event->pos()).normalized(); // 防误触:矩形太小(比如只有一次点击)就不创建图元 if (releaseRect.width() >= 3 && releaseRect.height() >= 3) { // 将 viewport 坐标的两个对角点映射到场景坐标 QPointF topLeft = mapToScene(releaseRect.topLeft()); QPointF bottomRight = mapToScene(releaseRect.bottomRight()); QRectF sceneRect(topLeft, bottomRight); auto *rectItem = new QGraphicsRectItem(sceneRect); rectItem->setPen(QPen(QColor(0, 160, 255), 2)); rectItem->setBrush(Qt::NoBrush); rectItem->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable); scene()->addItem(rectItem); } m_drawing = false; event->accept(); }这里做了一个二次判断:宽或高小于 3 像素的矩形直接丢弃。意义在于,用户在已有区域上误点一下也会触发完整的 press-release 流程,如果每次都创建一个 0x0 的矩形图元,场景里会堆满不可见的垃圾 item,后续遍历场景做序列化时会非常痛苦。3 像素这个阈值也可以改成更大,比如 5。
QGraphicsRectItem 的 flags 里,ItemIsMovable 让矩形创建后可以直接用鼠标拖动位置,ItemIsSelectable 让它可以被点击选中并显示选中框。这两项组合是标注工具图元的基础配置。setPen 设置边框颜色为浅蓝、宽度为 2,这里的单位是场景单位,也就是说 view 放大两倍后,边框视觉上会变成 4 像素宽。如果你希望边框粗细恒定不变,需要在 item 的 paint 里根据 view 的缩放比例动态计算,这个放到后面避坑章节讲。
addItem 之后,场景会接管这个 item 的所有权和绘制职责。以后它的移动、选中、擦除都不需要 DrawView 关心了,QGraphicsScene 会维护好每一帧的重绘。这也是为什么 release 里不需要再 repaint 橡皮筋区域——addItem 内部会触发场景更新,把这个矩形绘制到屏幕上,手工画在 viewport 上的临时像素自然就被覆盖掉了。
3.4 坐标换算:viewport、scene、item 三层坐标怎么转
看懂了这个工程的鼠标事件代码,剩下要迈过去的坎就是坐标换算。GraphicsView 框架里存在三套坐标系,混用是初学者最容易踩的坑。
viewport 坐标是屏幕像素坐标,event->pos() 返回的就是这个坐标。scene 坐标是场景世界坐标,所有 item 的位置、尺寸都基于这个坐标系。item 坐标是图元自身的局部坐标,对矩形来说就是左上角为原点的局部坐标系。三者的换算关系可以总结成下面这张表:
| 接口 | 作用 | 典型用法 |
|---|---|---|
| view->mapToScene(pos) | viewport 坐标转场景坐标 | 鼠标点击位置转场景坐标 |
| view->mapFromScene(scenePos) | 场景坐标转 viewport 坐标 | 在 viewport 上绘制时用 |
| item->mapFromScene(scenePos) | 场景坐标转 item 局部坐标 | 判断点是否落在 item 内 |
| item->mapToScene(itemPos) | item 局部坐标转场景坐标 | 获取 item 在场景中的真实位置 |
在 3.2 节中,橡皮筋绘制全部使用 viewport 坐标,因为 QPainter 画在 viewport 上,坐标基准就是像素。而在 3.3 节创建 QGraphicsRectItem 时,必须把 viewport 坐标通过 mapToScene 转成场景坐标,否则 view 只要做了缩放或者平移,矩形位置立刻漂移。
我踩过的那个坎是:一开始图省事,在 mouseMoveEvent 里直接用 m_startPos 和 event->pos() 计算矩形,然后把这个 QRect 存下来用来创建 item。看起来没什么问题,直到我把 view 缩放到 200% 再画矩形,发现矩形总是画在鼠标右下方偏移一大截的位置。原因就是 QGraphicsRectItem 接受的 rect 是场景坐标,viewport 坐标在缩放后和场景坐标已经不是 1:1 的关系了。从那以后我养成了习惯:任何从鼠标事件拿到的坐标,第一件事就是转成场景坐标,再进入后续逻辑。
4. 避坑与排查:绘制矩形常见的五个翻车现场
4.1 矩形尺寸为负:向左上角拖拽时矩形消失或闪一下
现象:鼠标从右下往左上拖拽画矩形,预览框要么完全不显示,要么只在某个特定方向时显示,release 后创建的矩形位置也不对。
原因:QRect(m_startPos, event->pos()) 在终点位于起点左上方时,width 和 height 为负数。负数宽高的矩形在 repaint 时会被拒绝刷新,在 drawRect 时绘制方向异常,最终 mapToScene 得到的场景坐标也偏离预期。
解决:所有用于计算显示和刷新的 QRect,创建后立即调用 normalized(),它会自动把左上角和宽高修正为正数方向。这个调用应该在 press、move、release 三个环节统一使用,不要只在其中一个环节处理。
4.2 点到已有图元时误画零宽矩形
现象:场景里已经有一些矩形,此时点击已有的矩形想选中它,结果鼠标按下的瞬间又触发了一次绘制流程,每次点击都新增一个不可见的细线矩形。
原因:mousePressEvent 里没有做击穿判断,任何左键按下都被当成绘制起点。item 的点击事件被 DrawView 拦截,永远到不了 item 自己手上。
解决:在 press 时用 scene()->itemAt(scenePos, transform()) 判断点击位置是否命中已有图元,命中则调用 QGraphicsView::mousePressEvent(event) 把事件透传给基类,由基类的默认分发机制完成 item 选中和拖动。同时配合 release 里的 3 像素最小尺寸判断,双保险。
4.3 拖拽中滚动滚轮,橡皮筋消失或错位
现象:按住左键拖拽到一半,用另一只手滚动滚轮想调整视图,结果虚线预览框瞬间消失,或者出现在一个完全不对的位置上。
原因:橡皮筋是用 QPainter 直接画在 viewport 像素上的,它不是场景内容,不参与 view 的变换。滚轮缩放触发 viewport 大面积重绘,手工画上去的像素立刻被覆盖清除。即使没被清除,缩放过后的坐标系也和你按下时不一样了,继续画也画不对。
解决:简单做法是在 DrawView 里重写 wheelEvent,m_drawing 为 true 时直接忽略滚轮事件。更彻底的做法是把预览方案从"QPainter 画在 viewport"改成"维护一个临时的 QGraphicsRectItem 挂在场景里",拖拽过程中不断 setRect,item 会自动跟随视图缩放平移,release 时把它正式加入场景并去掉临时状态,这样就没有"画布重绘丢橡皮筋"的问题了。两种方案各有取舍,如果后续要做旋转视图,建议直接上临时 item 方案。
4.4 view 缩放到 200%,矩形位置整体偏移
现象:把 view 放大两倍后再画矩形,虚线框跟手,但松手后生成的矩形边框和鼠标轨迹明显错位,放大倍数越大错位越严重。
原因:创建 QGraphicsRectItem 时直接用了 viewport 坐标构造的 QRect。这个矩形没有经过 mapToScene 转换,viewport 坐标在缩放场景中不等于场景坐标,错位量等于 view 的缩放倍数。手绘橡皮筋用 viewport 坐标是没问题的,但最终入场景的 item 几何信息必须是场景坐标。
解决:release 时用 mapToScene(releaseRect.topLeft()) 和 mapToScene(releaseRect.bottomRight()) 分别转换两个对角顶点,再构造场景坐标的 QRectF。我在排查这类问题时养成了一个习惯:把鼠标事件坐标和实际生成的 item rect 同时打印到控制台,对比一下就能看出坐标基准是否统一。
4.5 高 DPI 屏幕上矩形明显偏移半个像素
现象:在 Windows 高分屏上运行,DPI 缩放为 150% 或 200% 时,画出的矩形边框和鼠标指针之间有半像素到一像素的偏移,缩小时不明显,放大时很明显。
原因:Qt 6 默认开启高 DPI 缩放后,event->pos() 返回的是设备无关像素坐标。如果代码里手动使用了 winId() 相关接口或者用取整做过多次转换,坐标精度会丢失。常见误操作是"补偿缩放"时手动除以 devicePixelRatio——Qt 已经处理过了,再除一次反而错位。
解决:全部使用 event->pos() 和 mapToScene 提供的接口,不要手动除以 DPR。确保代码里没有混用 QWidget::pos() 和 QMouseEvent::pos() 两个来源的坐标。把 setRenderHint(QPainter::Antialiasing, true) 开启也能减少视觉上的锯齿错位。
5. 二次开发:把画矩形扩展成你的标注画布
5.1 改造一:按住 Shift 画正方形
这是标注工具里最常见的需求之一。做法是在 mouseMoveEvent 和 mouseReleaseEvent 里检查修饰键,当 Shift 按下时,把矩形的宽和高强制对齐为相等。下面这段逻辑可以直接替换 3.2 节橡皮筋绘制里的 currentRect 计算:
QRect currentRect = QRect(m_startPos, event->pos()).normalized(); if (event->modifiers() & Qt::ShiftModifier) { int side = qMax(currentRect.width(), currentRect.height()); if (event->pos().x() >= m_startPos.x()) { currentRect.setRight(currentRect.left() + side); } else { currentRect.setLeft(currentRect.right() - side); } if (event->pos().y() >= m_startPos.y()) { currentRect.setBottom(currentRect.top() + side); } else { currentRect.setTop(currentRect.bottom() - side); } }注意 release 里也要做同样的修正,否则预览是正方形、松手后却变成普通矩形,看起来就像卡顿了一下。修饰键判断用 event->modifiers() 而不是 Qt::KeyboardModifiers,避免拿旧状态判断。
5.2 改造二:按图形类型扩展,抽一个工厂逻辑
只画矩形终究不够用,椭圆、箭头、折线都是标注工具的常客。这时候不要在 DrawView 里堆 if-else 分支,我的做法是定义一个枚举表示当前工具类型,再在 release 里用工厂方式创建对应的 QGraphicsItem:
enum class DrawShape { Rectangle, Ellipse, Line }; void DrawView::submitShape(const QRectF &sceneRect, DrawShape shape) { QGraphicsItem *item = nullptr; switch (shape) { case DrawShape::Rectangle: item = new QGraphicsRectItem(sceneRect); break; case DrawShape::Ellipse: item = new QGraphicsEllipseItem(sceneRect); break; case DrawShape::Line: { auto *line = new QGraphicsLineItem( QLineF(sceneRect.topLeft(), sceneRect.bottomRight())); item = line; break; } } if (item) { item->setPen(QPen(QColor(0, 160, 255), 2)); item->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable); scene()->addItem(item); } }这个方向不需要改架构,新增形状只需扩枚举、加一个 case 分支,DrawView 的其他逻辑完全复用。拖拽预览阶段保留"画矩形"的显示逻辑,之后再按工具类型调整预览样式。
5.3 改造三:把矩形数据序列化到文件
标注完的矩形列表要能保存、能重新加载,二次开发里最实用的就是这步。做法是遍历 scene 的所有 item,把类型和几何数据写进 JSON:
QJsonArray array; const auto items = scene()->items(); for (QGraphicsItem *item : items) { if (auto *rectItem = dynamic_cast<QGraphicsRectItem *>(item)) { QJsonObject obj; obj["type"] = QStringLiteral("rect"); obj["x"] = rectItem->rect().x(); obj["y"] = rectItem->rect().y(); obj["w"] = rectItem->rect().width(); obj["h"] = rectItem->rect().height(); array.append(obj); } }加载时反向读取 JSON,逐条 new QGraphicsRectItem 并 addItem 到场景。有了这个基础,再往上是撤销重做就只剩维护一个 item 列表栈的问题了。
这个功能我第一次写的时候直接画在 QWidget 上,后来改成 GraphicsView 又踩了坐标换算的坑,前前后后改了三四遍才跑顺。从那以后我做任何画布功能,都先想清楚三个问题:绘制是临时预览还是持久图元、坐标基准是像素还是场景单位、事件是否需要透传给已有 item,把这三件事定下来再动手写代码,基本不会再翻车。希望帮到你。
本文还有配套的精品资源,点击获取