简介:基于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_NC;count取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: QHash<QString, Point> points; // 按变量名存储 QHash<int, 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_cast<GraphicItem *>(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_value,paint只做绘制,两者分离。这也是新手写组态最容易踩的坑之一,后面避坑章节还会提到。
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_cast<GraphicItem *>(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,这套代码没有用到C++20之类的新特性,换成5.12以上都能编过。
4.1 环境准备:Qt版本、编译器与第三方库
工程依赖不算花哨,主要是Qt自带的Core、Gui、Widgets、Network、Xml模块。以Qt在线安装包为例,勾选这5个模块和一个编译器就够。数据库用SQLite,Qt自带驱动;历史曲线用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_PATH="C:/Qt/5.15.2/msvc2019_64" cmake --build . --config Release --target power-configLinux环境下把前缀路径换成Qt安装目录,再增加一行-DCMAKE_BUILD_TYPE=Release。这里有一个容易忽略的细节: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里保存的是bind=A,点表导入时如果检测不到旧名A,就静默跳过,图元一直挂在一个已经不存在的变量上,收不到任何更新。解决方法是导入点表时做一次全工程交叉校验,扫描所有scene文件里的bind属性,发现引用了不存在的变量就弹警告,并列出关联图元坐标。从那以后我每次导入点表都强制跑一遍这个校验,不再靠肉眼巡检画面找问题。
5.4 现象:中文变量名在Linux下乱码
同一份工程在Windows下正常,拷到Linux系统运行,画面上的中文标签全变成问号。原因是工程XML在Windows上保存时带了BOM的UTF-8,Linux的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,不依赖手工触发。从那以后每次加控件我都要先把保存和加载跑通,再往里面填业务逻辑。希望这个流程对你也有帮助。
本文还有配套的精品资源,点击获取