news 2026/10/21 21:51:03

Qt GraphicsView鼠标绘制矩形:坐标换算与橡皮筋交互

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt GraphicsView鼠标绘制矩形:坐标换算与橡皮筋交互

简介:这是一个基于QT 5.15.2与VS2019环境编写的C++示例工程,核心利用QGraphicsView与QPainter实现鼠标矩形绘制,并将交互逻辑封装为QGraphicsRectItem。工程面向需要快速掌握图形视图框架与鼠标事件处理的Qt界面开发者,尤其适合刚接触QGraphicsView自定义图元、缩放、选择与拖拽操作的初学者,可直接作为教学演示或项目原型参考。资源共16个文件,以头文件、源文件、UI界面文件及工程配置文件为主,结构精简,便于直接打开编译和对照学习。目前已有2349人学习下载。通过该demo可完整了解矩形绘制、边缘拉伸调整宽高、按住拖动、Ctrl+滚轮缩放,以及右键菜单中的删除与保存功能;稍作修改即可扩展到多边形等其它图元的编辑处理,兼顾基础学习与二次开发需求。其中涉及的场景坐标与视图坐标转换、图元选中与事件分发等知识点,对深入理解QGraphicsView框架也有直接的帮助。

1. 用鼠标在 GraphicsView 里画矩形:这件事的核心不在"画"而在交互状态机

画图软件里最普通的功能——按住鼠标拖一个矩形出来——放到 Qt 的 GraphicsView 框架里实现,很多人第一反应是重写 paintEvent 然后攒一个 QPixmap,结果画着画着就碰到了缩放变形、坐标错位、拖出来的矩形闪一下就没了这些怪问题。原因很简单:QGraphicsView / QGraphicsScene / QGraphicsRectItem 这套模型里,鼠标拖拽的每一帧都要完成"视图坐标到场景坐标的换算、旧预览的销毁、新图形的重建"三个动作,任何一个环节没理顺,表现就是各种玄学。这篇文章就是帮你把这条链路彻底拆开,从事件循环讲到底层 Item 管理,再给出一套可以直接复用的 C++ 代码,适合那些手头有标注工具、流程图编辑器、截图框选需求,或者单纯想把 Qt 的 GraphicsView 交互吃透的开发者。我会把坐标换算、橡皮筋预览、矩形防误触这些细节全部摊开讲,源码结构也按工程标准拆好,你照着搭一遍就能改成自己的组件。

2. 为什么要在 QGraphicsView 里实现鼠标绘制矩形:先把三层架构和事件流理清楚

2.1 QGraphicsView 三件套到底是怎么协作的

QGraphicsView、QGraphicsScene、QGraphicsItem 是 Qt 图形视图框架的三个核心类,很多人一开始会混淆它们的边界。简单说,QGraphicsView 是你在界面上看到的"窗口",负责把场景渲染出来、接收鼠标键盘事件;QGraphicsScene 是"世界",管理所有 Item 的位置、碰撞、图层顺序,它可以比视图大很多,也可以被多个视图共享;QGraphicsRectItem 则是"物体",它知道自己所在的坐标、大小、画笔、画刷,还知道自己属于哪个 Scene。

当你在 QGraphicsView 上按下鼠标左键时,事件首先被 View 的 mousePressEvent 捕获,此时拿到的是 viewport 坐标。这个坐标不能直接用来创建矩形,因为 View 可能有滚动条、可能做了缩放、甚至设置了变换矩阵,viewport 上的 (100, 50) 映射到 Scene 里可能是 (200, -20)。正确的做法是用 mapToScene 这个函数把 viewport 坐标转换成场景坐标,再用场景坐标去创建或更新 QGraphicsRectItem。这套关系搞清楚以后,你会发现"鼠标画矩形"这件事,本质上就是一个在 View 上接收事件、在 Scene 上增删 Item 的过程。

我见过不止一个新人在 QGraphicsView 的子类里重写 paintEvent,想着每次鼠标移动就把图重画一遍,结果是又跟 Scene 的渲染冲突,又因为刷新区域没控制好导致拖影。其实完全没必要碰 paintEvent,View 只负责把事件坐标交出去,剩下的矩形显示交给 Scene 自己管,框架会帮你做局部刷新和碰撞检测,这比手动管理位图要稳定得多。

