简介这份压缩包是一份基于Qt框架开发的流程图编辑器课程设计面向需要完成图形界面大作业的C学习者。资源共51个文件核心源码包括6个cpp、5个h头文件、1个ui界面文件配合37个png图片素材与qrc资源管理配置整体仅149KB结构紧凑便于阅读。项目中通过QPainter完成节点和连线的绘制使用QGraphicsScene管理场景并借助鼠标事件实现图形元素的选中、移动、删除还包含图片资源加载、线条颜色调整和文本标注等功能。读者可以从中看到自定义图元Item、箭头连线随动更新、界面布局与资源文件组织方式也可以直接对照源码梳理绘图事件、信号槽连接和序列化存储思路。已有1949人学习浏览该资源适合作为理解Qt图形视图框架、流程图交互逻辑及工程目录组织的参考实现。1. 这套 Qt 流程图编辑器大作业从画布到序列化给 C 课设一份能直接跑的底稿如果你正在做 Qt 流程图编辑器课设最怕的不是画几个框连几根线而是交上去的代码经不起老师问“节点数据怎么存”“撤销怎么做”“缩放之后连线为什么错位”。这套基于 Qt 5.15.2 C 的流程图编辑器工程把基本流程图功能都补齐了节点拖拽、连线、选中删除、缩放平移、属性编辑、XML 工程文件保存与读取。我自己拆完源码后发现它最大的价值不是某个炫技功能而是给你一条完整的主线——从 QGraphicsScene 坐标系到自定义序列化协议每一步都能在代码里找到落点适合拿来改造成自己的课设框架而不是推倒重写。新手可以直接照着跑通全流程老手也能在工程结构、编码习惯上省不少时间。2. 编辑器核心架构为什么选 QGraphicsView 体系而不是自己画控件拿到这套工程第一件事不是点运行而是先看它的顶层结构。流程图编辑器难在“用鼠标操作图形”和“数据要持久化”这两件事是互相拉扯的屏幕上一个矩形框只是视觉元素但它背后的数据模型要能被保存、复制、重排甚至未来做自动布局。这套工程用的是 Qt 官方推荐的 QGraphicsScene QGraphicsView QGraphicsItem 三件套架构而不是自己用 QWidget 堆矩形和 QPainter 画连线。2.1 三件套的分工与坐标系换算逻辑QGraphicsScene 是数据容器QGraphicsView 是视口QGraphicsItem 是图元。我拆代码时最在意的是一点这个工程把“场景坐标”和“视图坐标”分得清楚。拖拽节点时鼠标事件从 view 进入换算成 scene 坐标再作用到 item 上缩放的时候只动 view 的 transform不动 scene 里的坐标数据。// 鼠标拖拽节点到新位置的逻辑简化自工程内实现 void FlowChartView::mouseMoveEvent(QMouseEvent *event) { if (!m_pressingItem) return; // 把视图坐标换算到场景坐标这一步不能省 QPointF scenePos mapToScene(event-pos()); // 用差值移动节点而不是直接把节点设置为 scenePos QPointF delta scenePos - m_lastScenePos; m_pressingItem-moveBy(delta.x(), delta.y()); m_lastScenePos scenePos; // 连线随节点移动刷新否则会出现“线头脱离节点”的经典翻车 if (m_currentLine) { m_currentLine-updatePath(); } }这里有一个很多新手会写错的点直接用item-setPos(scenePos)会把节点“吸”到鼠标位置导致拖拽时节点中心突然跳到鼠标点手感全无。工程里用的是moveBy(delta)每次只增加位移差这样不管鼠标点在节点的哪个角落相对位置都不变。m_lastScenePos要放在mousePressEvent里初始化并且第一次按下时就应该记录场景坐标而不是视图坐标。2.2 节点与连线的图元继承设计这套工程里所有节点都是QGraphicsItem的直接子类但连线单独拆了一个LinkItem。看代码你会发现连线的boundingRect()返回的是整条路径的包围盒而不是单个点。这个细节决定了“点击选中连线”的命中范围。如果直接把 boundingRect 写成线宽用户极难用鼠标点中它尤其是对角线。// 连线图元的关键参数摘取自 LinkItem 的 paint 与 shape QRectF LinkItem::boundingRect() const { // 必须加上线宽和边距否则抗锯齿绘制会溢出边界 return m_path.boundingRect().adjusted(-4, -4, 4, 4); } QPainterPath LinkItem::shape() const { // 用描边路径生成一个加宽的命中区域点线面积从 1px 扩大到 12px QPainterPathStroker stroker; stroker.setWidth(12); return stroker.createStroke(m_path); }shape()这个重写是识别度的关键。默认情况下 QGraphicsItem 的命中检测是基于boundingRect()的但它是个矩形你点到线的“空白角落”也会触发选中。工程里用QPainterPathStroker把路径加粗生成真实命中形状这样只有点到线附近 6px 范围内才算选中。这个技巧在你自己加连线类型时同样适用比如加一条虚线或贝塞尔曲线也要重新实现shape()否则看着是曲线点起来却是矩形框很别扭。另外这套工程里的连线是独立的LinkItem没有挂在某个节点内部所以移走节点时两条连线不会跟着动工程在updatePath()里做了重算而不是靠 item 的父子关系联动。2.3 工程文件结构与编译前置条件工程文件用的是 qmake 的.pro组织方式不是 CMake。如果你导师要求 CMake你需要手动重写构建脚本。解开压缩包后第一件事看main.cpp和mainwindow.cpp的包含关系——它依赖 Qt Widgets 模块和 Qt Xml 模块如果你要用 XML 序列化。在.pro里必须有QT core gui widgets xml CONFIG c11 TARGET FlowChartEditor TEMPLATE app SOURCES main.cpp mainwindow.cpp scene.cpp view.cpp nodeitem.cpp linkitem.cpp HEADERS mainwindow.h scene.h view.h nodeitem.h linkitem.h用 Qt 5.15.2 MSVC2019 打开.pro直接能跑。如果打开后编译报“无法打开 QtWidgets/QApplication”先检查 Kit 选择的是不是 MSVC2019_64再看QT widgets是否被注释掉。这套工程没有引入第三方库全部是 Qt 官方模块编译成功率高的原因就在这——凡是纯 Qt 模块的大作业最怕作者贪方便直接移植网上整合包带了一堆乱七八糟的依赖。3. 交互功能这样实现拖拽、缩放、对齐与连线的关键 API 参数流程图编辑器体验好坏不在于是不是能画图而在于是不是画得顺手。这套工程实现了完整的鼠标交互链按住左键拖出连线、右键菜单删除、Ctrl滚轮缩放、框选多节点后对齐。这里把每个交互背后的 API 参数说透你答辩时被问到“缩放怎么实现的”就能直接答出来。3.1 视图缩放如何设置锚点保证鼠标中心不跑偏Zoom 功能是流程图编辑器最容易出现“缩放后画面跑到看不见的地方”的翻车环节。工程里的实现是用QGraphicsView::setTransform叠加缩放系数并且在缩放前把视图中心设为鼠标所在位置这样缩放时鼠标指着的那个图形不会飘走。void FlowChartView::wheelEvent(QWheelEvent *event) { // 设置缩放锚点为鼠标当前坐标否则缩放时以视图中心为锚点画面会跳到另一边 setTransformationAnchor(QGraphicsView::AnchorUnderMouse); qreal scaleFactor 1.15; if (event-angleDelta().y() 0) { scale(scaleFactor, scaleFactor); } else { scale(1.0 / scaleFactor, 1.0 / scaleFactor); } }AnchorUnderMouse是个枚举值默认是AnchorViewCenter。很多程设模板没设置这个值导致滚轮一滚图形全飘走。另外还要注意缩放上下限否则用户无限制放大下去transform里的 m11 值会溢出。比较稳妥的做法是维护一个m_scaleValue每次缩放后判断是否在 0.2 到 3.0 之间超出范围直接 return。这套工程里没有做上限老师如果故意疯狂滚轮可能会出现图形抖动建议你自己补上这段防御代码。3.2 连线创建的模式切换与命中检测参数连线的交互逻辑是鼠标悬停在节点边缘的插座上时光标变成十字形按住左键拖出到另一个节点上松开则创建一条连接如果松开在空白区域则取消连线。这里有个关键技术点叫“进入连接模式”鼠标按下时就要进入该模式而不是先移动再判断。void NodeItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { // 判断是否命中边缘 8px 热区进入连线模式 QRectF edgeRect boundingRect().adjusted(-8, -8, 8, 8); if (edgeRect.contains(event-pos()) !boundingRect().contains(event-pos())) { m_scene-startLinking(this, mapToScene(event-pos())); event-accept(); } else { // 普通拖拽逻辑 QGraphicsItem::mousePressEvent(event); } }注意mapToScene这步转换如果漏掉它连线的起点就是节点局部坐标画出来直接偏掉。工程里还有一个细节值得学习连线目标的判断放在scene层而不是view层。在mouseMoveEvent里通过scene-items(scenePos)拿到鼠标下的所有 item再用qgraphicsitem_cast判断它是不是NodeItem。这里有一个性能相关的隐性坑如果场景里 item 数量极大超过几百个每次都调用items()做全局命中检测会有明显卡顿。常见的优化是维护一个“插座点列表”只对每个节点的插座点做距离判断避免全场景碰撞检测。工程里图元数量少直接用默认方案没问题但答辩时如果能说出这个优化思路印象分差别很大。3.3 框选与对齐Qt 内置 rubber band 还是自己画虚线框这套工程支持 Ctrl拖拽框选选中的节点周围出现高亮边框。我看源码时注意到框选用的是QGraphicsView::setDragMode(QGraphicsView::RubberBandDrag)没有自己画虚线框这是最省事也最稳的方案。但是 RubberBandDrag 有个局限它只能用来选中不能做框选后的批量操作。// 在 MainWindow 中绑定对齐快捷键 connect(shortcutAlignLeft, QShortcut::activated, this, [this]() { QListQGraphicsItem* selected m_scene-selectedItems(); if (selected.size() 2) return; // 按所有选中节点中最小的 x 坐标作为对齐线 qreal minX std::numeric_limitsqreal::max(); for (QGraphicsItem *item : selected) { minX qMin(minX, item-sceneBoundingRect().left()); } for (QGraphicsItem *item : selected) { item-setPos(minX, item-sceneBoundingRect().top()); } });这里有一个逻辑陷阱setPos是设置图元本地坐标原点而不是它的包围盒左上角。如果你直接item-setPos(minX, item-pos().y())对齐之后的视觉效果是每个节点的左上角 x 相同但节点图标内部如果有边距看起来就没对齐。更精细的做法是用sceneBoundingRect()而不是boundingRect()因为前者已经包含了transform缩放和旋转的影响。流程图编辑器里节点的缩放一般不做但如果你允许用户缩放节点就要全换sceneBoundingRect否则连线就会错位。4. 序列化与撤销重做这两个模块直接决定课设答辩的深度分流程图编辑器如果只能画不能存那等于白做。这套工程用 XML 格式保存工程文件节点坐标、尺寸、颜色、文本内容、连线两端节点 ID 全部写进 XML。这一个模块覆盖了 Qt 的QXmlStreamWriter和QXmlStreamReader两个类也是你答辩时最能展开讲的部分。4.1 XML 序列化的数据模型设计看工程里saveToXml的实现会发现它给每个节点分配了一个nodeId连线不直接存坐标而是存startNodeId和endNodeId坐标在加载时重新计算。这个设计非常好——如果直接存连线两个端点的像素坐标加载后节点一移动连线就留在原地变成断线。// 节点保存的核心代码 void Scene::saveToXml(const QString fileName) { QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) return; QXmlStreamWriter xml(file); xml.setAutoFormatting(true); xml.writeStartDocument(); xml.writeStartElement(FlowChart); xml.writeAttribute(version, 1.0); foreach (QGraphicsItem *item, items()) { NodeItem *node qgraphicsitem_castNodeItem *(item); if (!node) continue; xml.writeStartElement(Node); xml.writeAttribute(id, QString::number(node-nodeId())); xml.writeAttribute(x, QString::number(node-pos().x())); xml.writeAttribute(y, QString::number(node-pos().y())); xml.writeAttribute(width, QString::number(node-nodeWidth())); xml.writeAttribute(height, QString::number(node-nodeHeight())); xml.writeTextElement(Text, node-nodeText()); xml.writeEndElement(); } // 连线遍历类似用 startId/endId 写两端引用 xml.writeEndElement(); xml.writeEndDocument(); file.close(); }这里的xml.writeAttribute和xml.writeTextElement的区别要能讲清楚属性适合存数字和短字符串子元素适合存可能有换行或特殊字符的多行文本。节点文本里如果有或写入属性中 Qt 会自动转义读取时也会自动反转义不会破坏 XML 结构。你答辩时如果能主动说一句“XML 的转义由 QXmlStreamWriter 自动处理不需要手动替换特殊字符”老师会觉得你真的跑过一遍而不是纯背代码。4.2 加载时为什么必须先建节点再连连线加载 XML 的反序列化逻辑有个顺序讲究第一遍读所有节点、创建图元并加入 scene第二遍读连线通过节点 ID 查找到对应图元。如果指望 XML 里连线的startNodeId恰好小于endNodeId或者按文件顺序先遇到连线再遇到节点就会出现空指针崩溃。// 恢复连线的核心逻辑注意要按 ID 映射到图元指针 void Scene::loadFromXml(const QXmlStreamReader xml) { QMapint, NodeItem * nodeMap; while (!xml.atEnd()) { xml.readNext(); if (xml.isStartElement() xml.name() Node) { int id xml.attributes().value(id).toInt(); NodeItem *node createNode( xml.attributes().value(x).toDouble(), xml.attributes().value(y).toDouble() ); node-setNodeId(id); node-setNodeText(xml.readElementText()); addItem(node); nodeMap.insert(id, node); } } // 第二次遍历连线此时 nodeMap 已完整 while (!xml.atEnd()) { xml.readNext(); if (xml.isStartElement() xml.name() Link) { int startId xml.attributes().value(startId).toInt(); int endId xml.attributes().value(endId).toInt(); if (nodeMap.contains(startId) nodeMap.contains(endId)) { createLink(nodeMap[startId], nodeMap[endId]); } } } }加载时另一个常见问题是readElementText()的调用位置如果你在一个 startElement 里先取了属性然后调readNext()继续往下读再调用readElementText()返回的是空字符串或错误文本。readElementText()必须紧跟在读完 startElement 之后调用它会把当前元素内的所有字符拼起来。工程代码里刻意先保存了x、y属性最后才调用readElementText()这个顺序就是为兼容这个 API 的限制。4.3 撤销重做用命令模式还是简单快照这套工程里的撤销重做做的是“快照式”——每次操作后把 scene 里的所有节点坐标、文本、连线关系存到一个QByteArray里撤销时直接恢复上一个快照。代码量最小但对大场景不友好快照多了内存会炸。// 快照式撤销的核心实现序列化到内存而不是文件 void Scene::saveSnapshot() { QBuffer buffer(m_snapshotBuffer); buffer.open(QIODevice::WriteOnly); QDataStream out(buffer); // 把所有节点数据写到流中 foreach (QGraphicsItem *item, items()) { NodeItem *node qgraphicsitem_castNodeItem *(item); if (!node) continue; out node-nodeId() node-pos() node-nodeText(); } m_undoStack.push(m_snapshotBuffer); }快照式撤销存在两个问题。第一是内存膨胀每操作一次就全量存一份场景下限 100 个节点、上限几百个历史记录时内存占用很可观。第二是连线关系没有序列化进快照撤销节点移动后连线会飘因为连线坐标是实时更新的但快照恢复时没有重算连线路径。如果你要用这套工程答辩最好把快照改成命令模式为“移动节点”和“删除节点”各写一个QUndoCommand子类。如果没时间改至少保证快照只保存节点数据撤销后对 scene 里残留的 LinkItem 做一次updatePath()刷新。5. 避坑实录Qt 流程图编辑器最常见的四个崩溃级错误qt 崩溃是个高频检索词这里把我在复现这套工程时踩过的坑集中写出来。这些坑不是玄学每一个都可以从 Qt 文档里找到解释但先踩了再看文档印象会深得多。5.1 现象关闭程序时崩溃报错 QGraphicsScene destroyed while items are still in it原因窗口关闭时 QGraphicsScene 先被销毁但 scene 里的 QGraphicsItem 还在item 析构时尝试访问 scene 已经不可用。解决在MainWindow的析构函数里先调用scene-clear()把 item 都删干净再让 scene 自己析构。很多人不是没写clear()而是写了但顺序在delete scene之后等于白写。MainWindow::~MainWindow() { // 先清空场景中的所有图元再销毁 scene 对象 if (m_scene) { m_scene-clear(); // 这里会 delete 所有 item delete m_scene; // 此时 scene 内没有剩余 item才安全 } }5.2 现象按下删除键后节点消失了但连线还留在画布上原因删除节点时只调用了scene-removeItem(node)但removeItem只把 item 从场景中摘除不会 delete也不处理与之关联的连线。解决手动遍历scene-items()找到所有LinkItem判断其 start 或 end 是否指向被删节点先删连线再删节点。注意delete node之后不能再访问该指针所以要在删节点前收集所有相关连线。void Scene::deleteSelectedItems() { QListQGraphicsItem * toDelete; foreach (QGraphicsItem *item, selectedItems()) { if (NodeItem *node qgraphicsitem_castNodeItem *(item)) { // 找出所有连接到该节点的连线 foreach (QGraphicsItem *other, items()) { if (LinkItem *link qgraphicsitem_castLinkItem *(other)) { if (link-startNode() node || link-endNode() node) { toDelete link; } } } toDelete node; } } qDeleteAll(toDelete); }5.3 现象加载工程文件后节点位置全对但连线跑到了左上角原点原因加载时节点和连线都加到了 scene但连线路径是通过startNode-pos()计算的。如果加载流程是先加连线再加节点那么连线计算时节点坐标还是默认的 (0,0)。解决严格按“先节点后连线”的顺序加载或在创建连线后手动调一次updatePath()。这题就是 4.2 节说的加载顺序问题在这里再次强调因为它确实高频。5.4 现象在高 DPI 屏幕上运行鼠标点选节点总偏差 2-3 像素原因Qt 5.15 对高分屏缩放的支持还在磨合期如果你没有在main.cpp里设置QApplication::setAttribute(Qt::AA_EnableHighDpiScaling)在 125%/150% 缩放的屏幕上view 的坐标映射会有偏差。解决在创建QApplication之后立即加一行QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication app(argc, argv);这行必须在QApplication构造之前调用否则不生效。Qt 6 里这个属性默认开启但如果你的课设环境是 Qt 5.15不加就是会有偏差。6. 发布与演示进阶如何让流程图编辑器在别的机器上跑起来以及动态属性扩展收尾聊一个最实际的话题课设要交 Demo怎么把工程打包成能在导师电脑上双击运行的 exe。很多人把 debug 目录下的 exe 直接拷过去到别人机器上双击没反应或者报qt.qpa.plugin: could not find the Qt platform plugin windows。这个问题你搜一下就知道原因很简单缺少 platform 插件目录。6.1 打包发布windeployqt 是唯一正规手段在你的 Qt 安装目录下找到windeployqt.exe路径一般是C:\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe。把编译生成的FlowChartEditor.exe放到一个单独的文件夹然后在该文件夹打开命令行执行C:\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe FlowChartEditor.exe它会自动把需要的 Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll 以及 platforms 目录下的 qwindows.dll 复制到 exe 旁边。执行完之后文件夹里会多出几十个文件整个文件夹拷走就能在别的 Windows 机器上运行。如果你用了 Qt Xml 模块记得检查有没有 Qt5Xml.dll 被拷进来windeployqt有时候会漏掉非默认模块。最简单的验证办法把文件夹拷到一台没装 Qt 的虚拟机里跑一遍。# 验证发布文件夹是否完整dll 依赖一键检查 dumpbin /dependents FlowChartEditor.exe如果缺失依赖输出里会列出找不到的 dll 名字。dumpbin是 VS 自带工具需要在 VS 开发人员命令行里执行。工程里如果引用了 Qt5Xml而发布文件夹里没有程序会在启动时静默崩溃而不报错这是最难受的坑。6.2 扩展一个动态属性面板用 Q_PROPERTY 代替硬编码字段答辩时老师大概率会问“如果我想给节点加一个颜色属性怎么改”。如果代码里写死了nodeText和nodeId扩展一个属性就要改五六个文件。工程的分层其实已经为你预留了扩展空间节点类继承自 QGraphicsObject而非 QGraphicsItem说明它已经支持 Qt 元对象系统。给节点增加属性最优雅的方式是往 XML 里加一个自定义子元素同时注册一个Q_PROPERTY// 在 NodeItem 头文件中声明颜色属性 class NodeItem : public QGraphicsObject { Q_OBJECT Q_PROPERTY(QColor fillColor READ fillColor WRITE setFillColor) public: QColor fillColor() const { return m_fillColor; } void setFillColor(const QColor color) { m_fillColor color; update(); // 触发重绘颜色立即生效 } private: QColor m_fillColor Qt::white; };有了Q_PROPERTY后属性浏览器或样式表就能直接读写这个属性不需要专门写 setter 的调用逻辑。保存到 XML 时加一行xml.writeAttribute(fillColor, m_fillColor.name())加载时用QColor(xml.attributes().value(fillColor).toString())读回来。整个过程不碰 Scene 和 MainWindow 的代码新属性从添加到底层到 UI 完全解耦。6.3 验证自己的工程把整套流程手动跑一遍你拿到这张工程后我建议按下面路径先完整验证一遍确认没漏件再开始改新建工程文件并拖入两个节点、一条连线保存为.flow.xml关闭程序重新打开加载刚才的文件确认节点坐标、文本和连线完全还原框选一个节点并尝试删除确认连线随节点一并被清除按住 Ctrl 滚轮缩放确认鼠标指向的图元没有飘走把发布文件夹拷到另一台电脑双击 exe 确认能正常启动。五个步骤如果全部通过这套工程在你机器上已经是稳妥状态剩下的才是你自己的扩展功课。说句实在话流程图编辑器这类课设之所以叫大作业就是因为“能跑”和“能演示完整”之间隔着一条交互细节的鸿沟。这套工程的价值在于帮你跨过那道鸿沟——你不用花三天把画布底层磨平而是拿到一个已经能跑通的骨架把省下来的时间全部投入到答辩亮点上。希望这篇笔记能帮你少踩几个坑把那台电脑上的流程图编辑器稳稳跑起来。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?