简介这是一套基于QT开发的电力组态系统源码面向电力监控与数据管理场景适合C开发者学习图形界面与组态架构。资源共320个文件以122个h头文件、26个cpp源文件为核心配合79个png及ui、qss等界面资源另含工程文件与运行库打包6.21MB。已有689人学习下载。压缩包重点提供绘图部分源码覆盖QGraphicsView/QGraphicsScene渲染、自定义图形元素、动态数据绑定与模板脚本扩展涉及主窗口、属性编辑器、实时数据库等模块。通过研究这些实现可以理解QT在电力组态系统中的可视化搭建逻辑为类似监控界面与交互设计提供工程参考。1. 电力组态系统为什么用 Qt 而不是纯 Web 方案做电力监控这块组态画面是刚需而且往往不是一张两张是几十张、上百张地出。很多团队一开始想用 Web 方案图省事结果做到第二张图就发现拖拽、缩放、设备图元刷新全卡在浏览器渲染上更别说对接实时库时 WebSocket 那套断线重连有多让人头疼。Qt 在电力组态里的优势很直接Graphics View 框架天生就是干这个的——图元管理、视图变换、图层顺序、事件体系都是桌面级不需要像 Web 那样用 canvas 或 SVG 硬扛。这套资源的核心价值在于它不是给你一坨散乱的源码而是把「画图 → 建模 → 绑定实时数据 → 工程交付」整条链路串起来。适合两类人一类是刚接手电力监控项目、需要快速立项的 Qt 开发者另一类是已经在做传统组态软件比如 WinCC、组态王替换的团队想用 Qt 造一套自己的东西。文章后面我会从工程结构讲到实时库对接再讲到归档格式和打包裁剪全是实操层面的东西。2. 项目工程结构先看懂目录再谈改代码拿到这套 Qt 电力组态系统源码第一步不是急着编译而是先把目录结构读明白。一个工程文件放错位置或者依赖库漏了后面所有折腾都是在浪费时间。这一章我把目录的职责边界、构建配置和启动流程拆开讲让你能在一小时内搞清楚「加一个新画面到底该动哪个目录」。2.1 目录职责从界面层、核心层到数据层的解耦方式这套项目把代码分成了几个大块核心原则是「界面不碰数据数据不碰业务」。界面层存放所有与显示相关的代码包括主窗口、画面编辑器、图元选择面板核心层放的是图形场景管理、图元类型注册、连线拓扑计算数据层则是对接实时库、处理快照推送、管理映射表。project_root/ ├── app/ # 入口与主窗口 │ ├── main.cpp │ └── MainWindow.cpp/h ├── editor/ # 组态编辑界面 │ ├── SceneEditor.cpp/h │ ├── ItemPalette.cpp/h │ └── PropertyPanel.cpp/h ├── core/ # 图形场景与图元库 │ ├── GraphicsScene.cpp/h │ ├── base/ # 图元基类、基类工厂 │ └── symbols/ # 变压器、断路器、母线等设备图元 ├── data/ # 实时库适配层 │ ├── RtcAdapter.cpp/h │ └── MappingTable.cpp/h └── resources/ # 图标、字体、样式表这个分层和 Windows 组态软件的逻辑很像但比传统 MFC 风格好改得多。你如果要新增一种设备图元只需要在core/symbols/下加一个类注册到工厂即可不需要碰 SceneEditor 或者 MainWindow。常见做法是图元的绘制函数统一走虚函数paint()属性面板则从图元对象上读取属性列表自动生成编辑表单。这种「模型-视图」分离的思路在后续加脚本联动时会省很多事。2.2 构建配置qmake 与 CMake 的选择以及关键编译参数这个项目默认用的是 qmake因为电力行业现场环境有时候极其老旧有的机器连 CMake 都没装qmake 的.pro文件在 Windows 下配合 Qt Creator 是最稳妥的。但如果你要持续开发和模块化CMake 更利于增量编译和第三方库集成。QT core gui widgets CONFIG c17 DEFINES RTC_ENABLE_LOG SOURCES app/main.cpp \ app/MainWindow.cpp \ editor/SceneEditor.cpp HEADERS app/MainWindow.h \ editor/SceneEditor.h RESOURCES resources/resources.qrcc17是为了用std::variant做图元属性值的多类型存储比如电压是 double、状态是枚举、备注是 QString用 variant 统一收口比 QVariant 更直接也不用在类型转换上做层层判断。RTC_ENABLE_LOG是数据层的日志开关现场查问题时打开正式发布时关掉避免日志文件把磁盘写满。如果你换成 CMake注意CMAKE_AUTOMOC和CMAKE_AUTOUIC必须打开否则 Qt 的元对象编译会失败。还应该用WIN32标志隐藏控制台窗口否则客户那边双击 exe 会弹一个黑框这在电力机房场景很不体面。2.3 启动流程与画面加载从空白场景到首张组态图主程序启动后做了这么几件事读取配置文件、初始化实时库适配层、创建主窗口、加载默认画面文件。画面文件里记录的坐标和连线关系会被反序列化成一棵图元树再挂到 GraphicsScene 上。int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow w; w.loadProject(config/default.ini); w.show(); return app.exec(); }这里有一个我踩过的坑画面文件里保存的坐标是「逻辑坐标」Graphics View 在显示时会根据视图缩放比例做变换。如果你在加载画面后马上调用fitInView()正在播放的动画会被打断。我一般会在主窗口显示后延迟 50ms 再执行一次缩放匹配用QTimer::singleShot就能解决。启动流程里还要注意配置文件解析的容错。常见做法是解析失败时不直接退出而是弹出选项框让用户选择「继续用空工程」还是「打开备份配置」。电力机房有时会出现断电导致配置文件写到一半损坏这种情况你直接让程序崩溃客户会非常崩溃——别问我是怎么知道的。3. 拖拽建模与图元体系这一段是整套代码的骨架组态编辑最核心的操作就是拖拽建模从图元面板拖一个断路器到画布连上线保存再让实时库把状态推过来刷新。听起来简单但实际做起来涉及图元基类设计、坐标吸附、连线绑定、层次管理四个问题。这一章把这四件事的代码和逻辑讲透。3.1 图元基类与类型注册为什么每个设备都继承同一个基类项目里所有设备图元都继承自一个基类BaseSymbol。基类里定义了设备 ID、名称、状态枚举、旋转角度、属性表这几个核心字段以及paint()、boundingRect()、updateState()三个纯虚函数。这种设计的核心价值在于对外只暴露统一接口上层逻辑处理的是BaseSymbol*指针不需要关心具体是变压器还是隔离开关。class BaseSymbol : public QGraphicsObject { Q_OBJECT public: BaseSymbol(const QString id, QGraphicsItem* parent nullptr); virtual void updateState(const DeviceState state) 0; virtual QVariantMap dumpConfig() const 0; virtual void loadConfig(const QVariantMap cfg) 0; signals: void stateChanged(const QString deviceId); protected: QString m_deviceId; int m_rotation; QVariantMap m_properties; };dumpConfig和loadConfig是工程存档与加载的关键。存盘时把每个图元的 ID、坐标、旋转、属性表全部导出成 QVariantMap再序列化为 INI 或 JSON加载时反向操作。我在实际项目里还加了一个difficulty字段用来标记这个图元是否已经通过一致性校验方便编辑器在非编辑模式下把未完成的图元半透明显示。图元注册机制用的是工厂模式。一个静态工厂里维护QHashQString, std::functionBaseSymbol*()新增图元只需调用registerSymbol(breaker, [] { return new BreakerSymbol; })。别小看这个组态系统的图元数量一多手写 if-else 会变成维护噩梦。3.2 拖拽图元从图元面板到画布鼠标事件链路拖拽的实现在 SceneEditor 的mousePressEvent和mouseMoveEvent里。按下时如果图元在面板内就创建一个「影子图元」跟随鼠标移动松开时执行吸附判断和碰撞检测合法就落位。void SceneEditor::mouseMoveEvent(QMouseEvent* event) { if (m_dragging m_shadowItem) { QPointF scenePos mapToScene(event-pos()); QPointF snapped snapToGrid(scenePos, 20); m_shadowItem-setPos(snapped); } QGraphicsView::mouseMoveEvent(event); }snapToGrid的实现最烦吸附步长必须是 20 的倍数设备中心要落在线缆网格的交点上否则后续连线对不齐。组态行业里有个不成文的规定母线、线缆、连接端子全部在同一个网格上设备本体可以放大缩小但连接点必须落在网格线上。这样出图才规矩后续自动布线算法也好使。3.3 连线与拓扑绑定关系如何存储为什么母线比想象中难画连线这块很多第一次做组态的人会用QGraphicsLineItem直接画一条线段完事。但这套系统没有这么做——它把连线单独抽象成ConnectorItem内部记录「起点设备」「终点设备」「起点端口号」「终点端口号」四个字段。拓扑重算时遍历所有 ConnectorItem就能还原出整个网络结构。母线画出来容易难的是「母线是若干线段组成的连续导体」它要允许设备在任意位置挂接而不是像普通连线那样只能两端接入。我见过不少项目在母线上挂设备时连线的起终点坐标写死母线稍微移动一下就全部错位。这个项目的做法是把母线拆成「母线模型」和「母线段图元」两层——母线模型是一个抽象导体对象提供addTap(deviceId, pos)方法母线段图元负责视觉呈现。这样改母线端点位置时所有挂接设备的连接关系不丢。3.4 对齐与坐标捕获不要放过细节点位问题坐标捕获的隐藏坑在于图元缩放后连接点位置可能不再是中心点。断路器的左下角和右上角各有一个端子端子坐标必须跟随图元旋转和缩放同步变化。我一般会在paint()里绘制端子标记一个小圆点或小矩形同时在getConnectorPos(port)中实时计算端子坐标不把它缓存死。以上是这套系统的图元骨架。如果只学一个东西建议把「基类 工厂 映射表」的思路吃透这也是后面实时库能跟图元绑定的根。4. 实时数据绑定与刷新从 RTC 到图元状态的点对点映射图元画完后真正让它动起来的是实时库RTC的数据绑定。很多开发者在做完画面编辑后就开始卡壳——「怎么让电压数字在图上实时跳」「怎么让开关状态变颜色」。这一章讲的是数据从实时库到图元属性再到界面重绘的完整链路。4.1 属性订阅表把图元 ID 与实时库点表关联电力监控里每个设备在实时库里是一个点Tag点的值是 float、int 或 bool可能带质量戳。组态系统要做的就是建一张「图元 ID → 点表条目」的映射表。这个映射通常在工程配置阶段由工程师手工配好运行时通过这一张表把数据精准地送到对应图元。mapping { QS1: {tag: DI101, attr: state, type: bool}, T1: {tag: AI201, attr: voltage, type: float}, K01: {tag: DI103, attr: breaker_state, type: bool}, }MappingTable 通常用一个QHashQString, MappingEntry存着key 是设备 IDvalue 是 tag、attr、type、缩放系数这几个字段。RTC 推送来时先拿出设备的 mapping按 attr 把值写入m_properties[state] QVariant(true)再调用update()重绘。这个表还承担一个职能把 RTC 的原始点表名称翻译成组态系统内部属性名。因为点表名称是现场工程师从站端数据定义拷来的有可能叫1#主变_A_相电压这种带中文和下划线混合的名字而图元属性是纯英文的。映射表就是这层翻译不做这步整个代码里到处是硬编码中文维护成本直接就炸了。4.2 数据刷新策略不要每帧都全量重绘数据刷新是组态性能瓶颈最常出现的地方。很多初学者的写法是「RTC 来一个数据就调一次 update() 重绘整个场景」如果频率是 1 秒 100 个点CPU 直接被打满。正确做法是把 RTC 推送的数据先收集到一个缓冲区每 100ms 统一触发一次场景重绘且只重绘属性发生变化的那几个图元。void RtcPolling::onDataReceived(const QByteArray data) { m_buffer.append(data); if (!m_flushPending) { m_flushPending true; QTimer::singleShot(100, this, [this]() { flushBuffer(); m_flushPending false; }); } }flushBuffer里解析出所有更新项逐一调用对应图元的updateState最后统一调用scene-update()。这个 100ms 窗口是经验值——太快会丢失数据合并效果太慢又会让操作员感觉画面卡顿。60fps 的界面刷新对电力监控场景已经足够它不像游戏需要 144Hz。4.3 属性绑定到状态色开关状态、电压越限、坏值判定开关状态绑定的最常见需求是闭合显示红色分闸显示绿色未知状态显示灰色。这套系统在BaseSymbol::paint()里读m_properties[state]再去查颜色映射表。电压越限的提示多加了一个阈值判断——从实时库拿来的值先和「上限/下限」做比较越限时显示告警闪烁。这段如果做成代码就是每个图元在 paint 里查状态、查越限但实际操作里我会建议把「状态 → 外观」做一个统一的 style 表而不是每个图元自己写死颜色。内部维护一个QHashQString, QColor不同状态对应不同色值。图元只需要向 style 表查询不需要知道「红色是告警还是合闸」这样产品和工程上的需求变更就不用大范围改代码了。4.4 刷新高频场景用「脏标记 局部重绘」保证性能当一屏有几百个图元同时变化时全量重绘撑不住。这套系统的做法是给图元加一个 dirty 标记只有标记为脏的图元才重新绘制。在updateState里写入新值之后如果属性确实发生了变化就setDirty(true)在 scene 的重绘函数里只遍历 dirty 图元。同时还有一个细节Graphics View 的item-update()默认是刷新整个图元的外接矩形如果你把 boundingRect 写大了刷新面积也大。我习惯在 boundingRect 里精确返回设备所占区域比如一个旋钮图元旋转 45 度后的外接矩形要比旋转前稍微大一圈这个可以直接用 QPolygonF 的 boundingRect 计算不要手动写一个放大的固定值。这么做能减少 20% 以上的重绘面积滚动时体感特别明显。5. 实时库与通信协议这层是很多项目被卡死的地方组态系统如果只做画面那它只是一个画图工具。真正的电力监控系统画面必须要和实时库互通数据。这一章我讲 RTC 适配层怎么做常见网络协议怎么收到真实数据以及最容易见鬼的一些现场问题。5.1 RTC 适配层把通讯协议从画面代码中隔离在项目里实时库适配层被独立成一个RtcAdapter类内部封装连接管理、帧解析、数据回调三部分。业务界面层不直接碰 socket只收到dataReady(const QByteArray)信号。这样换协议比如从 OPC 换成 104 规约界面代码一行不改。class RtcAdapter : public QObject { Q_OBJECT public: void start(const QString host, quint16 port); void subscribe(const QStringList tags); signals: void dataReady(const QByteArray payload); void disconnected(); };这个接口设计得越小越好。很多项目死在「适配层越做越肥」——解析逻辑、报警逻辑、日志逻辑全塞进一个类里。我后期的习惯是适配层只负责「收原始报文 → 拆出数据帧 → 发 signal」数据怎么用是上层的事。订阅操作在连接建立后做一次后续断线重连后自动重新订阅。5.2 常见规约解析104 规约、modbus 轮询、私有协议的接入方式电力行业最常见的三种协议处理方式完全不同。104 规约是主动上送适配层只需要在收到 I 帧时解析typeId、infoAddr和data三块信息Modbus 是主从轮询适配层要有定时器每隔一段时间发一次读请求收到响应后按寄存器地址前后映射数据私有协议往往是最考验耐心的很多站端产品自己做一套纯文本协议字段用逗号分隔结尾带\r\n直接QString::split都能解析。if (typeId 0x09) { // 遥测帧 quint16 infoAddr getInfoAddr(payload); float value getFloatFromPayload(payload); emit onTelemetry(infoAddr, value); }这段是 104 遥测帧的典型解析骨架。getFloatFromPayload 里要注意字节序104 规约常见的大端字节序在 x86 平台上要手动转换。如果你设备点是 4 字节 float从报文里按字节抠出来后不转换读出的数值可能完全不可理喻——从 1.0 变成接近 nil 的异常数字。这是最常见的翻车点没有之一。5.3 断线重连与标签点调度别做无脑重连好多团队在对接实时库时断线后就是一个while循环死连。这在局域网的嵌入式场景还行但在跨交换机或带防火墙的电力调度网络里无脑重连会把自己打成 DDoS。这套系统里重连策略是「指数退避 上限封顶」——第一次断线等 1 秒重连第二次 2 秒第四次 8 秒封顶 30 秒。如果重连超过 10 次还没成功就弹提示并不再自动重试把操作权交给值班员。标签点调度这里我每次都会看工程人员的点表有没有「垃圾点」——很多现场会配几千个点但实际组态画面只用一两百个。让适配层订阅了所有点带宽和实时库压力都会失控。默认做法是通过配置文件限定订阅范围只订阅组态画面里用到的点这样网络负载减少一半。5.4 通信日志与现场排查玄学问题的万能抓手现场最怕出现的问题就是「偶尔收不到数据」「画面跳变但是时好时坏」。这类问题 90% 和协议没关系而是报文帧错位或解析状态机不对。我的习惯是从第一版就把通信日志写全时间戳、原始报文字节串 hex、解析结果、校验值。有了这些日志远程排查至少能推进 80%而不是在微信里现场同事一遍遍「你再测一下」。在代码里日志走qInfo().noquote()带上下文输出不要用qDebug()因为生产环境可以屏蔽 debug 等级但 info 等级保留。一旦遇到现场玄学问题打开 info 日志基本就能看出是哪一步开始错的。6. 工程交付与高级玩法把这套系统变成可交付的东西做完画面编辑、数据绑定、协议对接这套组态系统基本上已经能用了。但交付给客户并不是把代码跑起来就算完事。这一章讲讲工程版本管理、画面模板复用、与第三方系统交互这几个只有做项目才用得上的点也是网上教程很少提及的部分。6.1 工程画面对外导出的格式JSON vs 二进制组态画面的存档格式直接决定你交付的工程能不能被后续工具继续加工。这套项目里默认导出 JSON 而不是二进制原因有三个可读、可 diff、可扩展。JSON 在文本层面就能看出「哪两个设备连了线」「哪个图元改了位置」做版本对比时特别方便。{ sceneVersion: 1.2, symbols: [ {type: breaker, id: QF1, x: 120, y: 80, state: 1}, {type: transformer, id: T1, x: 320, y: 220, state: 0} ] }如果担心画面文件太大一个 200 图元的工程 JSON 也就几十 KB完全不需要压缩。但有一点要注意——JSON 里的坐标系必须是场景坐标不要存视图坐标。视图坐标和缩放有关存进去换一台屏幕分辨率就全乱了。6.2 画面模板复用让现场工程师的工作量从 10 天变 2 天做过电力监控项目的都知道现场几十张组态图里高压、低压、变压器、馈线这几个柜型重复率极高。如果每次从零开始画一张图要半天做成模板后五分钟改一个名字和点位就能出图。模板里不存具体点表而是存「相对坐标」和「变量占位符」。这样一张主变间隔的模板复制到另外两路间隔上只需要把占位符替换成对应点表的 tag 名即可。# 把模板中的占位符替换为实际点表 sed -i s/\$TAG_DEVICE$/1#主变/g 主变间隔.json对于程序员来说可能有点粗暴但现场工程师用这个方式效率极高。我实际做的时候还会在界面里加一个「导出模板/应用模板」按钮把模板管理固化到软件里比到处教 sed 靠谱得多。6.3 与第三方系统的交互OPC UA、数据库写库、Web 发布电力组态系统很少单打独斗经常要接 OPC UA 跟监控后台通信或者把历史数据写到 MySQL 里让报表系统去读。这套架构在最外层预留了IntegrationGateway接口数据通过信号槽向外广播。只要把新数据源适配到同一个接口规范上多系统交互就顺了。增长最快的需求是 Web 发布——客户想用浏览器远程看到同样的画面。Qt 侧做法一般是把画面批量导出成图片或矢量图再推送到前端展示并不直接让 Qt 跑在浏览器里。图像推送的间隔用 5 秒一张足够不用做到实时视频流否则机房带宽撑不住。6.4 交付检查清单那些最容易在现场翻车的点最后的交付阶段我习惯按下面这几条过一遍每条背后都有真实的惨痛经历中文乱码问题源代码文件一律 UTF-8 编码Qt 5 里 MSVC 编译和 MinGW 编译对中文字符串的处理不同如果乱码在客户电脑上出现大概率是编码问题而不是程序问题。DLL 缺失Wind本文还有配套的精品资源点击获取