2.2 橡皮筋式交互的三大状态与事件脉络

鼠标绘制矩形属于典型的橡皮筋交互:按下时确定起点,移动时实时更新终点,松开时确定最终矩形。整个过程需要维护三个状态:待命、拖拽中、已结束。在代码层面,这个状态机一般通过一个 bool 成员变量加上一个指向当前预览矩形的指针来表达。我一般会定义一个 QGraphicsRectItem* currentRect 和一个 QPointF 起点,在 mousePressEvent 里把 currentRect 创建出来并加入 Scene,在 mouseMoveEvent 里更新它的 rect,在 mouseReleaseEvent 里做收尾工作。

有个容易踩坑的细节:移动鼠标时旧矩形如果直接调用 setRect,画面会更新,但如果你在 setRect 之前没有判断起点和当前点是否重合,就会生成一个宽或高为 0 的退化矩形,这种 item 虽然不会报错,但在后续的碰撞检测里会表现得非常诡异。常见做法是维护一个能产生有效矩形的逻辑:用 QRectF(startPoint, endPoint).normalized() 来保证宽高为正,并且在 mouseMoveEvent 里直接拿这个 normalized 之后的矩形去 setRect。这样从事件发生到画面反馈的整个链路就走通了,而且代码量非常少。

2.3 为什么不用 QPainter 直接画在 View 上:可管理性的差距

可能有人会问,矩形数量少的时候用 QPainter 在 paintEvent 里画位图,不是更简单吗?如果你只想画一个固定矩形,确实可以直接 paintEvent 一把梭,但只要你需要对这些矩形做选中、拖拽、删除、缩放,或者让它们跟其他图元交互,位图方案就基本没救了。QGraphicsRectItem 天然支持 setPos、setRect、setPen、setBrush,还可以通过 setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable) 让用户直接拖动已画好的矩形,这些功能如果用 QPainter 全得手写,而且还得自己管理"哪个矩形被点中了"。

QGraphicsScene 本身还提供 items(QPointF) 这个查询接口,给定一个场景坐标就能返回该位置命中的所有 Item 列表,用来做点击拾取和碰撞检测非常方便。相比之下,自己维护一个矩形坐标数组再逐一遍历判断鼠标是否命中,代码量大且不容易处理嵌套图元和复杂变换。所以结论很明确:只要你的场景里需要存在多个矩形、需要后续编辑,就老老实实用 QGraphicsRectItem,把绘制工作交给框架,你只负责业务逻辑。

3. 核心实现:一份可以直接跑起来的鼠标绘制矩形代码

3.1 自定义 View 类的头文件:把接口设计得通用一点

先给一份我实际项目里用过的头文件结构。类名我习惯叫 DrawRectView,它继承 QGraphicsView,内部维护橡皮筋绘制的全部状态,同时暴露一个信号让外部知道矩形绘制完成了,这样可以方便地把矩形坐标同步到属性面板或者存储模块。

#ifndef DRAW_RECT_VIEW_H #define DRAW_RECT_VIEW_H #include <QGraphicsView> #include <QGraphicsRectItem> #include <QPointF> class DrawRectView : public QGraphicsView { Q_OBJECT public: explicit DrawRectView(QWidget *parent = nullptr); // 设置画笔、画刷、颜色,后续创建矩形时统一使用 void setRectPen(const QPen &pen); void setRectBrush(const QBrush &brush); // 是否启用绘制模式;关闭后鼠标拖拽不再创建矩形 void setDrawingEnabled(bool enabled); bool drawingEnabled() const; signals: // 矩形绘制完成,返回场景坐标系下的矩形 void rectFinished(const QRectF &sceneRect); protected: void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; private: bool m_drawingEnabled = true; bool m_isDrawing = false; QPointF m_startScenePos; QGraphicsRectItem *m_currentRect = nullptr; QPen m_rectPen; QBrush m_rectBrush; }; #endif // DRAW_RECT_VIEW_H

这份声明里有几个参数值得说明。m_drawingEnabled 是给外部控制用的,比如在已有图元上做点击操作时,需要临时禁用绘制模式,避免误触画出小矩形。m_currentRect 是指针而不是值对象,因为鼠标移动过程中这个 item 的坐标不断变化,必须通过指针来操作同一个实例。信号 rectFinished 发出去的是 QRectF,它是值类型,跨线程或跨模块传递都很安全,不需要担心 item 被销毁后悬空的问题。

