尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

基于Qt与QGraphicsView打造轻量级可视化仿真界面:从模块拖拽到实时波形显示

发布时间:2026/9/9 21:59:21

资讯中心
01
ARTICLE

基于Qt与QGraphicsView打造轻量级可视化仿真界面:从模块拖拽到实时波形显示

基于Qt与QGraphicsView打造轻量级可视化仿真界面:从模块拖拽到实时波形显示
简介基于Qt与C实现的类Simulink仿真界面小程序源码包面向希望学习Qt图形界面开发与数字仿真建模的开发者可熟悉可视化建模工具的基本设计思路。源码在VS2010与Qt 4.8.5环境下构建借助QGraphicsView框架实现模块拖放、连线编辑与动态图表刷新涉及Item/View/Scene架构、闭环控制回路绘制、微分方程离散求解等关键知识点。压缩包共122个文件约21.78MB主要包含cpp源文件、h头文件、ui界面描述、obj编译中间文件以及png/jpg图标、可直接运行的exe程序与项目配置sln/vcxproj方便使用者阅读代码并重新编译调试。目前已有3627人学习浏览。通过对照源码与可执行程序可以直观理解Qt图元设计、视图交互和实时仿真数据更新的实现细节项目目录中源码、界面文件与工程配置层次清楚对后续扩展仿真模块或开发类似工业组态工具具有实际参考价值。 如果你做过上位机开发或者嵌入式仿真验证大概率有过这种体会算法逻辑本身不复杂但想找一个既轻量又能自由定制的“可视化仿真界面”并不容易。Matlab/Simulink功能强可授权成本和部署规模摆在那自己从头画界面又容易陷进控件排布和绘图细节里出不来。去年我因为一个控制算法演示项目用Qt做了个类似Simulink仿真界面的小程序把模块拖拽、连线、参数配置、实时波形显示整条链路跑通之后发现这件事远比想象中顺手而且能给后续开发省下大量验证时间。这个项目本质上是一个“缩小版Simulink”左侧是模块库中间是画布右侧是属性面板底部是波形显示区。支持常数源、增益、一阶滤波/惯性环节、PID控制器、示波器显示、数据导出等模块模块之间通过拖拽连线传递数据。点击运行后按固定步长迭代计算并把结果实时绘制成曲线。如果你是Qt开发者、做嵌入式上位机、或者想基于Qt做一套自己的仿真工具链这篇文章里的架构思路和踩坑记录可以直接拿来用。1. 这个工具能做什么以及为什么选择Qt来做1.1 功能拆解一个缩小版Simulink的完整闭环这套小程序的核心功能可以拆成四块模型搭建、参数配置、仿真计算、结果展示。模型搭建指用户从左侧模块库拖出模块放到画布模块之间用连线表示数据流向参数配置是点击某个模块后右侧属性面板显示对应的可调项比如一阶滤波的时间常数T、增益模块的放大倍数K修改后实时生效仿真计算是按固定采样步长执行整张模型图每一步更新所有模块的输出结果展示则是在底部波形区用QCustomPlot把数据画出来。听起来不复杂但把四块串起来后你会发现它已经覆盖了Simulink最常用的交互闭环。我们的实际用途是用来做信号处理演示输入一个带噪声的正弦波经过一阶低通滤波再输出滤波前后的对比曲线。这个场景在控制类课程、算法验证、甚至产品demo里都很有代表性。项目从零到能演示完整流程只花了一个多星期主要时间都花在交互层和数据模型上。1.2 选型理由为什么不用Web前端也不用纯QWidget堆控件可能有朋友会问现在Web端拖拽库那么多为什么偏要选Qt原因是目标运行环境是工业现场的上位机客户机器上没有浏览器环境反倒一定有Windows或Linux桌面系统。Qt在这个场景下有几个天然优势一是QGraphicsView框架本身就是为“大量图元 自由交互”设计的拖拽、框选、缩放、连线这些操作手写起来很直接二是信号槽机制非常适合仿真数据流模块A输出更新后直接emit信号模块B的槽函数收到后开始计算逻辑上完全映射数据流关系三是QCustomPlot做实时曲线几乎是零成本接入比在Web端调ECharts或Canvas省事得多。再说为什么不直接用纯QWidget。刚开始我也觉得用QWidget放一堆自定义控件就能凑合但QWidget的坐标系统、子控件刷新策略在频繁拖拽和动态连线时会非常吃力。一个模块拖到另一个位置所有连线要跟着更新QWidget需要手动管理大量update()而QGraphicsView里图元自带局部刷新机制性能差距非常明显。所以架构上一开始就定了画布部分走QGraphicsView周边面板用普通QWidget两边通过信号槽解耦。2. 先定数据模型模块、端口和连线的底层设计2.1 模块、端口、连线三要素的类结构设计自研可视化流程图工具最容易犯的错误是一上来就写界面代码结果画布画到一半发现数据结构撑不住交互。我这个项目里最先落地的不是界面而是三个基础类Module、Port、Connection。Module代表一个仿真模块内部有模块类型、实例名称、参数表QMapQString, QVariant和输入输出端口列表Port代表端口记录所属模块、端口方向输入/输出和数据类型Connection代表一条连线保存源端口和目标端口的指针以及这条线在场景中的图元指针。这里有个关键设计连线不直接持有数据而是持有端口指针。每次仿真迭代时从源Port读取上一拍输出写入目标Port的接收缓冲区。这样做的好处是模块本身不需要关心“我的输入是谁给的”只要在计算前从自己的输入端口缓冲区取值即可。模块之间彻底解耦新增模块类型时不需要改任何连线逻辑。数据类型用QVariant统一包装。数值、波形数组、布尔值都能塞进去端口在连接时检查类型是否匹配。实测下来QVariant做通用数据容器虽然有一定转换开销但在这个量级的仿真场景下完全可接受换来的是扩展性大幅提升——后来加一个“字符串显示”模块只用了半天。2.2 拓扑排序保证每个模块按正确的顺序执行拖拽模型搭出来的图执行顺序不能靠用户手工排列否则连线稍有调整就可能算出错误结果。这里必须用有向图的拓扑排序。每次点击运行按钮时先把所有连线的源端口和目标端口映射成有向边对整张图做一次Kahn拓扑排序得到一个模块执行列表。算法本身很经典统计每个模块的入度入度为0的模块可以执行执行完后把所有以它为起点的边删除更新相邻模块入度重复直到所有模块处理完。如果最后还有模块没进执行列表说明图中存在环这时候需要弹窗提示用户“模型存在依赖回路”并且定位到环上的模块名。我在仿真里暂时只支持前馈结构遇到环直接拒绝运行这个策略简单且务实。伪代码如下L 空列表 S 所有入度为0的模块 while S 非空: n S.pop() L.append(n) for m in n的所有下游模块: m.入度 - 1 if m.入度为0: S.push(m) if L.size ! 模块总数: 报告存在环2.3 模型保存与加载用QJson做快照既然是可复用的仿真工具模型保存加载就得做得像样。这里直接用QJsonDocument把整个画布序列化每个模块记录其类型、位置scene坐标、参数表每条连线记录源端口ID和目标端口ID。加载时先反序列化模块再连接端口。为了保证连线在加载后端口指针能正确重建模块实例名必须唯一端口ID使用“模块实例名 方向 端口序号”的字符串约定。JSON方案的优点是直观、可读性好。调试模型文件时甚至可以直接用文本编辑器打开看结构。后续如果要支持版本迁移只需在JSON里加一个version字段读入时做不同版本的分支处理即可。这里踩过的坑是模块指针在加载时不能直接保存和恢复因为内存地址每次运行都不同必须用实例名建立映射加载完成后再统一转成内存指针。3. 图形交互层拖拽、连线和画布操作的实现思路3.1 QGraphicsView架构下的图元规划画布部分我用了经典的Model/View结构QGraphicsScene负责管理所有图元QGraphicsView负责渲染和交互。自定义了两个核心图元类ModuleGraphicsItem模块外观和ConnectionGraphicsItem连线外观。ModuleGraphicsItem内部嵌套多个PortCircleItem作为端口可视对象端口圆点贴在模块边框的左输入右输出两侧。这里嵌入的两个细节很关键。第一端口圆点不能作为独立顶层图元加到Scene里而要作为ModuleGraphicsItem的子图元这样模块拖动时端口位置自动跟随连线只需要在端口坐标变化时更新路径即可否则要手动同步一堆坐标。第二模块图元的boundingRect()必须预留端口和阴影的空间否则选中虚线框会截断端口导致点击端口命中不到。3.2 连线交互拖拽预览、命中检测和重连连线是这类画布工具里最容易做崩溃的操作。我的实现方式是鼠标移到输出端口上时光标变成十字按下左键后在当前鼠标位置创建一个临时的ConnectionGraphicsItem线的一端粘在源端口中心另一端跟随鼠标移动拖动过程中实时做目标端口的命中检测命中后高亮目标端口边框松开鼠标时如果目标端口合法方向相反、模块不同、类型一致则正式创建Connection对象并加入场景否则撤销临时线。命中检测有一个很实用的优化不要遍历场景里所有图元而是维护一份所有输入端口圆点的列表。鼠标移动时遍历这份列表用QPointF的manhattanLength判距40像素以内视为命中。这个开销极小而且能避免命中到不属于本模块内部的图形项上。已连接的端口还有“占用”问题。一个输入端口同一时刻只能连一条线否则后面写入的数据会覆盖前面写入的。解决方案是给端口增加connectedPorts计数达到上限后拒绝新增连线并在UI上置灰端口圆点。输出端口可以接多条线因为一个模块的输出广播到多个下游是仿真里的常态。3.3 缩放、平移和框选画布的顺手操作QGraphicsView自带平移、框选机制但仍需重写wheelEvent实现“以鼠标位置为中心”的缩放这个交互习惯用过Visio或Simulink的人都会很在意。实现时用QGraphicsView::anchorUnderMouse配合scale()效果非常顺滑。中键拖拽平移通过重写mousePressEvent和mouseMoveEvent临时改变光标形状在dragMode设为ScrollHandDrag时其实不用手写Qt自带行为就能满足。框选功能用setDragMode(RubberBandDrag)就能启用但要注意鼠标点击事件冲突。比如要拖动模块而QGraphicsView在RubberBandDrag模式下会把点击空白区域的拖动都理解为框选。我最后采用动态切换策略按下鼠标时先用QGraphicsItem::itemAt判断按下位置是否有模块图元如果有则切换到NoDrag让模块处理拖动如果没有则切换到RubberBandDrag开始框选。这个策略实测交互手感最稳定。缩放时另一个容易翻车的坑是如果画布上的模块名文本或端口标签跟着缩放一起变缩得太小会糊成一团、放得太大又占地方。解决办法是在端口文本图元上设置ItemIgnoresTransformations标志这样它永远保持原始字号显示只是位置跟着模块走视觉上清晰很多。4. 仿真内核与数值计算画完图之后真正跑起来的环节4.1 定步长迭代模块刷新的调度策略仿真引擎我实现成了一个独立的SimulationEngine类不依赖任何QGraphics图元。运行流程拆成四步第一步根据当前模型构建拓扑排序列表第二步遍历所有模块调用initialize()完成状态复位滤波器初值清零等第三步按固定周期dt循环计算每轮遍历执行列表对每个模块调用step(dt)方法从输入端口缓冲区取数、计算、写输出端口第四步通知波形区刷新数据。这里的核心是“一拍延迟”的近似处理。在非实时仿真中如果完全同步更新所有模块可能会遇到代数环。为避免这种复杂度所有模块的输入都从上一拍存放的缓冲区取值当前拍计算结果只写入下一拍才生效的缓冲。这样整张图从输入到输出天然有一个仿真步长的延迟控制精度在可接受范围但稳定性提升明显。模块刷新过程中信号槽的使用要谨慎。仿真引擎的运行线程和UI线程如果是同一个波形区直接用定时器刷新问题不大但如果后续要支持实时硬件数据采集仿真线程就不能阻塞UI。我的做法是把仿真计算放在QtConcurrent的线程池里每隔固定拍数通过信号槽跨线程通知UI更新曲线。这里有个性能教训不要每次仿真步都emit一次更新波形信号那样qcustomplot重绘开销会把CPU拖垮正确方式是累计一段时间后批量发一次。4.2 一阶滤波模块的离散化实现项目里最核心的数值模块是一阶惯性/低通滤波对应Simulink里的Transfer Fcn或First-Order Filter模块。连续域传递函数是G(s) 1 / (T*s 1)仿真程序里必须用离散形式。我用了一阶后向差分法把s替换为(1 - z^-1)/dt整理后得到差分方程y[n] y[n-1] (dt / (T dt)) * (x[n] - y[n-1])这个公式写出来只有一行但参数T和dt的配合非常关键。T越大滤波越平缓、响应越慢dt越小计算精度越高但实时性压力越大。如果dt相对T过大离散化误差会导致滤波效果明显偏离连续域预期。C实现如下void FirstOrderFilterModule::step(double dt) { double x inputs[in].value.toDouble(); double alpha dt / (T dt); y y alpha * (x - y); outputs[out].value y; }运行时我加了一个辅助功能点击模块后可以选“显示阶跃响应”程序会自动在本模块输入端注入一个单位阶跃信号输出端采集过渡过程曲线。这样能快速验证滤波参数是否合理后来调试PID参数时也大量依赖这个功能。4.3 波形显示与时域转频域qcustomplot和FFT的结合波形显示区基于QCustomPlot我用了两条曲线输入信号灰色和输出信号橙色横向滚动显示最近N秒数据。QCustomPlot本身支持setData()批量更新比逐点append高效得多。实时刷新的关键是控制重绘频率用QTimer固定20ms触发一次replot不管期间收到了多少仿真数据点都只重绘一次。项目增加时域转频域功能时最初我只在qcustomplot上画时域波形后来发现用户有看频谱的需求就引入了kissfft做快速傅里叶变换。流程很简单从仿真结果中截取一段长度为2的整数次幂的样本比如1024个点加窗我用汉宁窗减少频谱泄漏然后调kissfft计算幅度谱和相位谱再用第二块QCustomPlot绘制。实测下来1024点FFT耗时微秒级对实时显示毫无压力。频域显示本身也是一个模块化的思路。我在模块库里加了“频谱分析”模块输入端连上任意信号输出侧显示频谱。这样不用改仿真主循环就把分析能力扩展进来了。后来还调通了像示波器那样的“暂停/继续”按钮暂停时冻结当前曲线数据继续时自动清空重画这个交互对观察瞬态过程非常有用。5. Qt开发与调试中的高频问题排查5.1 场景图元崩溃删除连线和模块时的悬空指针开发过程中遇到的崩溃大多集中在删除图元。Qt的QGraphicsScene在删除某个item后如果其他item还持有它的指针在函数返回时可能触发“纯虚函数调用”或随机地址访问。踩坑的典型场景是用户删掉一个模块有几条连线已经从场景里移除但Connection对象里仍持有源端口和目标端口的指针随后信号槽访问这些Port时程序直接崩溃。解决办法是分层释放。删除模块时先收集所有和该模块端口相关的Connection对象逐个从场景移除并delete再把Connection对象从动态列表中移除最后才能delete模块。删除操作结束后再通知其他模块清理端口引用。这个顺序一条都不能乱。为了保险我还在Connection类里把端口指针定义成QPointerPort这样即使端口先被销毁QPointer也会自动变成空指针访问前判空就能避免崩溃。这个改动加上后就没再出现过删除相关崩溃。5.2 跨线程信号槽和波形更新的常见坑仿真计算放到后台线程后一个最常见的报错是“Cannot create children for a parent that is in a different thread”——如果你在线程里直接new QCPGraph或操作QCustomPlot就会当场崩掉。正确的做法是所有UI对象只能在主线程创建和修改。我的方案是后台线程只填充一个共享缓冲区用QMutex保护主线程的QTimer定时从缓冲区读取数据并更新图表。另一个容易忽视的坑是信号槽连接类型。如果后台线程emit信号、主线程接收而连接方式是默认的AutoConnection那一次emit就是一次跨线程事件投递。但如果高频发送信号事件队列会积压导致波形显示越来越卡。解决方法是不要高频emit而是在后台线程累积一段时间后只发一次“新数据就绪”信号UI端再做批量读取。5.3 常见问题速查从中文乱码到发布打包漏文件整理几个我实际遇到过的Qt开发问题做成排查表按常见程度排序问题现象原因解决方案Qt程序在别人电脑上双击没反应缺动态库或插件路径不对用windeployqt部署注意把platforms插件目录一并拷贝中文显示变成乱码源文件编码和编译器不一致源码统一UTF-8MSVC下加/utf-8编译选项Windows下提示no Qt platform plugin程序没找到qwindows.dll检查可执行文件同级目录下是否有platforms文件夹QCustomPlot曲线不更新忘记调用replot()修改数据后必须显式调用replot()触发重绘qcustomplot头文件找不到include路径配置漏了qmake加INCLUDEPATH 路径CMake加target_include_directorieswindeployqt打包后还缺DLL第三库没被自动收集手动把对应qcustomplot等DLL放到exe同级目录后台线程操作UI崩溃违反线程归属原则用信号槽或QMetaObject::invokeMethod切回主线程操作UI发布打包这块如果只是自己学习测试不需要打包成安装包。项目目录下放一个deploy.bat内容就两三行先调用qmake环境变量初始化再执行windeployqt指向构建目录下的exe。我最初打包时忘了拷贝qcustomplot的dll结果换台电脑运行就报缺库。后来直接在deploy.bat里加了条xcopy命令手动把三方DLL复制到发布目录问题彻底解决。6. 项目后续可以扩展的方向这个类Simulink的小工具后续扩展空间很大。比如给模块增加“使能/禁用”状态禁用时模块输出直通或输出零这在模拟故障注入时很有用也可以实现批量调参对某个模块的多个参数做扫描仿真一次性生成多条曲线对比这比手动一次次改参数高效太多。我个人最想加的功能是串口对接。Qt对串口的支持很成熟只需新增一个串口输入模块配置好波特率、数据位和校验位就能把真实传感器的数据接入仿真链路让算法仿真和实物数据在同一个画布里跑起来。这样工具就从“纯仿真演示”升级成了“硬件在环调试台”商业价值完全不同。做这类工具最深的体会是界面和交互做得再炫底层的数据模型和仿真调度也必须严谨否则越往后改越痛苦。网格和坐标吸附、连线上色、自动对齐这些体验优化反而是最后才需要考虑的事。如果你想练手或做一套内部工具强烈建议按“数据模型优先、交互层封装、仿真内核独立”的思路来搭前期多花两天梳理结构后面能省下几周返工时间。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。