资讯动态

基于QGraphicsView的Qt鼠标绘制矩形实战与避坑指南

发布时间:2026/10/21 13:14:15 来源:尧图企业网站定制
简介一份基于Qt 5.15.2与VS2019的源码工程展示QGraphicsView、QPainter与QGraphicsRectItem三者配合完成鼠标绘制矩形的实现方式。这个小型演示覆盖了在视图上框选绘制一个或多个矩形、拖拽图元移动位置、拉动边缘调整宽高、按住Ctrl滚动滚轮实现整体缩放以及右键删除和保存结果等常用交互所有操作围绕图元事件与视图坐标处理展开逻辑结构清楚参照相同思路也可以绘制多边形与其他图形。资源包采用zip压缩大小仅18KB包含16个文件其中5个头文件与5个C源文件构成主要代码剩余文件包括界面定义、资源描述、工程配置等可直接在VS2019中加载编译。已有2349人学习或下载适合刚开始接触Qt图形视图框架或需要在项目里实现自定义绘图控件的开发者借鉴。从示例中不仅能理解视图分发鼠标事件与坐标换算的原理还能掌握图元选中、拖动、实时重绘以及配合Ctrl缩放的操作细节对于希望从事图像标注、CAD制图或界面编辑器开发的读者是一份入门极佳的参考。1. 在 Qt 里用鼠标画矩形为什么我不直接写 paintEvent做矩形标注、截图框选这类交互时我第一个想到的是 QGraphicsView而不是直接在 paintEvent 里画矩形。核心原因是 QGraphicsView 自带 scene 坐标与 viewport 坐标换算还有现成的图元选中、拖拽和缩放能力省下的代码量比预想多一倍。这篇笔记拆一个基于 QGraphicsView QGraphicsRectItem QPainter 的鼠标绘制矩形实现从鼠标按下到松开在场景中画出可选中、可拖拽、可导出的矩形图元。适合刚接触 Qt 图形视图框架的开发者也适合想快速搭建框选标注工具的人。代码量不大但坑不少整体是一条可以直接跟进来的最小可运行路径。2. 三层架构与首个画布为什么选 QGraphicsView 而不是 paintEvent2.1 三层图形框架选型view、scene、item 各管一摊写矩形绘制技术上至少有三条路可以走直接在 QWidget::paintEvent 里记录坐标、调用 QPainter::drawRect用 QRubberBand 做橡皮筋再落图或者把整套交互放进 QGraphicsView 框架。常见的做法是选 QGraphicsView三层架构里每个矩形是一个 QGraphicsRectItem 实例view 负责把 scene 坐标画到窗口scene 负责管理所有图元之间的层次与碰撞item 只负责自己的绘制和几何变化。这样带来的直接好处是缩放、平移、选中、拖拽、碰撞检测全部由框架接手不需要在 paintEvent 里重复实现。与之对比paintEvent 方案的“命中测试”要自己遍历矩形集合每次鼠标移动都要记录 dirty 区域并调用 update()一旦矩形数量上百刷新性能就会明显退化。QGraphicsView 当然也有自己需要适应的地方比如事件分发机制比普通 QWidget 复杂但它的收益更直接你不需要维护矩形数组和重绘逻辑只需要关心图元数据和业务规则。对做标注工具、结构示意图编辑器的小团队来说这个选型基本是标准答案。举个例子光标在屏幕上移动view 收到的是 QMouseEvent 里的窗口 widget 坐标。框架内部通过 viewport 变换把它换算成 scene 坐标持有的是同一个逻辑坐标系里的 QRectF。这样一来窗口被拉伸、视图被 scale 之后矩形依然贴着鼠标走这正是新手最容易写错的部分。很多教程教你先扣像素再自己换算其实在图形视图框架里 mapToScene 已经把脏活做完了。2.2 最小骨架一个能显示图元并能画矩形的窗口我一般把代码分成四块main.cpp 启动程序MainWindow 负责搭建 view 与 scene自定义的 CanvasView 继承 QGraphicsView 并处理鼠标事件业务层通过 scene 添加或移除矩形图元。这样做的好处是鼠标交互逻辑与窗口逻辑解耦后面加工具栏、撤消面板都不需要重构画布代码。下面是最小的窗口骨架// mainwindow.h #include QMainWindow class QGraphicsView; class QGraphicsScene; class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent nullptr); private: void setupCanvas(); QGraphicsView *m_view nullptr; QGraphicsScene *m_scene nullptr; };// mainwindow.cpp #include mainwindow.h #include QGraphicsView #include QGraphicsScene MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { setupCanvas(); } void MainWindow::setupCanvas() { m_scene new QGraphicsScene(this); m_scene-setSceneRect(0, 0, 800, 600); // 逻辑坐标范围 m_view new QGraphicsView(m_scene, this); m_view-setRenderHint(QPainter::Antialiasing, true); m_view-setDragMode(QGraphicsView::RubberBandDrag); setCentralWidget(m_view); }这里的参数有三个值得注意。setSceneRect 决定场景的逻辑边界后续导出图片和坐标定位都以它为基准Antialiasing 控制矩形边缘是否出现锯齿画标注线时最好打开RubberBandDrag 是 QGraphicsView 自带的框选模式它拖出来的是选择框而不是业务矩形之后我们会替换成自己的鼠标事件处理。这个最小骨架编译通过后你应该能打开一个空白画布窗口下一步就是让鼠标能“画”出矩形。2.3 自定义 CanvasView把鼠标事件收拢到一个类里把鼠标绘制逻辑直接写在 MainWindow 里虽然能跑但事件处理很容易膨胀。通常做法是定义一个 CanvasView把 press、move、release 三个事件集中在 view 子类里再用信号把“矩形已创建”抛给外部业务层。这样既保留了框架自身的选中、拖拽响应又能在子类里拦截我们不希望冒泡的行为。// canvasview.h #include QGraphicsView class QGraphicsScene; class QGraphicsRectItem; class CanvasView : public QGraphicsView { Q_OBJECT public: explicit CanvasView(QGraphicsScene *scene, QWidget *parent nullptr); protected: void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; signals: void rectCreated(const QRectF sceneRect, const QColor color); private: bool m_drawing false; QPointF m_startPoint; QGraphicsRectItem *m_currentItem nullptr; };在这个声明里m_drawing 是绘制开关m_startPoint 记录鼠标按下时的场景坐标m_currentItem 是当前正在绘制的临时矩形。rectCreated 信号把结果交给外部外部可以用来做几件事存入业务模型、写进 undo 栈、输出 XML。类之间靠信号通信MainWindow 不需要反向依赖 CanvasView 的内部状态。信号里的 QRectF 参数是场景坐标传出去之前不要把坐标值转成 int否则到了高 DPI 屏幕或缩放视图时会损失精度下一章我们逐个填满这三个事件。3. 鼠标事件与坐标转换从 viewport 像素到 scene 逻辑坐标3.1 按下与移动创建临时矩形并实时预览画矩形的核心直觉是“橡皮筋”按下左键时记录起点移动时实时更新一个临时矩形松开时把它转成正式图元。常见做法是在 scene 里直接放置一个临时的 QGraphicsRectItem移动时反复 setRect而不是在 view 上绘制 overlay。原因有两个一是临时矩形会跟着视图缩放自动贴合鼠标位置不需要手动做坐标转换二是如果在 viewport 上直接画 QPainter拖动时还要处理重绘区域稍微一不留神就出现残影。先看按下和移动的处理void CanvasView::mousePressEvent(QMouseEvent *event) { // 先交给基类保证点击图元、取消选择等默认行为不丢失 QGraphicsView::mousePressEvent(event); if (event-button() ! Qt::LeftButton) return; if (m_drawing) return; // 防止一次拖拽中重复触发 m_drawing true; m_startPoint mapToScene(event-pos()); // viewport 像素 - scene 坐标 // 放入场景的临时矩形pen 和 brush 决定预览样式 m_currentItem scene()-addRect( QRectF(m_startPoint, m_startPoint), QPen(Qt::red, 0), // 0 表示不受缩放影响的装饰性线宽 QBrush(Qt::transparent)); }void CanvasView::mouseMoveEvent(QMouseEvent *event) { // 绘制模式下不能再让基类触发默认框选逻辑 if (m_drawing) { QPointF cur mapToScene(event-pos()); // normalized() 保证反向拖拽时矩形宽高不为负 QRectF newRect QRectF(m_startPoint, cur).normalized(); m_currentItem-setRect(newRect); return; } QGraphicsView::mouseMoveEvent(event); }逻辑说明press 里第一行先调基类是为了保留“点空白处取消选择”这类默认交互之后再进入自己的绘制流程。move 里如果 m_drawing 为真一切移动事件都用于更新临时矩形不再交给基类避免和 RubberBandDrag 打架。normalized() 是关键很多人第一次画矩形时从右下往左上拖得到的 QRectF 宽度为负图元会消失或跑到相反方向调用 normalized 后会自动把 rect 调整成左上到右下的标准形式。参数说明QPen(Qt::red, 0) 中第二个参数 0 是装饰性画笔宽度意思是无论视图怎么缩放线宽始终是屏幕上的一像素不随缩放变粗或变细。如果这里写 2视图放大 10 倍后边框也会变成 20 像素宽视觉上标注线粗细会失控。brush 用 transparent因为预览阶段一般只显示边框。3.2 松开鼠标临时矩形转正并送出信号按下和移动阶段已经把预览做出来了松开时要做三件事把临时矩形从预览状态切换为可交互图元把结果通过信号抛给业务层最后清空绘制状态避免下一次点击残留旧指针。void CanvasView::mouseReleaseEvent(QMouseEvent *event) { if (event-button() ! Qt::LeftButton || !m_drawing || !m_currentItem) { QGraphicsView::mouseReleaseEvent(event); return; } m_drawing false; m_currentItem-setPen(QPen(QColor(80, 140, 220), 0)); m_currentItem-setBrush(QBrush(QColor(80, 140, 220, 32))); // 允许用户选中、拖拽该矩形并且位置变化时能触发 itemChange m_currentItem-setFlags(QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemSendsGeometryChanges); m_currentItem-setData(0, m_itemCounter); // 给图元一个自增 id emit rectCreated(m_currentItem-rect(), m_currentItem-pen().color()); // 转正完成后图元交给 scene 管理view 不再持有 m_currentItem nullptr; }这段代码把矩形从临时预览变成正式业务图元。setFlags 决定了用户能否选中、能否用鼠标拖动、以及几何变化时 QGraphicsItem::itemChange 钩子能否被触发后面做撤销重做和属性面板同步时少了这个 flag 会多花很多时间排查。setData(0, id) 是 Qt 图元上的“提示牌”可以在不新增成员变量的前提下给图元挂上业务 id导出和回读时直接 item-data(0).toInt() 就能知道矩形是哪一个。这也是和直接画 painter 的最大区别图元不是一次性绘制输出而是场景中一直存在的对象。你可以随时修改它的 pen、brush、坐标和层级改动会立即反映到画布上。业务层拿到的 rect 是 scene 坐标系里的逻辑矩形和窗口像素无关存到文件里的是这套逻辑坐标下次打开再按比例渲染即可。3.3 mapToScene 的正确用法不要手动除设备像素比地图标注类的坑常在坐标转换鼠标事件的 pos() 是 QWidget 坐标系而图元使用的是 scene 坐标系。mapToScene 内部剥掉了 view 的滚动偏移、缩放比例和变换矩阵得到的是图元真正所在的逻辑坐标所以我前面全部使用 mapToScene(event-pos())。这里有一个常见的“好心办坏事”的操作有开发者发现高 DPI 屏上坐标不对就手动用 event-pos() 除以 devicePixelRatio结果矩形位置更奇怪。Qt6 默认启用高 DPIview 内部已经处理了设备像素比再除一遍反而把原本正确的坐标弄错。另外一个容易踩的点是事件处理顺序。我在 mousePressEvent 里先调用 QGraphicsView::mousePressEvent(event)再进入自己的逻辑。如果顺序反过来基类可能在这个事件中触发选中逻辑或改变鼠标光标后续 mapToScene 的结果会受到多余变换影响。虽然大多数情况下影响细微但把它放在第一行是防御性编码习惯。框架的默认事件处理是在 viewport 层面完成的不要在事件进入前修改 event 的位置值否则默认逻辑和你自己的逻辑会各算各的排查时很难定位。3.4 最小尺寸限制防止误触产生 0x0 矩形实际使用中单击和双击也会经历 press、move、release 三个过程用户只是按下去没有拖动release 时 rect 宽高往往只有一两个像素。这类矩形在标注场景里通常是误操作。常见的做法是在 release 时检查宽高太小就放弃并删除临时图元const qreal kMinSize 4.0; QRectF finalRect m_currentItem-rect(); if (finalRect.width() kMinSize || finalRect.height() kMinSize) { scene()-removeItem(m_currentItem); delete m_currentItem; m_currentItem nullptr; m_drawing false; return; }这个阈值按实际业务定。标注不规则区域时 4 像素已经算很低如果是电路图中的矩形元器件很多工具会设置为至少 8 像素或按网格吸附。我一般会把 kMinSize 做成可配置项不同业务模式用不同阈值。另外需要注意的是临时图元在取消时必须通过 removeItem 加 delete 两步操作只 remove 不 delete 会内存泄漏只 delete 不 remove 会让场景里残留悬空指针后续遍历场景时报错。4. 图元管理与数据导出让画出来的矩形回读成业务数据4.1 让矩形可选中、可拖动QGraphicsRectItem 的 flags 参数业务里几乎总会要求矩形可以被选中、移动、甚至修改大小QGraphicsRectItem 提供了统一的 flag 机制需要准确设置否则会出现“矩形选中不了”或者“拖动后位置没变化”的玄学现象。核心 flag 有三个ItemIsSelectable 决定是否能被点击选中ItemIsMovable 决定是否能用鼠标拖拽ItemSendsGeometryChanges 会在图元位置或尺寸变化时触发 itemChange 钩子。组合效果如下参数组合交互表现典型用途默认无 flag图元完全静态只读标注展示ItemIsSelectable可单击选中不可移动信息浏览、点击定位ItemIsSelectable | ItemIsMovable可选中并拖拽普通标注矩形再加 ItemSendsGeometryChanges拖拽时可同步业务数据联动编辑、撤销栈实际用法是在图元创建后统一配置QGraphicsRectItem *rectItem scene()-addRect(rect, pen, brush); rectItem-setFlags(QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemSendsGeometryChanges); rectItem-setAcceptHoverEvents(true);setAcceptHoverEvents(true) 可以让图元响应 hover 事件常用于“鼠标悬停时改变光标”这类手感优化。如果矩形包含辅助元素可以重写 item 的 shape() 返回一个扩大后的点击区域这样用户不需要精确点在线条上也能选中。4.2 数据回读从画布到业务模型画布上的矩形最终要落到业务数据里一般保存四个值矩形 id、左上角 x、左上角 y、宽高。这里不建议直接把 QGraphicsRectItem 指针塞进业务模型因为图元可能在画布上被删除指针悬空后很难排查。常见做法是保存 id 和 scene 坐标需要时遍历场景再找到对应图元。回读代码可以这样写QListRectData collectRects(QGraphicsScene *scene) { QListRectData result; const QListQGraphicsItem* items scene-items(); for (QGraphicsItem *item : items) { if (auto *rectItem qgraphicsitem_castQGraphicsRectItem*(item)) { RectData data; data.id rectItem-data(0).toInt(); data.rect rectItem-rect(); data.color rectItem-pen().color(); result.append(data); } } return result; }注意 rectItem-rect() 是图元自身的本地坐标。由于我们创建时直接把 scene 坐标传给了 QRectF这里取到的就是 scene 坐标。但如果矩形经过旋转或缩放取到的就是本地矩形还需要用 item-mapToScene(item-rect()) 转换回场景坐标再取包围盒否则保存的数据会偏移。qgraphicsitem_cast 类似 qobject_cast类型不匹配时返回 nullptr遍历场景时不会把其他图元强转成矩形。4.3 用 QPainter 渲染导出图片和屏幕走同一套代码最终交付时往往需要把画布连同矩形一起导出成 PNG 或 PDF。QGraphicsScene 自带 render 方法内部使用 QPainter 完成绘制这意味着导出图片和屏幕显示走的是同一套绘制代码保持显示与导出一致QImage exportSceneToImage(const QGraphicsScene *scene, const QSize targetSize) { QImage image(targetSize, QImage::Format_ARGB32); image.fill(Qt::white); // 画布底色避免透明背景导出后变成黑色 QPainter painter(image); painter.setRenderHint(QPainter::Antialiasing, true); QRectF srcRect scene-sceneRect(); QRectF targetRect(0, 0, targetSize.width(), targetSize.height()); scene-render(painter, targetRect, srcRect); painter.end(); return image; }这里的关键在 render 的三个参数targetRect 是图片上的目标区域srcRect 是场景中的源区域两者比例不一致时框架自动做缩放。如果不传 srcRect默认是整个场景但包含大量空白导出后的图片边缘会有大块白边。如果要导出某个矩形标注区域把 srcRect 设为矩形 rect 扩展若干像素即可整张图正好框住业务区域。4.4 图元层级与阴影让画布更像编辑器矩形之间通常会重叠后画的默认在顶层。管理层级可以用 item-setZValue(value)z 值大的图元绘制在上方。常见做法是每个矩形维护一个自增 z同时给当前选中的矩形临时加一个 z 偏移让选中时跳到最上层。但这个技巧要注意拖拽时没必要调整 z释放后再调否则连续拖拽会有层级跳变。想让画布更接近正式编辑器还可以给矩形加个轻微阴影。QGraphicsRectItem 没有直接阴影 API但可以通过 setGraphicsEffect 挂一个 QGraphicsDropShadowEffect代价是绘制性能降低图元较多时不要滥用。对于矩形数量几百以内的标注工具影响不大如果图元上千建议还是去掉阴影换取更流畅的交互。4.5 位置变化通知itemChange 钩子与业务联动矩形被用户拖到新位置后属性面板或业务模型应该跟着更新这就要用到 itemChange。ItemSendsGeometryChanges flag 打开后重写 QGraphicsRectItem::itemChange就能在图元位置变化被提交之前拿到新值#include QGraphicsRectItem class MyRectItem : public QGraphicsRectItem { public: using QGraphicsRectItem::QGraphicsRectItem; protected: QVariant itemChange(GraphicsItemChange change, const QVariant value) override { if (change ItemPositionChange scene()) { QPointF newPos value.toPointF(); emit positionChanged(newPos); // 自定义信号业务层监听 } return QGraphicsRectItem::itemChange(change, value); } signals: void positionChanged(const QPointF pos); };这里返回的 value 可以修改如果希望矩形只能出现在网格点上在 itemChange 里把 newPos 四舍五入到网格再返回即可。这是标注编辑器里很常用的实现拖拽时实时吸附网格而不是等 release 后才纠正坐标。要注意的是必须在 setFlags 里加上 ItemSendsGeometryChanges否则这个回调根本不会被触发。5. 绘制矩形的避坑指南五条踩坑记录与排查顺序5.1 坑一松开鼠标后矩形突然消失现象移动鼠标时能看到红色矩形跟着走松开后图元就没了场景里翻来翻去找不到。原因鼠标释放事件里没有把临时图元正确交给 scene。我见过最典型的错误创建临时矩形时用了栈对象QGraphicsRectItem temp(rect); scene-addItem(temp);变量是栈对象函数退出后析构矩形图元就被移出场景。还有另一种情况第二次绘制时没有清 m_currentItem旧图元失去引用看起来像消失了。解决一律用new QGraphicsRectItem创建图元通过 scene-addItem(item) 加入场景临时矩形转正后把成员指针置空图元由场景持有。判定某个矩形是否存在用 scene-items() 遍历不要凭指针非空判断。5.2 坑二反向拖拽矩形就跑偏现象从右下角往左上角拖矩形不出现或者矩形出现在鼠标的对称位置图形镜像。原因直接用 QRectF(startPoint, endPoint) 构造矩形当 endPoint 的 x、y 都小于 startPoint 时矩形宽和高是负数。负宽度矩形无法正常绘制setRect 后图元有时候显示不出来有时候显示成反向。解决QRectF(startPoint, endPoint).normalized()。normalized 会自动交换起终点把宽高修正为非负值这个函数在 QRectF 和 QRect 里都有。养成习惯之后基本可以告别反向拖拽问题。5.3 坑三视图缩放后矩形边框变粗现象视图放大到 200% 后原本 1 像素的细线变成 2 像素缩到 50% 后又细到看不清。原因创建 QPen 时写了QPen(Qt::red, 1)这是固定逻辑宽度的画笔会随 scene 的变换矩阵缩放。标注工具一般希望标注线始终和屏幕像素保持一致而不是和逻辑坐标保持一致。解决构造 QPen 时宽度传 0这是装饰性线条永远以屏幕像素为基准不受缩放影响。如果工具栏里希望允许用户调粗细应把“线宽”作为业务字段保存绘制时仍然写 0用额外的视觉元素模拟粗细而不是直接设置逻辑宽度。5.4 坑四高 DPI 屏幕上矩形位置偏移现象同一套代码普通屏画矩形正常换到 2x 缩放的笔记本上矩形跟鼠标错开一段距离差不多是一半尺寸。原因有人在坐标转换时手动除以 devicePixelRatiomapToScene(event-pos() / dpr)。Qt6 默认启用高 DPIviewport 坐标系里事件位置已经是设备像素而 QGraphicsView 的变换矩阵已经包含 DPR 补偿。再除一次坐标被人为压小一半图元自然偏到左上方。解决永远用 mapToScene(event-pos()) 获取场景坐标不要手动处理设备像素比。实在要排查高 DPI 相关问题用 qDebug 打印 event-pos() 和 mapToScene 的结果对比不要靠肉眼猜。5.5 坑五第一帧预览有残影或闪烁现象鼠标快速移动时矩形拖出几道残影或者整个视图闪白。原因分两种。残影常见于把临时矩形画在 viewport 的 overlay 上没有正确清理过期区域闪白常见于 setSceneRect 设置过大、视图默认背景是白色快速 update 时视口重绘来不及覆盖。解决按前文方案把临时矩形放进 scene 里由框架处理重绘残影基本消失。如果仍有闪烁给 viewport 设置Qt::WA_OpaquePaintEvent属性可以降低闪白概率。不要手动混用 update() 和 repaint()那会让刷新节奏更乱。5.6 排查顺序遇到问题时先做的三步画矩形报错时我一般按固定路径排查避免陷入瞎改参数的死循环。第一步确认事件没有被基类吞掉在 press、move、release 里加 qDebug打印事件位置和 m_drawing 状态看事件链是否完整。第二步确认新建矩形真正加入场景打印 scene()-items().count()release 后数量应该加 1。第三步确认坐标落在 scene 范围内如果 sceneRect 只建立在 (0,0,800,600)而鼠标初始位置不在这个范围内矩形会绘制但不可见。把坐标打印出来检查一遍大多数问题都能定位到具体环节。6. 再进一步撤销重做与键盘微调让画矩形工具真正可交付6.1 撤销重做用 QUndoStack 接管矩形增删业务上用户一定会误删矩形没有撤销功能的标注工具很难上线。Qt 自带的 QUndoStack 和 QUndoCommand 是好选择核心思路是把“添加矩形”和“删除矩形”封装成命令对象。给一个最小实现class AddRectCommand : public QUndoCommand { public: AddRectCommand(QGraphicsScene *scene, const QRectF rect, QUndoCommand *parent nullptr) : QUndoCommand(parent), m_scene(scene), m_rect(rect) { setText(QStringLiteral(添加矩形)); } void redo() override { m_item m_scene-addRect(m_rect, QPen(Qt::blue, 0)); m_item-setFlags(QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemIsMovable); } void undo() override { if (m_item m_item-scene()) { m_scene-removeItem(m_item); delete m_item; m_item nullptr; } } private: QGraphicsScene *m_scene; QRectF m_rect; QGraphicsRectItem *m_item nullptr; };一个常见错误是 redo 里创建图元、undo 时忘记删图元导致界面和命令栈不一致。每次撤销后场景图元数必须与命令栈维护的数据同步否则用户按 CtrlZ 时矩形还在画布上撤销就会显得失灵。压入命令时也有讲究// 在 CanvasView 的 rectCreated 信号处理里压入命令而不是直接 addItem void MainWindow::onRectCreated(const QRectF rect) { auto *cmd new AddRectCommand(m_scene, rect); m_undoStack-push(cmd); // push 会自动调用 redo() }m_undoStack 是主窗口里的 QUndoStack 实例绑定到菜单的 undoAction、redoAction 即可。同一时刻只能有一个 QUndoStack最好独立拿出来交给菜单管理。6.2 键盘微调与拖拽边界让手感再进一步撤销重做之外我用得最多的是方向键微调选中矩形后按上下左右每次移动 1 像素或网格整数倍。这里要考虑边界限制否则微调很容易把矩形拖到空白区域找不回来。bool MyRectItem::sceneEventFilter(QEvent *event) { if (event-type() ! QEvent::KeyPress) { return QGraphicsRectItem::sceneEventFilter(event); } auto *keyEvent static_castQKeyEvent*(event); qreal step keyEvent-modifiers() Qt::ShiftModifier ? 10.0 : 1.0; QPointF delta(0, 0); if (keyEvent-key() Qt::Key_Left) delta QPointF(-step, 0); else if (keyEvent-key() Qt::Key_Right) delta QPointF(step, 0); else if (keyEvent-key() Qt::Key_Up) delta QPointF(0, -step); else if (keyEvent-key() Qt::Key_Down) delta QPointF(0, step); else return QGraphicsRectItem::sceneEventFilter(event); moveBy(delta.x(), delta.y()); return true; }关键逻辑是 shift 键切换步长默认 1 像素按住 Shift 变成 10 像素网格对齐类工具一般也走这个模式。我在给某图像处理 Demo 做交互时第一次没限制边界结果用户按住右方向键不松手矩形一路飘出场景拖不回来。从那以后我每写一个可拖拽图元都会强制检查三件事KeyPress 是否被吞、moveBy 后是否过滤越界、撤销栈是否记录这次移动。希望这些细节习惯能帮你在自己的 Qt 绘图工具里少走弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