3.2 源文件实现:三个事件函数把交互状态机跑起来

接下来是 .cpp 文件,重点看三个鼠标事件函数怎么配合。除了事件处理之外,构造函数里还需要对 View 做一些基础设置,比如开启抗锯齿、关闭 scrollbar 误触、设置场景范围等。

#include "DrawRectView.h" #include <QMouseEvent> #include <QScrollBar> DrawRectView::DrawRectView(QWidget *parent) : QGraphicsView(parent) { // 让场景比视口大很多,避免拖拽时自动扩展到底图边界 setScene(new QGraphicsScene(this)); scene()->setSceneRect(-5000, -5000, 10000, 10000); // 抗锯齿让矩形的边框在缩放时更平滑 setRenderHint(QPainter::Antialiasing); // 这两行保证拖动鼠标时视图本身不滚动,事件全部交给 item 处理 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // 默认画笔画刷 m_rectPen = QPen(QColor(0, 120, 215), 2); m_rectBrush = QBrush(QColor(0, 120, 215, 30)); } void DrawRectView::mousePressEvent(QMouseEvent *event) { // 只在左键按下且绘制模式开启时启动橡皮筋 if (event->button() == Qt::LeftButton && m_drawingEnabled) { m_isDrawing = true; m_startScenePos = mapToScene(event->pos()); // 创建预览矩形,初始为一个退化矩形,后续 move 事件里修正 m_currentRect = new QGraphicsRectItem(QRectF(m_startScenePos, m_startScenePos)); m_currentRect->setPen(m_rectPen); m_currentRect->setBrush(m_rectBrush); m_currentRect->setFlag(QGraphicsItem::ItemIsSelectable, false); scene()->addItem(m_currentRect); event->accept(); return; } QGraphicsView::mousePressEvent(event); } void DrawRectView::mouseMoveEvent(QMouseEvent *event) { if (m_isDrawing && m_currentRect) { // 先把视口坐标转为场景坐标,再用 normalized 保证宽高为正 QPointF scenePos = mapToScene(event->pos()); QRectF newRect = QRectF(m_startScenePos, scenePos).normalized(); m_currentRect->setRect(newRect); event->accept(); return; } QGraphicsView::mouseMoveEvent(event); } void DrawRectView::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton && m_isDrawing) { if (m_currentRect) { QRectF finalRect = m_currentRect->rect(); // 过滤过于小的矩形,防止误触产生垃圾数据 if (finalRect.width() > 2.0 && finalRect.height() > 2.0) { emit rectFinished(finalRect); } else { // 太小就删掉,不当做有效绘制 scene()->removeItem(m_currentRect); delete m_currentRect; } } m_currentRect = nullptr; m_isDrawing = false; event->accept(); return; } QGraphicsView::mouseReleaseEvent(event); } void DrawRectView::setRectPen(const QPen &pen) { m_rectPen = pen; } void DrawRectView::setRectBrush(const QBrush &brush) { m_rectBrush = brush; } void DrawRectView::setDrawingEnabled(bool enabled) { m_drawingEnabled = enabled; // 如果正在绘制期间被禁用,主动取消当前绘制 if (!enabled && m_isDrawing) { if (m_currentRect) { scene()->removeItem(m_currentRect); delete m_currentRect; m_currentRect = nullptr; } m_isDrawing = false; } } bool DrawRectView::drawingEnabled() const { return m_drawingEnabled; }

这段代码有四个地方需要重点解释。第一,mapToScene 的调用时机必须是在获取鼠标 position 之后立刻进行,不能先存一个 QPoint 后面再转,因为如果 View 在这期间发生了滚动画面的操作,同一个 QPoint 对应的场景坐标就变了。第二,normalized() 这个函数很有用,它保证你从右下往左上拖拽时,rect 的左上角坐标始终小于右下角坐标,后续做标准化存储就不会出现负宽度。第三,mouseReleaseEvent 里做了一个最小尺寸过滤,宽度和高度都小于 2 像素的矩形直接丢弃,这个阈值你可以根据实际精度需求调整,我用 2.0 是为了避免用户单击时误生成一个像素大小的点。第四,setFlag(QGraphicsItem::ItemIsSelectable, false) 这句看起来多余,但其实很重要,如果你不显式关闭,生成后的矩形会在鼠标松开时立刻被选中,然后你后续再点击它时它会跟着鼠标移动,就跟绘制功能打架了。

