资讯动态

Qt流程图绘制:QGraphicsView/Scene/Item三件套与连线避坑指南

发布时间:2026/10/21 22:33:20 来源:尧图企业网站定制
简介这是一份面向Qt开发初学者与图形界面方向学习者的流程图绘制实践资料围绕Qt图形视图框架中的图元与视图两大核心概念展开帮助读者理解如何用QGraphicsItem与QGraphicsScene、QGraphicsView搭建可交互的流程图编辑环境。资源包共36个文件以9个cpp源文件、8个h头文件为主配合12个png界面图标、1个qrc资源文件与1个pro工程文件另有若干bak备份文件整体约47KB结构紧凑可直接导入Qt Creator编译运行。内容涵盖自定义图元类、连接线绘制、图元拖动缩放旋转、鼠标事件重载以及流程图的保存与加载等模块适合对照源码理解图形项管理、事件处理与序列化思路。目前已有3999人学习下载可作为课程设计或小型绘图工具的参考起点。1. 从零拆解 Qt 流程图绘制图元与视图到底怎么配合很多人第一次做流程图编辑器都会掉进同一个坑把图元直接塞进场景然后发现拖动卡顿、连线错位、缩放后文字糊成一团。我见过一个模拟项目需求是画一张带分支的审批流结果开发同学用 QGraphicsItem 硬堆了三百多个节点鼠标一划就掉帧。问题不在 Qt在于没搞懂图元与视图的分工。图元负责“我长什么样、我能不能被拖”视图负责“我怎么把这一堆东西画到屏幕上、怎么把鼠标坐标翻译成场景坐标”。这份 Qt 流程图绘制资源核心就是围绕 QGraphicsView、QGraphicsScene、QGraphicsItem 三件套把节点、连线、锚点、缩放、序列化串成一条可复现的链路。适合已经会写 Qt 窗口、但一画图就乱的人也适合想从 MFC 或 Web 前端转过来做桌面图形编辑的从业者。下面按“先跑起来、再改得动、最后不翻车”的顺序拆。2. 图元与视图的职责划分为什么你的节点一多就卡2.1 三件套的边界Scene 是数据View 是相机Item 是演员Qt 的 Graphics View Framework 把图形系统拆成三层。QGraphicsScene 是数据容器它不关心窗口多大只维护图元的逻辑坐标和碰撞关系。QGraphicsView 是观察者它把场景映射到窗口负责滚动、缩放、坐标变换。QGraphicsItem 是场景里的实体每个节点、每条连线都是一个 Item自己决定绘制内容和交互响应。常见的新手错误是把业务数据直接存在 Item 的成员里然后又在 Scene 里维护一份列表。两边一改就不同步。我一般会这样分Item 只存“显示必需”的字段比如节点标题、端口位置、选中状态真正的业务数据放在一个独立的 Model 类里Item 通过指针或 ID 回查。这样序列化时只存 Model重建时再生成 Item逻辑干净很多。另一个边界是坐标。Item 的 pos() 是相对父 Item 的坐标scenePos() 才是场景绝对坐标。连线时如果混用缩放后就会飘。记住一条凡是跨 Item 计算一律先转 scenePos()再转目标 Item 的局部坐标。2.2 自定义图元的最小实现paint 与 boundingRect 必须成对写一个可拖动的节点图元最少要重写 boundingRect() 和 paint()。boundingRect() 告诉场景“我占多大”paint() 决定“我长什么样”。很多人只写 paint 不写 boundingRect结果点击区域和绘制区域对不上选中框乱飞。class FlowNodeItem : public QGraphicsItem { public: FlowNodeItem(const QString title, QGraphicsItem *parent nullptr) : QGraphicsItem(parent), m_title(title) { setFlags(ItemIsMovable | ItemIsSelectable | ItemSendsGeometryChanges); setAcceptHoverEvents(true); } QRectF boundingRect() const override { // 节点主体 120x60额外留 8px 给阴影和选中光晕 return QRectF(-60, -30, 120, 60).adjusted(-8, -8, 8, 8); } void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override { Q_UNUSED(option); Q_UNUSED(widget); painter-setRenderHint(QPainter::Antialiasing); // 主体圆角矩形 QRectF body(-60, -30, 120, 60); QLinearGradient grad(body.topLeft(), body.bottomLeft()); grad.setColorAt(0, isSelected() ? QColor(80, 140, 220) : QColor(240, 244, 250)); grad.setColorAt(1, isSelected() ? QColor(50, 100, 180) : QColor(220, 228, 240)); painter-setBrush(grad); painter-setPen(QPen(QColor(100, 120, 150), 1.5)); painter-drawRoundedRect(body, 8, 8); // 标题文字居中裁剪 painter-setPen(Qt::black); QFont f painter-font(); f.setPointSize(10); painter-setFont(f); painter-drawText(body, Qt::AlignCenter, m_title); } // 端口位置供连线使用 QPointF inputPort() const { return mapToScene(-60, 0); } QPointF outputPort() const { return mapToScene(60, 0); } private: QString m_title; };这段代码里boundingRect 比实际绘制区域大一圈是为了让选中光晕和阴影不被裁掉。ItemIsMovable 让节点能被拖动ItemSendsGeometryChanges 让位置变化时通知场景后面连线更新要用。inputPort 和 outputPort 用 mapToScene 把局部坐标转成场景坐标这样无论节点怎么移动、父级怎么变换连线端点都能算对。参数上节点尺寸 120x60 是经验值太小放不下中文标题太大一屏画不了几个。圆角 8px 在 100% 缩放下视觉舒服放大到 200% 也不会太夸张。渐变填充比纯色更容易区分选中态但别用太艳的颜色流程图是工作界面不是海报。2.3 视图的缩放与平移别让 Ctrl滚轮变成灾难QGraphicsView 默认支持滚动但缩放要自己接。常见做法是重写 wheelEvent按住 Ctrl 时按比例缩放否则交给基类滚动。void FlowView::wheelEvent(QWheelEvent *event) { if (event-modifiers() Qt::ControlModifier) { // 以鼠标位置为锚点缩放避免视图乱跳 const double factor event-angleDelta().y() 0 ? 1.15 : 1.0 / 1.15; const QPointF before mapToScene(event-position().toPoint()); scale(factor, factor); const QPointF after mapToScene(event-position().toPoint()); // 平移补偿让鼠标下的点保持不动 translate(after.x() - before.x(), after.y() - before.y()); event-accept(); } else { QGraphicsView::wheelEvent(event); } }这里的关键是“以鼠标为锚点”。如果直接 scale 不补偿缩放中心是视图左上角鼠标下的节点会跑掉。补偿逻辑是先记录缩放前鼠标对应的场景坐标缩放后再算一次差值平移回去。1.15 这个系数是手感调出来的太大一次跳太远太小滚半天没反应。还要注意缩放后文字会跟着放大如果不想让文字变糊可以在 paint 里根据视图的 transform 动态调整字体或者用 ItemIgnoresTransformations 标志让文字保持屏幕大小。后者简单但节点整体缩放时文字不缩视觉上会怪。我一般用前者在 paint 里取 painter-transform().m11() 算当前缩放比反算字体大小。3. 连线与锚点从“能画出来”到“拖不散”3.1 连线图元的设计贝塞尔曲线比直线更抗干扰节点之间的连线如果直接用直线两个节点靠近时线会重叠分支多的时候像蜘蛛网。常见做法是用三次贝塞尔曲线控制点根据端口方向偏移。class FlowEdgeItem : public QGraphicsItem { public: FlowEdgeItem(FlowNodeItem *from, FlowNodeItem *to, QGraphicsItem *parent nullptr) : QGraphicsItem(parent), m_from(from), m_to(to) { setZValue(-1); // 连线在节点下方 setFlag(ItemIsSelectable); m_from-addEdge(this); m_to-addEdge(this); } QRectF boundingRect() const override { return m_path.boundingRect().adjusted(-4, -4, 4, 4); } void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override { Q_UNUSED(option); Q_UNUSED(widget); updatePath(); painter-setRenderHint(QPainter::Antialiasing); QPen pen(isSelected() ? QColor(220, 80, 80) : QColor(120, 130, 150), 2); painter-setPen(pen); painter-setBrush(Qt::NoBrush); painter-drawPath(m_path); // 箭头 if (m_path.length() 1) { QPointF last m_path.pointAtPercent(1.0); QPointF prev m_path.pointAtPercent(0.95); drawArrow(painter, prev, last); } } void updatePath() { prepareGeometryChange(); QPointF p1 m_from-outputPort(); QPointF p2 m_to-inputPort(); double dx qAbs(p2.x() - p1.x()) * 0.5; QPointF c1(p1.x() dx, p1.y()); QPointF c2(p2.x() - dx, p2.y()); m_path QPainterPath(p1); m_path.cubicTo(c1, c2, p2); } private: FlowNodeItem *m_from; FlowNodeItem *m_to; QPainterPath m_path; };updatePath 里控制点的水平偏移取两点水平距离的一半这样曲线从输出端口水平出发到输入端口水平进入视觉上顺滑。如果两个节点垂直排列dx 会很小曲线接近直线也不会难看。prepareGeometryChange() 必须在修改 m_path 之前调用否则场景的索引不更新选中和碰撞会出错。箭头用 pointAtPercent 取曲线末端附近的两个点算方向比直接算切线简单。注意箭头大小要随缩放调整否则放大后箭头小得看不见。可以在 drawArrow 里根据 painter 的 transform 缩放箭头尺寸。3.2 节点移动时连线怎么跟ItemChange 与场景更新节点拖动时连线必须实时更新。最直接的办法是在 FlowNodeItem 的 itemChange 里通知所有关联的边。QVariant FlowNodeItem::itemChange(GraphicsItemChange change, const QVariant value) { if (change ItemPositionHasChanged) { for (FlowEdgeItem *edge : m_edges) { edge-updatePath(); edge-update(); } } return QGraphicsItem::itemChange(change, value); }这里用 ItemPositionHasChanged 而不是 ItemPositionChange是因为前者在位置已经生效后触发此时端口坐标才是新的。如果边很多每拖一下都全量更新会卡可以加一个定时器做节流或者只更新与当前节点关联的边。我一般会在节点里存一个 QListFlowEdgeItem*添加边时注册删除时反注册。另一个坑是删除节点时边没删。常见做法是在 FlowNodeItem 的析构里遍历 m_edges把边从场景移除并 delete。但要注意边可能同时引用两个节点析构顺序不对会二次释放。稳妥的做法是让 Scene 统一管理删除或者用 QPointer 弱引用。3.3 锚点吸附让连线“自动找端口”如果每个节点只有一个输入输出端口固定还好。但审批流节点可能有多个分支需要多个输出锚点。常见做法是在节点上画小圆点作为锚点鼠标靠近时高亮松开时吸附。void FlowScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if (m_drawingEdge) { m_tempEdge-setEndPoint(event-scenePos()); // 检测鼠标下是否有锚点 FlowNodeItem *hover nodeAt(event-scenePos()); if (hover) { QPointF port hover-nearestPort(event-scenePos()); m_tempEdge-setEndPoint(port); hover-highlightPort(port); } } QGraphicsScene::mouseMoveEvent(event); }nearestPort 遍历节点的所有端口算距离最小的那个超过阈值就返回空。阈值一般取 20px 场景坐标太小不好吸太大容易误吸。高亮用端口 Item 的 setVisible 或改颜色别用动画拖拽时动画会拖慢响应。4. 避坑与排查流程图编辑器最容易翻车的五个地方4.1 缩放后鼠标点击错位现象视图放大到 200%点击节点没反应或者点 A 选中 B。原因在自定义 Item 的 mousePressEvent 里用了 event-pos() 当场景坐标。event-pos() 是 Item 局部坐标缩放后和场景坐标差一个 transform。解决一律用 event-scenePos() 做命中判断需要局部坐标时用 mapFromScene 转。4.2 连线在节点下方被遮挡现象连线画出来了但被节点盖住看起来像断的。原因QGraphicsItem 默认 Z 值都是 0后添加的在上层。节点通常后添加就盖住了先添加的边。解决在边构造函数里 setZValue(-1)节点保持 0 或正数。如果有多层用 Z 值分层背景 -10边 -1节点 0选中框 10。4.3 删除节点后程序崩溃现象选中节点按 Delete界面卡死或直接退出。原因节点析构时边还在引用它边更新时访问了已释放的指针。解决删除节点前先遍历关联边从场景 removeItem 并 delete再把节点从 m_edges 列表清空。或者用 QPointer 包一层访问前判断是否为空。4.4 大量节点时拖动卡顿现象节点超过 100 个拖动一个节点整个视图掉帧。原因每次 itemChange 都触发全场景重绘或者 boundingRect 返回过大导致无效重绘区域太大。解决给节点设置 ItemUsesExtendedStyleOption让 Qt 只重绘脏区域连线更新用节流比如 16ms 合并一次关闭不必要的抗锯齿只在静止时开。4.5 序列化后重建连线全乱现象保存成 JSON 再打开节点位置对了但连线连到了错误的端口。原因保存时只存了节点 ID没存端口索引重建时按默认端口连多输出节点就错位。解决序列化时每条边存 fromNodeId、fromPortIndex、toNodeId、toPortIndex。端口索引在节点创建时按固定顺序分配不要依赖 QList 的遍历顺序。5. 进阶技巧用 QGraphicsItem 的 ItemIgnoresTransformations 做固定大小标签流程图画到后面总会遇到一个需求节点可以缩放但节点上的文字或小图标不想跟着缩。比如一个“审批中”的状态角标放大后如果跟着变大会挡住节点内容。Qt 提供了 ItemIgnoresTransformations 标志让 Item 忽略视图的缩放变换始终保持屏幕大小。做法是把标签做成独立的 QGraphicsItem设上这个标志然后作为节点的子 Item。子 Item 的坐标仍然受父 Item 影响但绘制时不受视图缩放影响。class StatusBadgeItem : public QGraphicsItem { public: StatusBadgeItem(const QString text, QGraphicsItem *parent nullptr) : QGraphicsItem(parent), m_text(text) { setFlag(ItemIgnoresTransformations); setPos(50, -25); // 相对父节点右上角 } QRectF boundingRect() const override { return QRectF(0, 0, 40, 18); } void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override { Q_UNUSED(option); Q_UNUSED(widget); painter-setRenderHint(QPainter::Antialiasing); painter-setBrush(QColor(255, 180, 60)); painter-setPen(Qt::NoPen); painter-drawRoundedRect(boundingRect(), 4, 4); painter-setPen(Qt::black); QFont f painter-font(); f.setPointSize(8); painter-setFont(f); painter-drawText(boundingRect(), Qt::AlignCenter, m_text); } private: QString m_text; };这里的关键是 setFlag(ItemIgnoresTransformations) 和 setPos 的配合。setPos(50, -25) 是相对父节点的局部坐标父节点移动时角标跟着走但视图缩放时角标大小不变。boundingRect 返回的是屏幕像素尺寸不是场景单位所以 40x18 在 100% 和 400% 缩放下看起来一样大。验证方法很简单把视图放大到 300%角标应该还是原来的大小而节点主体已经变大。如果角标也跟着变大检查是否漏了 setFlag或者父 Item 是否也设了忽略变换导致坐标计算混乱。我一般会在角标上再加一个 QGraphicsDropShadowEffect但注意特效在 ItemIgnoresTransformations 下可能渲染异常实测 Qt 5.15 之后才稳定。如果发现阴影偏移直接去掉特效用两层圆角矩形模拟。从那以后我每次做流程图编辑器都会先把图元、视图、场景的坐标转换写成一个工具类所有跨 Item 计算强制走这个类不再手写 mapToScene。这个习惯帮我省掉了至少三次“缩放后连线飘走”的返工。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