简介基于QT框架实现的电力组态系统源码包面向电力监控、组态软件开发者可用于快速搭建可视化设备监控、数据管理及报警提示类界面。资源共320个文件压缩包大小6.21MB以122个h头文件与26个cpp源文件为核心源码另含79个png、7个jpg、2个bmp等图形素材以及13个dll、3个lib运行库和2个pro工程文件可辅助还原编译环境、梳理项目结构。源码重点演示QGraphicsView与QGraphicsScene的复杂2D图形绘制涵盖仪表盘、实时曲线、数据点等动态效果并涉及网络通信、数据库及多线程模块用于支撑数据采集、远程控制等组态常用功能同时提供模板与脚本机制便于扩展功能逻辑。虽然仅公开绘图部分代码但已包含主窗口、绘图视图、属性编辑等关键模块清晰展示了QT控件与电力业务绑定的典型路径。目前已有689人浏览学习适合具备C基础、希望从零上手QT图形开发或了解电力组态方案的读者参考。1. 电力组态系统的第一印象拖拖拽拽背后的实时数据流说句实话第一次把这套QT电力组态系统跑起来的时候我的第一反应是这不就是个画图软件吗电气主接线图、断路器、隔离开关、数字仪表全是拖拽就能放到画布上。可一旦把通信端的数据接上画面里的开关跟着遥信变位变色电表数字跟着遥测值跳动报警灯按阈值闪烁整个系统才露出真面目——一个不靠Web页面、纯C/Qt堆出来的电力SCADA人机界面。它解决的不是“画面好不好看”的问题而是“图形界面怎么和电力实时数据焊在一起”的问题。适合谁想做电力监控、能源管理、机房动环这类带图形组态诉求的Qt开发者或者手里有现成点表、想快速搭一套监控界面的团队。这套源码能直接回答一件事从通信收包到画面刷新中间那层数据链路到底怎么组织。2. 整体架构与目录结构从主窗口到底层通信的全景图电力组态系统不是单文件小工具它至少要拆成“图形编辑期”和“运行期”两套模式。编辑期负责拖图元、连线、绑变量运行期隐藏属性框只保留实时刷新和交互操作。这套工程用Qt Widgets做框架场景基于QGraphicsView数据层集中在core里通信协议单独抽成一个模块跟界面完全解耦。说白了换协议、换数据库、换UI主题都不会把整个工程掀翻。2.1 技术选型为什么是Qt而不是Web组态组态界现在很多产品走B/S路线用SVG或Canvas在浏览器里画电气接线图。可电力监控现场大量存在老的Windows操作员站、嵌入式工控机讲究的是稳定、低延迟、离线可用。Qt做这件事有几个天然优势QGraphicsView自带场景管理和碰撞检测画图元比直接操作DOM轻C编译产物不依赖浏览器版本Signal/Slot做数据驱动的界面刷新特别顺手。代价也存在——分发体积相对大、界面样式不如Web灵活所以这套系统把图元渲染和业务逻辑都留在本地数据库用轻量级SQLite通信模块按电力行业常见的IEC 104来写。实际跑起来后这种选型带来的优势会体现在刷新流畅度上尤其在画面里放了上千个图元时Web方案很难做到同样的拖拽跟手度。2.2 源码目录拆解一个典型的Qt电力组态工程长什么样打开工程根目录不是扁平一坨文件而是按功能分层。我拆完以后把结构简化成下面这个样子power-config/ ├── app/ # 可执行程序入口 │ ├── main.cpp │ ├── MainWindow.cpp # 主窗口菜单、工具栏、编辑/运行切换 │ └── MainWindow.h ├── core/ # 核心库二次开发主要面向这里 │ ├── realtimedb/ # 实时数据库变量管理、订阅、快照 │ ├── graphics/ # 图形场景图元、连线、选中、缩放 │ ├── protocol/ # 通信协议IEC 104、Modbus TCP封装 │ ├── project/ # 工程文件点表导入、画面XML序列化 │ └── alarm/ # 报警模块阈值判断、事件记录 ├── plugins/ # 图元插件 │ ├── basic/ # 矩形、文本、管道、指示灯 │ └── elec/ # 断路器、隔离开关、变压器、电表 ├── tools/ # 离线工具点表转配置、历史数据导出 ├── tests/ # 单元测试和通信模拟器 └── CMakeLists.txt # 顶层CMake工程这里有个细节值得注意图形部分和协议部分都放在core里但具体图元放在plugins里。这么设计是给二次开发留口子——要新增一种特殊图元不需要改动core和app单独编译一个动态库丢进插件目录就能被工厂加载。实际项目里很多人在core里堆几十个图元类看着当时方便后面加需求就得动主工程回归成本高。我之所以愿意继续读这套源码很大程度上就是看中了这一层的隔离设计。2.3 数据链路架构画面、实时库、通信前置机如何协作很多组态系统难懂不是UI难是数据从哪来到哪去没理清。这套系统的链路可以概括成三句话协议模块只负责收包、把字节变成离散量或模拟量实时库是中枢维护一张变量表支持按名字或地址读写图元是终端的“显示器”通过订阅变量刷新自己。画一条实时数据流通信前置机或网关把IEC 104报文通过网络发过来协议模块解析出信息体地址和值调用RealtimeDb::updateValue()入库。实时库更新后发出valueChanged信号绑定了该变量的图元收到信号触发update()重绘。这样图元不需要知道是IEC 104还是Modbus换协议对画面来说完全透明。反过来操作员在画面上点一个合闸按钮指令走反向链路图元发出控件事件实时库找到对应的遥控点协议模块组帧成C_SC_TC命令下发。一条链路两个方向复用。这个架构在工程文件上也做了配合画面XML只记录“哪个图元的哪个属性绑定了哪个变量名”不跟寄存器地址绑定。地址映射放在独立点表里负责变量名和地址的翻译。好处是画面可以脱离硬件环境做离线预览调试时点表文件一换整套画面就能从“演示模式”切到“现场模式”。我在实际调试里经常用这个特性先在办公室用模拟器跑画面再拎着笔记本下现场接真设备省了大量来回折腾的时间。3. 核心模块拆解一个遥测数据从采集到画面刷新的完整链路有了整体架构接下来看核心代码。这一章跟着一条真实的遥测数据走从TCP流里收到IEC 104报文到电表数字在画面上动起来。每个模块只贴最能说明问题的代码段把关键逻辑和参数讲清楚。3.1 通信前置模块IEC 104协议解析与帧处理电力监控里IEC 104是应用最广的规约之一。它跑在TCP之上一帧数据最少6个字节前6字节是控制域APCI后面是ASDU数据单元。遥测一般用M_ME_NC短浮点类型一个信息体包含地址、4字节浮点值和品质描述字。首先要解决的是TCP粘包拆包// 从缓冲区取一帧返回完整帧长度数据不足一帧则返回-1 int Iec104Session::parseFrame(const QByteArray buf) { if (buf.size() 2) return -1; if ((quint8)buf[0] ! 0x68) // 启动字符68H return -1; // 帧头不对链路未同步需要重新找帧 int len (quint8)buf[1]; // 第二个字节是长度不含启动字符 if (buf.size() len 2) return -1; // 数据还没收完整继续等后续包 return len 2; // 完整帧长度 }这个函数本身很单纯检查帧头、根据长度字段判断缓存是否凑齐一帧。注意它不解析具体内容只返回“这帧需要几字节”。TCP流是连续字节拆包是必须做的否则下一条报文会跟在上一条尾部粘在一起。调用方拿到完整帧后需要把已消费的数据从buffer头部移除再循环解析直到剩余字节不足一帧。真正从帧里读数据的地方长这样// 处理一帧I帧解析里面的ASDU数据单元 void Iec104Session::handleIFrame(const QByteArray frame) { quint16 sendSeq (quint8(frame[2]) 0x03) 8 | (quint8)frame[3]; quint16 recvSeq (quint8(frame[4]) 0x03) 8 | (quint8)frame[5]; const QByteArray asdu frame.mid(6); // APCI占6字节ASDU从第6字节开始 if (asdu.size() 10) return; int typeId (quint8)asdu[0]; // ASDU类型标识 int count (quint8)asdu[1] 0x7F; // 可变结构限定词低7位是信息体个数 if (typeId 13 || typeId 21) { // M_ME_NC 遥测短浮点 int offset 6; // ASDU中第一个信息体从第6字节开始 for (int i 0; i count; i) { int addr fromBigEndian(asdu.mid(offset, 3)); // 信息体地址3字节 float val fromFloat(asdu.mid(offset 3, 4)); // 值4字节短浮点 bool valid ((quint8)asdu[offset 7] 0x01) 0; if (valid) db-updatePoint(addr, val); offset 8; // 每个信息体 3字节地址 4字节值 1字节品质 } } }参数说明typeId是ASDU类型标识13对应M_ME_NCcount取asdu[1]的低7位表示这一帧里带了多少个数据点品质描述字第0位置1通常表示无效或溢出这种坏数据直接丢掉防止画面乱跳。这段代码做完协议细节就被挡住了后面只剩下db-updatePoint()的调用。这里要特别提醒通信模块如果和界面放在同一个线程前端一卡收发就会跟着堵。这套系统把通信独立放进QThread通过信号跨线程投递所以即使画面在重绘大场景报文依然不会丢。3.2 实时数据库与变量标签让图元动起来的关键实时库不是完整的数据库更像一张哈希表加一套信号机制。变量除了当前值还要记录质量戳、时间戳、工程单位供报警和历史曲线使用。看这个结构struct Point { QString name; // 变量名例如 #110kV进线.有功功率 int addr; // 协议解析后的信息体地址 float value; // 当前值 quint8 quality; // 质量位 QDateTime ts; // 最近刷新时间 QString unit; // 工程单位 kW }; class RealtimeDatabase : public QObject { Q_OBJECT public: void updatePoint(int addr, float val) { if (!addrIndex.contains(addr)) return; QString key addrIndex[addr]; Point p points[key]; p.value val; p.ts QDateTime::currentDateTime(); emit valueChanged(key, val); } signals: void valueChanged(const QString key, float val); private: QHashQString, Point points; // 按变量名存储 QHashint, QString addrIndex; // 地址到变量名的映射 };这个设计里有个关键点updatePoint用addr而不是name。协议模块拿到的是信息体地址如果让它再翻译成中文变量名就得知道点表映射现在地址索引在实时库里配好协议模块就不用关心画面叫什么。导入点表时把addr和name写进两个哈希表导入完成后协议侧只剩数字进数字出的逻辑。valueChanged信号是整个系统的牵线人所有图元都订阅它但只在key匹配时才重绘。这比定时器全界面轮询省太多性能。图元和变量怎么绑起来画面XML里每个图元节点有类似bind#110kV进线.有功功率的属性。图元构造时把这个属性传给订阅器订阅器向RealtimeDatabase注册回调。绑定粒度可以细到属性比如一个按钮图元的颜色绑一个变量、文本绑另一个变量这样不必要为每种组合单独写图元类型。3.3 画面组态核心基于QGraphicsView的图元系统QGraphicsView是Qt做二维场景最典型的框架。组态软件里场景是一张无限大的画布图元是QGraphicsItem的子类。难点不在创建图元而在运行时如何从XML反序列化出对应图元。这套系统用工厂模式加QDomDocument实现class GraphicItemFactory { public: static QGraphicsItem *createItem(const QDomElement el) { QString type el.attribute(type); QGraphicsItem *item nullptr; if (type Breaker) { item new BreakerItem(); } else if (type Meter) { item new MeterItem(); } else if (type Trend) { item new TrendItem(); } else { item new BasicRectItem(); } if (auto *graphItem dynamic_castGraphicItem *(item)) graphItem-loadFromXml(el); // 每个图元自己解析自己的属性 return item; } };这个工厂特别朴素但守住了扩展性新增图元时只要在if链里加一段再让新类实现loadFromXml和paint即可。plugins里的图元就是通过这套接口注册的。MeterItem的paint大致是这样void MeterItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opt, QWidget *w) { Q_UNUSED(opt); Q_UNUSED(w); painter-drawRoundedRect(rect(), 4, 4); painter-setBrush(m_alarm ? Qt::red : Qt::white); painter-drawRect(rect().adjusted(4, 4, -4, -4)); if (m_bindKey.isEmpty()) return; QString text QString::number(m_value, f, 2) m_unit; painter-drawText(rect(), Qt::AlignCenter, text); }这里m_value是槽函数里更新过来的成员变量paint只负责把它画出来。注意不要在paint里直接读实时库因为QGraphicsScene的绘制阶段对耗时很敏感直接查哈希表可能造成微卡顿。正确做法是在订阅信号里把值存到m_valuepaint只做绘制两者分离。这也是新手写组态最容易踩的坑之一后面避坑章节还会提到。3.4 工程文件XML序列化与点表导入工程文件是组态的“存档”。这套系统一个画面对应一个XML文件根节点保存画面大小、背景色子节点按顺序保存图元。保存代码不复杂void ProjectSaver::saveScene(QGraphicsScene *scene, const QString path) { QDomDocument doc; QDomElement root doc.createElement(scene); root.setAttribute(width, scene-sceneRect().width()); root.setAttribute(height, scene-sceneRect().height()); for (auto *item : scene-items()) { auto *gItem dynamic_castGraphicItem *(item); if (!gItem) continue; QDomElement el doc.createElement(item); gItem-saveToXml(el); // 图元把类型、坐标、绑定写进el root.appendChild(el); } doc.appendChild(root); QFile f(path); if (f.open(QIODevice::WriteOnly)) { f.write(doc.toByteArray(2)); // 缩进2方便后续diff和审查 } }点表导入是另一个独立工具读CSV或Excel导出的文本生成addr,name,unit,deadband配置。这里有个常见坑如果点表里出现重复addr实时库的addrIndex会覆盖导致两个变量名同时指到一个地址。合格的导入器会做重复度检测发现重复就跳过并输出警告行而不是静默丢数据。工程文件只保存变量名不保存寄存器地址这是前面强调过的设计原则。直接好处是现场实施时信息表变了不需要重新画画面只换点表再重启工程。这一点对电力项目尤其重要变电站的接线方式、间隔名称不同但画面结构往往高度相似点表驱动让复用成为可能。4. 编译、运行与工程配置把系统拉起来的完整步骤代码光看没用要跑起来才算数。这一章按我实际编译的顺序写。系统本身是Qt Widgets工程用CMake构建。默认目标平台是Windows 10 MSVC2019 64位Linux下也能编译只是需要装对应的xcb依赖。我这边用的是Qt 5.15这套代码没有用到C20之类的新特性换成5.12以上都能编过。4.1 环境准备Qt版本、编译器与第三方库工程依赖不算花哨主要是Qt自带的Core、Gui、Widgets、Network、Xml模块。以Qt在线安装包为例勾选这5个模块和一个编译器就够。数据库用SQLiteQt自带驱动历史曲线用QCustomPlot源码文件放在third_party目录下一起编译。协议部分不依赖第三方库IEC 104和Modbus TCP都基于QTcpSocket自己封装部署时不用带一堆额外dll。装完Qt后先编译一个空白工程确认环境没毛病再打开这个项目的CMakeLists.txt。第一次编译建议把构建类型设为Release组态软件运行期要的是稳定和性能Debug模式下绘制循环会慢不少尤其拖拽图元时能明显感觉到区别。4.2 编译步骤从CMake配置到出包编译步骤很短按顺序执行就行# Windows MSVC环境下先打开Qt自带命令行(x64 Native Tools) mkdir build cd build cmake .. -DCMAKE_PREFIX_PATHC:/Qt/5.15.2/msvc2019_64 cmake --build . --config Release --target power-configLinux环境下把前缀路径换成Qt安装目录再增加一行-DCMAKE_BUILD_TYPERelease。这里有一个容易忽略的细节CMake会自动找Qt但你必须保证CMAKE_PREFIX_PATH指到对应当前编译器的Qt库。我早期用MinGW编译器的Qt包配MSVC的IDE链接阶段报了一堆LNK2019折腾半天发现是Qt库的编译器和当前编译器不匹配。换成系统自带的Qt命令行后一次通过这个教训后面就没有再犯过。编译完成后可执行文件在build/app/Release/power-config.exe。如果运行时提示找不到Qt5Widgets.dll说明没有把Qt运行库拷过去。用windeployqt一条命令部署cd build/app/Release C:/Qt/5.15.2/msvc2019_64/bin/windeployqt.exe power-config.exe它会自动把依赖的Qt dll拷到exe同目录。部署完双击运行能看到主窗口说明环境没问题。Linux下用linuxdeployqt做类似的事只是依赖到的xcb相关库需要提前装好。4.3 工程配置从点表导入到画面绑定程序起来之后先建工程。实际项目里这步可以做得比较粗新建一个工程目录里面放scene文件夹、点表csv、报警配置。这套系统有向导式界面流程很直接新建工程 - 设置画面大小(如1920x1080) - 导入点表csv - 打开画面编辑点表CSV格式如下addr,name,unit,deadband 1024,#110kV进线.有功功率,kW,0.5 1025,#110kV进线.无功功率,kvar,0.5 2048,高压室.1QF开关,,0注意deadband是死区模拟量变化绝对值小于这个值时不会触发刷新专门治频繁抖动。调试阶段可以把它设成0跑现场再按实际波动重调。导入完成后进画面编辑器左侧拖一个“电表”图元右侧属性框里填bind字段为#110kV进线.有功功率。保存工程点运行模式。这时候画面还不会动因为没通信数据。再打开通信调试窗口选“模拟数据源”按固定周期生成正弦波写进实时库此时电表数字开始跳说明绑定成功。4.4 运行效果验证与判断链路验证链路通不通不能只看画面跳不跳。我的判断顺序是这样先把实时库的值打印打开确认协议模块或模拟源写进来的值进了库再看图元是否响应valueChanged。两个都正常链路通则通。如果值到了库里但画面不动问题一定出在订阅或图元更新逻辑如果库没值问题在通信或点表映射。日志窗口等级分成DEBUG、INFO、WARN、ERROR四档。联调时调到DEBUG能看到每一帧IEC 104报文的原始HEX以及解析出的addr/value。这套系统有个挺贴心的设计日志输出把通信源信息体地址和点表导入后的变量名打到同一行省去翻代码找映射关系。用Wireshark抓包当然是更底层的办法但在这个系统里能解析出实际值说明TCP层已经通了剩下问题基本都在业务层。5. 高频踩坑与排查Qt电力组态常见的六个问题整个拆解过程里我实际遇到过不少坑这一章整理成现象、原因、解决的排查记录都属于那种看着不大、卡起人来要命的类型。5.1 现象图元选中卡顿拖拽时CPU暴涨在画面上框选几十个图元拖动明显感觉不跟手打开性能分析发现重绘时间占到60%。原因出在很多图元实现boundingRect()不严谨要么返回整个场景的rect要么把包围盒设得过大。场景一扩大任何小改动都要重绘大范围自然卡。解决方法是让每个图元的boundingRect()精确到自身包含画笔宽度和阴影尺寸同时给QGraphicsScene设置合适的索引用BspTreeIndex替代默认索引。改完后框选上百个图元再拖帧率从15帧提到接近60帧。这类问题在Web组态中通常要靠Canvas局部重绘而QGraphicsView的Item系统一旦包围盒规范收益非常直接。5.2 现象IEC 104报文偶尔丢帧重启后恢复联调时发现设备上送的数据偶尔缺几个点程序重启后正常跑一会又开始丢。原因不是线程阻塞而是接收缓冲区拆包逻辑有问题。TCP粘包发生时一个字节数组里可能有两帧完整数据旧代码处理完第一帧后没有把剩余数据挪到缓冲区头部而是直接丢弃第二帧后半段被截掉。解决方法是收到新数据先append到内部缓冲再循环调用parseFrame每解析完一帧用remove(0, len)清掉已消费数据直到缓冲区长度不足一帧。这是所有网络协议解析都要刻进肌肉记忆的点循环解析别一帧只处理一次。5.3 现象修改变量名后画面部分图元不刷新点表里把变量A改名成B重新导入点表运行画面还有图元显示旧值。原因很隐蔽画面XML里保存的是bindA点表导入时如果检测不到旧名A就静默跳过图元一直挂在一个已经不存在的变量上收不到任何更新。解决方法是导入点表时做一次全工程交叉校验扫描所有scene文件里的bind属性发现引用了不存在的变量就弹警告并列出关联图元坐标。从那以后我每次导入点表都强制跑一遍这个校验不再靠肉眼巡检画面找问题。5.4 现象中文变量名在Linux下乱码同一份工程在Windows下正常拷到Linux系统运行画面上的中文标签全变成问号。原因是工程XML在Windows上保存时带了BOM的UTF-8Linux的QDomDocument解码结果受系统locale影响更常见的坑是代码里用QTextCodec固定读了GBK换环境后直接乱码。解决方法是统一编码规范保存时强制UTF-8无BOM加载时用QFile读回原始字节再用QTextStream并显式setEncoding(UTF-8)不做任何locale假设。这事价值不高但能在上线当天卡掉整个上午。5.5 现象操作员站重启后工程文件路径失效程序部署到现场操作员站重启后画面加载不出来排查发现工程文件路径写的是绝对路径但现场服务名、磁盘盘符往往和开发机不一样。解决方法是把工程默认路径改成相对可执行目录用QCoreApplication::applicationDirPath()作为基准首次启动时自动创建工程目录并生成默认画面。这个坑在开发机上很难复现因为开发机的目录结构和现场总是一致的所以也最容易在交付时炸。5.6 现象运行模式切换编辑模式偶发崩溃运行一段时间后切回编辑模式点某个图元的属性时瞬间崩溃。原因是删除图元时信号槽里还持着它的裸指针运行中的图元可能在数据刷新和销毁之间发生竞态。解决方法是图元析构之前确保断开所有订阅连接或者在订阅器里用QPointer管理裸指针切换模式前把场景的items()先拷贝一份再遍历避免迭代器失效。这类问题本质上都是“图元生命周期”和“数据更新线程”不同步我在其他工控项目里也反复撞见。6. 二次开发从添加一个新图元到发布自定义控件的完整流程最后落到上手改代码。假设现场需要一个展示发电机轴振动的趋势图元拖进画面后能显示最近一分钟的实时值曲线。传统做法是放一个QCustomPlot控件再包进QGraphicsProxyWidget但代理控件的渲染效率和样式联动总别扭。更干净的做法是写一个自定义QGraphicsItem直接画曲线。实现大致三步新建TrendItem类继承QGraphicsObject实现paint和boundingRect再在工厂里注册类型。paint里的核心代码void TrendItem::paint(QPainter *p, const QStyleOptionGraphicsItem *opt, QWidget *w) { Q_UNUSED(opt); Q_UNUSED(w); p-drawRect(rect()); QPolygonF line; for (int i 0; i m_points.size(); i) { qreal x i * 4.0; qreal y rect().bottom() - m_points[i] * 10; line QPointF(x, y); } p-drawPolyline(line); p-drawText(rect().topLeft() QPointF(4, 12), m_name); }注意m_points的更新要放在valueChanged槽里不要在paint里做数据增长。然后在工厂的if链里加一行if (type Trend) item new TrendItem();这样XML反序列化时就能直接创建Trend。验证时新建一个测试场景手动往m_points里推模拟数据切到运行模式看曲线是否平滑绘制。再回到编辑模式确认saveToXml把这个图元的采样间隔、边框颜色全部存了进去重新加载工程能原样读出来。这类控件改完我有一个固定习惯每次加完新图元都会重新走一遍“新建工程→拖图元→绑变量→运行模式”的完整流程同时观察绘制性能和数据刷新。二次开发最容易漏掉的是编辑模式切运行模式时图元属性没有保存下来。我在这上面吃过亏后来在保存函数里强制遍历场景所有QGraphicsItem逐个调用saveToXml不依赖手工触发。从那以后每次加控件我都要先把保存和加载跑通再往里面填业务逻辑。希望这个流程对你也有帮助。本文还有配套的精品资源点击获取