3.3 主窗口怎么接住信号:把场景坐标变成业务数据

View 本身只负责产出矩形,真正要把矩形用起来,需要外部拿到 rectFinished 信号。下面是一个简单的主窗口代码片段,演示如何接收信号并把矩形信息打印出来或者存入列表。

#include <QMainWindow> #include <QDockWidget> #include <QPlainTextEdit> #include "DrawRectView.h" class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr) : QMainWindow(parent) { auto *view = new DrawRectView(this); setCentralWidget(view); auto *textEdit = new QPlainTextEdit(this); auto *dock = new QDockWidget("输出", this); dock->setWidget(textEdit); addDockWidget(Qt::RightDockWidgetArea, dock); // 每画完一个矩形,就在文本区域记录一次场景坐标 connect(view, &DrawRectView::rectFinished, this, [textEdit](const QRectF &rect) { QString msg = QString("rect: (%1, %2) w=%3 h=%4") .arg(rect.x()) .arg(rect.y()) .arg(rect.width()) .arg(rect.height()); textEdit->appendPlainText(msg); }); } };

信号槽连接这里用了 Lambda 表达式,好处是不用在主窗口里单独定义一个槽函数。注意 rectFinished 发的坐标是场景坐标系,如果你后续要存到文件里,最好统一这套坐标系,不要混用 viewport 坐标。还有一点是当你在 QDockWidget 里显示这些数据时,刷新频率不需要太勤,画完之后才发信号,完全不会影响鼠标拖拽的流畅度。

3.4 测试时要注意的 View 尺寸与 Scene 范围参数

跑这段代码之前,有几个 View 自身的属性值得先确认。第一,如果 View 比较小而你拖拽速度很快,鼠标事件不会丢失,但 mapToScene 得到的结果可能会超出当前场景边界,这在后续保存坐标时会让别的模块困惑,所以构造函数里我给场景设置了 ±5000 的范围,相当于给数据留出安全区。第二,如果你开启了滚轮缩放,也就是在 View 上加 setTransform,那么必须确认你更新矩形时使用的是 mapToScene 而不是简单地减一个偏移量,否则缩放后绘制的矩形会跟鼠标位置错位。第三,当 View 的 scrollBarPolicy 改成 AlwaysOff 之后,场景如果比视口大,你是滑不到边缘的,所以场景范围要按你的实际业务需求来定,不要照抄我的 ±5000。

4. 从 Demo 到可用组件:坐标系换算、渲染优化与多矩形管理

4.1 viewport 坐标、场景坐标与 Item 坐标:三者的换算关系

画矩形只是开始,真正让这个功能在复杂项目里站稳,必须把坐标映射机制搞透。鼠标事件给的是 viewport 坐标,它的原点在 QGraphicsView 的显示区域左上角;场景坐标是 Scene 的世界坐标,原点位置由 SceneRect 决定;Item 坐标则是每个图元自己的局部坐标,原点在 Item 的边界框左上角附近。QGraphicsView 里 mapToScene 和 mapFromScene 这两个函数负责第一组映射,QGraphicsItem 的 mapFromScene 和 mapToScene 负责第二组。如果你把 Item 添加到了 Scene,系统会自动维护这三级坐标的关联,前提是你不要直接修改 Item 内部的 QRectF 以外的东西。

有一个现象能帮你检测自己是否搞混了坐标系:画完矩形后鼠标点击矩形的右下方边缘,程序取到的 Item 坐标如果跟预期不一致,十有八九是你在处理点击事件时忘了调用 mapToScene 或者在 item 的局部坐标里混用了 viewport 数值。我一般会在调试阶段给每个矩形 addChild 一个小的文本 item,显示它的 scene rect 数值,这样一边拖一边看数值变化,比打日志直观得多。

4.2 画笔宽度、抗锯齿与缩放时的表现差异

QGraphicsRectItem 的 pen 宽度默认是 1,如果你想在放大视图时让边框粗细跟着变化,直接用默认设置就行;但如果你希望边框在所有缩放级别下都保持固定屏幕像素宽度,就得开启 cosmetic pen。具体做法是在 QPen 上调用 setCosmetic(true),或者用 setWidth(0) 表示宽度自适应屏幕。很多做地图标注的开发者会纠结"框线为什么放大后粗得吓人",这就是没区分这两种模式。如果你画的矩形是用于精确框选目标对象,我建议边框保持 cosmetic,也就是无论缩放多少,边框始终一条细线,不会遮挡目标内容。但注意 cosmetic pen 打印导出时不会按场景实际比例缩放,存 PDF 时需另作处理。

抗锯齿方面,setRenderHint(QPainter::Antialiasing) 要在 View 上设置而不是在 Scene 上。如果你发现缩放之后矩形边缘有锯齿,检查一下是否在 viewport 的 paintEvent 里被覆盖了。框架默认的渲染流程会读取 view 的 render hints,所以只要你在构造函数里设置一次,所有 Item 都会生效。还有一个容易被忽略的参数是 QGraphicsScene::setItemIndexMethod,默认是 BspTreeIndex,这在 item 数量很多时有加速效果,但如果你频繁增删 item,BSP 索引重建反而带来开销,这种情况下可以改成 NoIndex 试试。

4.3 同场景多矩形的选中、拖动与层级管理

当场景里存在多个矩形时,只用当前类里的代码是不能直接拖动已有矩形的,因为 mousePressEvent 里每次左键按下都创建新矩形。如果你需要在"画新矩形"和"编辑旧矩形"两种模式之间切换,我会把 setDrawingEnabled 当做一个总开关,关闭绘制模式后,鼠标事件会走 QGraphicsView 的默认逻辑,也就是选中并拖动已经存在的 Item。但要让已有矩形能被拖动,创建它时就得设置 ItemIsMovable 和 ItemIsSelectable 标志,前面为了防误触我关闭了这两个标志,所以需要提供另一个配置函数来创建可编辑矩形。

层级管理上,QGraphicsScene 里的 Item 默认按创建顺序叠放,后创建的在上层。如果你想新的矩形压在旧矩形上面,不用特别处理;如果你想实现"正在绘制的矩形永远在最上层",可以在 mousePressEvent 里调用 m_currentRect->setZValue(MAX_Z),松开后恢复到默认层级。常见做法是让绘制中的矩形 zValue 设为 1000,完成后设回 0,这样即使场景里已有大量图元,预览框也不会被遮挡。

4.4 大量矩形绘制的性能:刷新区域与存储结构

如果你需要连续画几千个矩形,比如批量标注任务,那么性能问题就会出现。QGraphicsRectItem 本身很轻,每个 item 只是存储一个 QRectF、一个 QPen、一个 QBrush,内存占用不是瓶颈,瓶颈在于 Scene 内部的索引更新和重绘区域计算。BspTreeIndex 在 item 数量上来之后有明显加速,但你需要避免在一个循环里连续 addItem 而不给 UI 呼吸时间,否则界面会卡住。常见做法是先把要画的数据攒成列表,一次性 addItem,再调用 scene()->update(),让 Scene 只在最后做一次区域刷新。如果你需要频繁更新某个矩形的内容,优先使用 item->setRect 而不是 removeItem 再加一个新的,因为 setRect 会触发增量更新,而重建 item 会引发索引重建。

性能还有一个优化点是矩形轮廓的简化计算。如果你的矩形都具有统一的画刷和画笔,可以考虑使用 QGraphicsRectItem 之外的自定义 item,把所有矩形合并到一个 QGraphicsItem 的 paint 里去画,这样 Scene 维护的 item 数量大幅减少,但代价是你必须自己管理命中检测。适合那些矩形数量过万、需要流畅拖拽视口的极端场景,一般项目里用不上,知道这个方向就行。

5. 避坑指南:鼠标画矩形最常见的五个翻车现场

5.1 矩形画出来一闪而过,松手就消失

表现是拖拽过程中能看到框,但鼠标松开瞬间矩形从场景里消失了。原因是 mouseReleaseEvent 里不小心把 removeItem 和 delete 调用放在了所有分支里,或者信号连接的一方在释放事件前把 Item 从场景移除了。解决方法是把删除逻辑严格限定在"无效矩形"的分支里,有效矩形只发信号不动 scene。另外,如果你把 scene()->clear() 放在不该放的槽函数里,也会造成所有矩形消失。

5.2 鼠标坐标对不上:画出来的矩形不在鼠标位置

现象是矩形位置跟鼠标图标有明显偏移,尤其在 View 有滚动条或者经过 setTransform 缩放之后。原因是用了 event->pos() 直接构造 QRectF,没有经过 mapToScene。解决方法是统一在三个鼠标事件里都先调用 mapToScene(event->pos()),再基于场景坐标做计算。我还见过一种情况是 View 的 layout 在窗口 resize 时产生了偏移,但 event->pos() 拿到的是 viewport 坐标,不受窗口布局影响,所以问题基本都出在漏掉坐标换算。

5.3 从右下角往左上角拖,生成的矩形宽高是负数

表现是虽然框能显示,但保存下来的 rect 的 width 和 height 出现负值,后续做数据处理时经常报错。原因是你直接用起点和终点构造 QRectF,没有调用 normalized。解决方法是每次 setRect 前都先对矩形做 normalized 处理,也建议在 rectFinished 信号里再拿到的 rect 做一次标准化,保证所有下游数据都是正宽高。这个坑太常见了,我会直接建议把 normalized 养成肌肉记忆。

5.4 画完的矩形一碰就跑:新画的矩形直接可拖动

现象是鼠标松开后,矩形跟手移动了,颜色也变蓝,看起来像被选中。原因是创建矩形时没有关闭 ItemIsMovable 和 ItemIsSelectable 标志,而 View 默认允许鼠标拖动可移动 item。解决方法是新建预览矩形时用 setFlag 把这两个标志关掉,并且检查你手动设置的标志是否被后续代码改回去。比如在 mouseReleaseEvent 里为了优化临时开启过 ItemIsSelectable,用完没关,就会出现这个情况。

5.5 场景里 Item 多了之后,绘制开始卡顿掉帧

现象是画三五十个矩形没问题,画到几百个时拖动鼠标明显不跟手。原因是 QGraphicsScene 默认的 BspTreeIndex 在频繁插入 item 时重建代价大,而且你没有限制刷新区域。解决方法是先确认是否采用了批量添加而非逐点添加,再检查 View 是否开启了可能触发全量重绘的属性。我一般会在创建 View 后立即设置 scene()->setItemIndexMethod(QGraphicsScene::NoIndex),然后测试一次性能,如果 item 数量确实需要上万才考虑用自定义 item 合并绘制。

5.6 补充一个非视觉坑:信号发送时的坐标系不统一

前面几节都在讲视觉异常,还有一个容易忽略的问题是信号里带出的坐标坐标系混乱。比如你在 mousePressEvent 里存了一个 viewport 坐标,release 时却拿场景坐标去发信号,外部收到的矩形就跟实际画的位置对不上。解决方法是整个类内部统一只用场景坐标传递数据,在事件入口处立刻转换,之后所有逻辑都基于场景坐标。这样无论 View 的缩放比例怎么变,外部拿到坐标都能正确映射回场景。

6. 画完不算完:给矩形加上编辑手柄和一点工程习惯

矩形绘制功能跑通之后,下一步往往就是让它可编辑——选中一个已画矩形,出现八个拖拽手柄,拖动边角调整大小,拖动中间移动位置。QGraphicsRectItem 本身支持 Items are movable,但缩放手柄需要自己实现。我常用的方案是给每个矩形附加一个子 item 作为控制点,父 item 的重构改为 QGraphicsObject,并在 pained 里根据当前模式绘制形状。下面是一个简化的手柄实现思路:

class ResizableRectItem : public QGraphicsObject { Q_OBJECT public: ResizableRectItem(const QRectF &rect, QGraphicsItem *parent = nullptr) : QGraphicsObject(parent), m_rect(rect) { setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable); } QRectF boundingRect() const override { return m_rect.adjusted(-8, -8, 8, 8); } void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override { painter->setPen(QPen(Qt::blue, 1)); painter->setBrush(QColor(0, 120, 215, 60)); painter->drawRect(m_rect); // 只画四个角上的手柄,边线不画太多点 QVector<QPointF> handles = { m_rect.topLeft(), m_rect.topRight(), m_rect.bottomLeft(), m_rect.bottomRight() }; painter->setBrush(Qt::white); for (const QPointF &p : handles) { painter->drawRect(QRectF(p.x() - 4, p.y() - 4, 8, 8)); } } private: QRectF m_rect; };

这段代码的关键是 boundingRect 必须把控制点的范围包含进去,否则手柄画出来会被裁剪掉。你还需要在 item 里重写 mouseMoveEvent,判断当前点击点是否靠近某一个手柄,然后更新 m_rect 并调用 update()。这个方案比直接用 QGraphicsRectItem 加外部边框的方式更可控,缺点是要自己维护手柄的命中逻辑。如果你的项目需要撤销重做功能,建议把每次调整前后的 m_rect 快照存到一个命令栈里,这样比直接改坐标容易回退。

我从那以后做所有标注类工具,都会在项目初期就把"绘制模式"和"编辑模式"彻底分开,绘制完成的同时立刻把 item 的可选中和可移动标志按照编辑模式的要求设好,避免后期返工。每次新增一个交互动作,就强制自己过一遍坐标换算、标志位、信号参数这三件事,几乎没再出现过拖拽错位的玄学问题。希望这篇笔记能帮你少走这些弯路,把时间省下来打磨真正属于你的功能逻辑。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 21:50:57

基于YOLOv8的车牌识别系统实战:从数据到部署全流程解析

前一阵帮某高校的A同学把“基于深度学习YOLOv8的智能车牌识别系统设计”从头到尾跑通了一遍&#xff0c;从需求分析、数据准备、模型训练到界面打包&#xff0c;整整折腾了两周。做完之后不少人来问&#xff1a;这套东西到底怎么落地&#xff1f;报告怎么写&#xff1f;答辩PPT…

作者头像 李华
网站建设 2026/10/21 21:50:13

知识管理插件实践:从信息捕获到自动化输出

做知识管理这几年&#xff0c;一个很深的体会是&#xff1a;工具越多&#xff0c;越容易把"收集"当成"掌握"。书签攒了几百条&#xff0c;稍后读从来没读完&#xff0c;笔记软件里躺着大量剪藏&#xff0c;真要输出一份报告时却连一个能直接引用的段落都翻…

作者头像 李华
网站建设 2026/10/21 21:50:08

轻量地图打点与整理工具开发:GeoJSON数据模型与前端实践

前阵子整理手机里的地图收藏夹&#xff0c;发现一个尴尬的事实&#xff1a;我收藏了两百多个地点&#xff0c;但真正要用的时候一个也想不起来。它们分散在不同地图应用、聊天记录和截图里&#xff0c;分类只有“想去”和“收藏”两种状态&#xff0c;至于“为什么想去”“和谁…

作者头像 李华
网站建设 2026/10/21 21:48:55

MongoDB 用户与权限管理实战:从认证授权到最小权限角色设计

1. 从一次权限误操作说起&#xff1a;为什么用户管理不能靠“凭感觉” 刚接手一个内容管理系统的数据库维护时&#xff0c;我犯过一个很典型的错误。当时为了图省事&#xff0c;直接给应用连接账号分配了 readWriteAnyDatabase 角色&#xff0c;想着“反正只是读写&#xff0…

作者头像 李华
网站建设 2026/10/21 21:48:47

基于Edge网关的PLC扫描枪数据采集与MES接口暴露实战

1. 项目缘起与整体架构拆解车间里那台老PLC还在吭哧吭哧跑着产线逻辑&#xff0c;扫描枪往工位上一架&#xff0c;操作员“滴”一声扫完条码&#xff0c;数据就得往MES系统里送。这个场景听起来简单&#xff0c;但真正落地的时候&#xff0c;麻烦事一堆&#xff1a;扫描枪是串口…

作者头像 李华
网站建设 2026/10/21 21:48:10

1000BASE-T一致性测试实战:示波器与信号发生器避坑指南

一开始&#xff0c;先说说为什么这玩意儿让人头疼做过以太网硬件或者Layout的工程师基本都有一个共同的记忆&#xff1a;千兆网口调通了、也能跑满速了&#xff0c;但只要遇上“一致性测试”四个字&#xff0c;心里就咯噔一下。尤其是在1000BASE-T这种四对线同时双向传输的方案…

作者头像 李华