1. 工业控制系统选型Qt的底层逻辑
1.1 为什么工业场景偏爱Qt而不是Web那一套
做过工业现场的人都有一个共识:车间里的东西跟互联网产品完全是两个世界。互联网讲究快速迭代、灰度发布、A/B测试,而工业控制系统讲究的是确定性——同样的输入必须给出同样的输出,今天跑得通的东西三年后还得跑得通。这个底层差异直接决定了技术选型的走向。
Qt在这个领域站稳脚跟,核心原因有三条。第一是原生性能与实时响应,工业控制里很多场景对延迟极其敏感,比如运动控制卡的指令下发、PLC数据采集的轮询周期,用浏览器套壳的方案光是渲染管线就够你喝一壶。第二是跨平台的一致性,同一套代码要跑在Windows工控机、Linux边缘网关、甚至某些嵌入式ARM板上,Qt的抽象层能让你少写大量平台适配代码。第三是离线可靠性,车间网络环境往往不稳定甚至完全隔离,Web方案一旦断网就是白屏,而Qt应用是本地进程,断网照跑。
我见过不少团队一开始图省事用Electron或者纯Web做上位机,demo阶段很爽,一到现场就各种问题:内存泄漏导致连续运行72小时后崩溃、串口通信在浏览器里根本没法直接操作、多线程数据采集被JS单线程模型卡死。这些问题不是不能解决,而是解决成本远高于一开始就用Qt。
1.2 Qt版本选择:2026年的现实考量
到2026年这个时间点,Qt的版本格局已经比较清晰了。Qt 6.x系列是绝对主力,Qt 5.15作为长期支持版本仍然在一些老项目中服役,但新项目没有理由再选5了。具体到小版本,建议选择Qt 6.5或更高的LTS版本,因为从6.5开始,Qt在工业通信模块、串口/Modbus支持、以及QML性能上都有实质性改进。
选版本时有几个坑要避开。一是许可证问题,Qt的商业授权和开源授权(LGPL/GPL)差异很大,工业项目如果是闭源交付,用LGPL需要动态链接且提供替换Qt库的能力,这一点在嵌入式部署时特别容易踩雷,建议提前跟法务确认。二是编译器匹配,Windows下MSVC和MinGW的选择会影响第三方库的兼容性,工业领域常用的很多SDK(比如某些板卡厂商提供的库)往往只提供MSVC版本的静态库,所以Windows平台优先选MSVC。三是Qt Modules的取舍,不是所有模块都需要装,工业项目常用的核心模块是Qt Core、Qt Gui、Qt Widgets、Qt SerialPort、Qt Network、Qt SQL、Qt Charts,QML相关的模块看是否需要做炫酷的HMI界面。
1.3 架构分层:把界面和业务彻底掰开
工业控制系统最怕的就是把业务逻辑写进界面代码里。我接手过的一个烂摊子项目,一个MainWindow.cpp写了八千多行,串口收发、协议解析、数据库操作、界面刷新全搅在一起,改一个按钮的位置都能引发数据采集异常。这种代码在工业场景里是灾难,因为工业项目生命周期长,需求变更频繁,耦合度高的代码维护成本会指数级上升。
合理的分层应该是这样的:通信层负责跟下位机(PLC、板卡、传感器)打交道,封装串口、TCP、Modbus等协议;数据层负责数据的解析、缓存、持久化;业务逻辑层负责控制算法、状态机、报警判断;界面层只负责展示和用户交互。层与层之间通过信号槽或者接口类通信,界面层绝对不直接调用通信层的具体实现。
这个分层说起来简单,做起来最容易犯的错是"图省事直接跨层调用"。比如界面上一个按钮点击后直接去写串口,看起来少写了几行代码,但后面要做单元测试、要换通信方式、要加日志记录的时候就傻眼了。我的经验是,在项目初期就定义好层间接口,哪怕一开始实现得很粗糙,也比后期重构强。
2. 开发环境搭建与工程结构设计
2.1 工具链安装的实操细节
Qt开发环境的搭建本身不复杂,但工业项目有些特殊要求。首先是Qt Creator + Qt库 + 编译器三件套,Windows下推荐用Qt官方在线安装器,勾选对应的Qt版本和编译器套件。安装路径绝对不要带中文和空格,这是血泪教训,某些第三方库的构建脚本对路径中的空格处理有问题,会导致莫名其妙的链接错误。
Linux下的安装稍微麻烦一点,Ubuntu系可以用apt装基础包,但版本往往偏旧,建议还是用官方安装器或者自己编译。自己编译Qt是个体力活,配置参数一大堆,工业项目常用的配置大概是这样:
./configure -prefix /opt/Qt/6.5.0 \ -opensource -confirm-license \ -release \ -nomake examples -nomake tests \ -skip qtwebengine -skip qt3d \ -qt-sqlite -qt-zlib \ -serialport -modbus这里-skip qtwebengine是因为工业项目基本用不到浏览器内核,编译它要耗费大量时间和磁盘空间。-serialport和-modbus是工业通信的刚需,一定要带上。
安装完成后,环境变量配置是新手最容易出问题的地方。PATH里要包含Qt的bin目录,同时要确保编译器路径也在PATH里。Windows下如果同时装了多个版本的Qt和多个编译器,建议用Qt Creator的Kits管理功能,为每个项目明确指定套件,不要依赖系统默认。
2.2 工程文件组织:从第一天就规范起来
一个工业控制项目的源码文件轻松上百个,如果目录结构混乱,后期找代码就是灾难。我推荐的目录结构是这样的:
project/ ├── src/ │ ├── core/ # 业务逻辑、状态机 │ ├── comm/ # 通信层,串口/TCP/Modbus │ ├── data/ # 数据模型、持久化 │ ├── ui/ # 界面相关 │ └── utils/ # 工具类、日志、配置 ├── include/ # 对外头文件 ├── resources/ # 图片、qss、翻译文件 ├── tests/ # 单元测试 ├── third_party/ # 第三方库 ├── docs/ # 文档 └── build/ # 构建输出(gitignore)对应的.pro文件或者CMakeLists要按模块组织。Qt 6之后官方主推CMake,新项目建议直接用CMake,虽然学习曲线比qmake陡一点,但生态更好,跟第三方库集成也更方便。一个典型的CMakeLists骨架:
cmake_minimum_required(VERSION 3.16) project(IndustrialControl VERSION 1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets SerialPort Network Sql Charts) add_subdirectory(src/core) add_subdirectory(src/comm) add_subdirectory(src/ui) target_link_libraries(IndustrialControl PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::SerialPort Qt6::Network Qt6::Sql Qt6::Charts core comm ui )CMAKE_AUTOMOC这几个开关一定要开,否则Qt的信号槽机制没法工作。工业项目里经常有人手动跑moc然后忘了,编译报一堆"undefined reference to vtable"的错误,排查半天。
2.3 版本控制与协作规范
工业项目的代码往往要维护很多年,版本控制必须从第一天就规范。Git是标配,但有几个工业场景的特殊约定。二进制文件不要进Git,比如板卡厂商提供的.lib/.dll/.so,用Git LFS或者干脆放在共享目录里用脚本拉取。配置文件模板要进Git,实际配置不要进,因为现场配置包含IP地址、串口号这些跟环境绑定的信息,每个现场都不一样。
分支策略上,工业项目不适合互联网那套复杂的Git Flow,简单点就好:main分支保持稳定可发布,develop分支做日常开发,每个功能或修复开feature/xxx分支。发布时打tag,tag命名带上版本号和日期,比如v1.2.0-20260315。这样现场出问题时能快速定位是哪个版本。
3. 核心通信模块的实现要点
3.1 串口通信:工业现场的老朋友
串口在工业领域活得比谁都久,RS232、RS485、RS422这些物理层标准几十年了还在大量使用。Qt的QSerialPort模块封装得不错,但工业场景下有几个细节必须处理好。
串口枚举与热插拔。工业现场经常有USB转串口的设备,用户可能先开软件再插设备,也可能中途拔掉。QSerialPortInfo::availablePorts()可以枚举当前可用串口,但要注意这个列表是快照,设备变化后需要重新枚举。我的做法是起一个定时器,每隔一两秒扫描一次串口列表,跟上次对比,有变化就更新界面上的下拉框。这样用户插拔设备后不用重启软件。
数据接收的粘包与断包。串口是字节流,一次readyRead信号触发的数据可能是半条指令,也可能是三条指令粘在一起。绝对不能假设一次read就是一条完整报文。正确的做法是维护一个接收缓冲区,每次收到数据就追加进去,然后按协议格式解析,能解析出完整帧就取走,剩下的留在缓冲区等下次数据。这个逻辑写起来不难,但新手特别容易在这里翻车。
void CommManager::onSerialReadyRead() { m_rxBuffer.append(m_serial->readAll()); while (true) { int frameLen = tryParseFrame(m_rxBuffer); if (frameLen <= 0) break; QByteArray frame = m_rxBuffer.left(frameLen); m_rxBuffer.remove(0, frameLen); processFrame(frame); } // 防止缓冲区无限增长 if (m_rxBuffer.size() > MAX_BUFFER_SIZE) { m_rxBuffer.clear(); emit errorOccurred("接收缓冲区溢出,已清空"); } }超时与重试机制。工业设备响应慢是常态,发一条指令等几百毫秒甚至几秒都正常。但也不能无限等,必须设超时。我的经验是超时时间设为设备典型响应时间的3倍,比如设备通常50ms响应,超时就设150ms。超时后重试,重试次数一般2-3次,连续失败就报警。重试间隔要留够,别把设备问懵了。
3.2 Modbus协议:绕不开的工业标准
Modbus在工业领域的地位不用多说,PLC、仪表、变频器基本都支持。Qt本身没有内置Modbus,但Qt 6提供了QtSerialBus模块,里面有QModbusClient和QModbusServer。不过实际项目里,我更多时候是自己实现Modbus RTU/TCP的编解码,原因有两个:一是QtSerialBus的API有时候不够灵活,二是自己实现能更好地控制超时和重试逻辑。
Modbus RTU的帧格式是:地址(1字节) + 功能码(1字节) + 数据(N字节) + CRC(2字节)。CRC校验是必须的,算错了设备直接不理你。CRC16的计算有个查表法,效率比逐位计算高很多:
quint16 ModbusHelper::calculateCRC(const QByteArray &data) { static const quint16 crcTable[256] = { /* 预计算的表 */ }; quint16 crc = 0xFFFF; for (char byte : data) { crc = (crc >> 8) ^ crcTable[(crc ^ static_cast<quint8>(byte)) & 0xFF]; } return crc; }Modbus TCP就简单多了,没有CRC,帧头是6字节的MBAP头(事务ID、协议ID、长度、单元ID),后面跟PDU。TCP的好处是不用担心粘包问题(TCP本身是流,但Modbus TCP有长度字段,好处理),坏处是网络延迟不可控,超时时间要设得比串口宽松。
寄存器地址的坑。Modbus的寄存器地址有0-based和1-based的区别,还有所谓的"PLC地址"(比如40001表示保持寄存器第一个)。不同厂商的文档写法不一样,有的写40001,有的写0,有的写1。对接新设备时,第一件事就是确认地址基准,否则你会对着一个读不出数据的寄存器调半天。我的做法是在配置界面里让用户明确选择地址基准,而不是自己猜。
3.3 多线程数据采集:别让界面卡死
工业控制系统的数据采集频率可能很高,比如1ms采集一次,如果全在主线程做,界面直接卡成PPT。Qt的多线程方案有几种:QThread子类化、moveToThread、QtConcurrent。工业项目里我推荐moveToThread方式,把通信对象移到一个独立线程,通过信号槽跟主线程交互。
// 创建采集线程 m_commThread = new QThread(this); m_commManager = new CommManager(); m_commManager->moveToThread(m_commThread); // 跨线程信号槽,自动排队 connect(m_commThread, &QThread::started, m_commManager, &CommManager::start); connect(m_commManager, &CommManager::dataReceived, this, &MainWindow::onDataReceived); connect(m_commThread, &QThread::finished, m_commManager, &QObject::deleteLater); m_commThread->start();这里的关键点是跨线程信号槽默认是QueuedConnection,数据会被拷贝到目标线程的事件队列里,所以传大数据量时要注意性能。如果数据量特别大,可以考虑用共享内存或者环形缓冲区,但那是另一个复杂度级别的话题了。
还有一个坑是线程退出时的清理。直接delete一个还在运行的线程对象会崩溃,正确做法是先quit()再wait(),确保线程真正结束。工业软件经常需要长时间运行,线程泄漏或者僵尸线程积累下来,跑几天就出问题。
4. 界面设计与用户体验的工业化改造
4.1 工业HMI的界面设计原则
工业软件的界面跟消费级软件完全是两个审美体系。消费级追求炫酷、动效、渐变,工业软件追求的是信息密度高、状态一目了然、误操作成本低。一个操作工可能盯着屏幕八小时,界面的可读性和舒适度直接影响工作效率和出错率。
配色上,深色背景配高对比度文字是主流,因为车间环境光线复杂,深色背景反光少。状态色要统一且符合直觉:绿色表示正常,黄色表示警告,红色表示故障,灰色表示离线。不要用花哨的颜色,一个界面上超过五种颜色就会显得杂乱。字体用等宽字体显示数值,方便对齐和快速读取。
布局上,重要信息放左上角,因为人眼习惯从左上开始扫描。报警信息要显眼,最好固定在某个区域,不要用弹窗,弹窗会遮挡其他信息且需要手动关闭。按钮要大,工业现场可能戴手套操作,小按钮点不准。危险操作要有二次确认,比如"急停"按钮点下去要弹确认框,但"启动"按钮反而不要确认,因为启动失败可以再点,急停误触代价太大。
4.2 QSS样式表:让Qt界面不那么"原生"
Qt默认的界面风格比较朴素,用QSS(Qt Style Sheets)可以做出接近现代工业软件的视觉效果。QSS语法类似CSS,但选择器是Qt的类名和对象名。
QMainWindow { background-color: #1e1e1e; } QPushButton { background-color: #3c3c3c; color: #e0e0e0; border: 1px solid #555; border-radius: 4px; padding: 8px 16px; min-width: 80px; min-height: 32px; } QPushButton:hover { background-color: #4a4a4a; } QPushButton:pressed { background-color: #2a2a2a; } QPushButton#emergencyBtn { background-color: #c62828; font-weight: bold; } QLabel#statusLabel[status="normal"] { color: #4caf50; } QLabel#statusLabel[status="alarm"] { color: #f44336; }QSS里有个很实用的特性是动态属性选择器,像上面[status="normal"]这样,可以在代码里用setProperty("status", "alarm")动态改变样式,然后调用style()->unpolish()和polish()刷新。这比手动改一堆颜色属性方便多了。
不过QSS也有性能问题,复杂的样式表在频繁刷新时会拖慢界面。工业软件里数据刷新频率高,如果每个数值变化都触发样式重算,CPU占用会飙升。我的经验是,静态样式用QSS,动态变化的部分(比如数值颜色随状态变化)用代码直接设置,或者用自定义绘制。
4.3 数据可视化:图表与仪表盘
工业软件离不开数据可视化,趋势图、仪表盘、状态灯这些是标配。Qt Charts模块提供了基础的图表功能,但说实话,Qt Charts的性能和美观度都一般,数据点多了会卡。如果项目对图表要求高,可以考虑QCustomPlot这个第三方库,性能好很多,API也更灵活。
仪表盘的话,Qt没有内置控件,要么自己用QPainter画,要么用QML做。QML做仪表盘很合适,声明式语法写起来快,动画效果也好。但QML和Widgets混用有坑,两者的事件循环和渲染机制不一样,混用可能导致性能问题或者奇怪的bug。我的建议是要么全Widgets,要么全QML,别混。
如果非要在Widgets项目里嵌入QML,用QQuickWidget,但要注意它是个重量级控件,数量多了会影响性能。工业软件里一般也就一两个仪表盘,问题不大。
5. 数据持久化与报警管理
5.1 数据库选型:SQLite够用吗
工业控制系统的数据存储需求通常是:高频写入、按时间查询、长期保存、单机部署。SQLite在这个场景下其实很合适,零配置、单文件、事务支持完善。但有几个点要注意。
写入频率。SQLite的写入性能在单条插入时大概每秒几百到几千条,如果采集频率是1ms一次,那就是每秒1000条,接近极限了。这时候要用批量插入,攒一批数据(比如100条或者1秒的数据)一次性提交事务,性能能提升一个数量级。
void DataStorage::flushBatch() { if (m_pendingData.isEmpty()) return; m_db.transaction(); QSqlQuery query(m_db); query.prepare("INSERT INTO realtime_data (timestamp, tag, value) VALUES (?, ?, ?)"); for (const auto &record : m_pendingData) { query.addBindValue(record.timestamp); query.addBindValue(record.tag); query.addBindValue(record.value); query.exec(); } m_db.commit(); m_pendingData.clear(); }数据库文件增长。长期运行的系统,数据库文件会越来越大,查询变慢,备份也麻烦。要定期做数据归档,比如把三个月前的数据导出到历史库,主库只保留近期数据。SQLite的VACUUM命令可以回收空间,但会锁库,要在低峰期做。
并发访问。SQLite默认是库级锁,多个线程同时写会冲突。Qt的QSqlDatabase不是线程安全的,每个线程要用自己的连接。工业项目里通常是采集线程写、界面线程读,两个连接,用WAL模式可以提升并发性能:
PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;5.2 报警管理:别让报警变成噪音
工业现场最怕的就是"报警疲劳"——报警太多,操作工麻木了,真正的故障反而被忽略。报警管理要做好几件事。
报警分级。至少分三级:提示(Info)、警告(Warning)、故障(Alarm)。提示类的不弹窗,只在日志里记;警告类的在状态栏显示;故障类的才弹窗加声音。分级标准要跟工艺人员一起定,不能拍脑袋。
报警去重与抑制。同一个报警在短时间内反复触发,只记录第一次,后续的合并计数。比如"温度过高"报警,5分钟内重复触发100次,界面上只显示一条,后面标注"x100"。这样操作工不会被刷屏。
报警确认机制。报警产生后需要操作工确认,确认后报警从活动列表移到历史列表。未确认的报警要一直显示,不能自动消失。有些系统报警响几秒就没了,操作工去处理的时候已经看不到是什么报警了,这是大忌。
struct AlarmRecord { QString id; QDateTime timestamp; AlarmLevel level; QString source; QString message; bool acknowledged; QDateTime ackTime; QString ackUser; };报警数据要持久化,不能只存内存。系统重启后,未确认的报警要恢复显示。这个逻辑看起来简单,但很多项目都忽略了,导致重启后报警丢失。
5.3 日志系统:出问题时能查得清
工业软件出问题时,现场往往没有开发人员,全靠日志排查。日志要记录:谁在什么时候做了什么操作、系统状态变化、通信收发数据、异常和错误。
日志分级用标准的DEBUG/INFO/WARN/ERROR/FATAL。生产环境一般开INFO级别,DEBUG只在排查问题时临时开。日志文件要按大小或日期滚动,不然跑几个月日志文件几个G,打开都费劲。
Qt里可以用qInstallMessageHandler自定义日志处理,把qDebug/qWarning这些重定向到文件:
void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { static QMutex mutex; QMutexLocker locker(&mutex); QString level; switch (type) { case QtDebugMsg: level = "DEBUG"; break; case QtInfoMsg: level = "INFO"; break; case QtWarningMsg: level = "WARN"; break; case QtCriticalMsg: level = "ERROR"; break; case QtFatalMsg: level = "FATAL"; break; } QString logLine = QString("[%1] [%2] %3") .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz")) .arg(level) .arg(msg); Logger::instance().write(logLine); }日志里不要记录敏感信息,比如密码、密钥。工业项目虽然不像互联网那样有严格的安全审计,但基本的规范还是要有的。
6. 部署与现场调试的实战经验
6.1 打包发布:让软件在裸机上跑起来
开发机上跑得好好的软件,拷到现场工控机上打不开,这是新手最常遇到的问题。原因通常是依赖库缺失。Qt程序依赖一堆动态库,Windows下用windeployqt工具可以自动拷贝依赖:
windeployqt --release --no-translations --no-system-d3d-compiler \ --no-opengl-sw IndustrialControl.exe但windeployqt不是万能的,第三方库(比如你自己链接的板卡SDK)它不认识,要手动拷。还有VC运行库,现场工控机可能没装,要么静态链接运行库,要么把vc_redist一起打包。
Linux下用linuxdeployqt或者手动用ldd查依赖。嵌入式ARM板的话,交叉编译后要把Qt库也部署到板子上,注意库的路径要跟编译时一致,否则运行时找不到。
安装包制作。工业软件一般用Inno Setup(Windows)或者deb/rpm包(Linux)。安装包里要包含:主程序、依赖库、配置文件模板、驱动(如果需要)、说明文档。安装路径让用户可选,但默认路径不要带空格。
6.2 现场调试:那些文档里不会写的事
现场调试是工业项目最考验人的环节。分享几个我踩过的坑。
串口被占用。现场工控机上可能装了其他软件,把你要用的串口占着。打开串口失败时,错误信息要明确,别只报"打开失败",要告诉用户是"串口COM3被占用"还是"串口不存在"。
电磁干扰。车间里变频器、电机一开,串口通信就出错。这是物理层问题,软件层面能做的是增加校验和重试。如果误码率太高,要考虑换屏蔽线、加磁环、降低波特率。
工控机时间不准。有些工控机的RTC电池没电了,每次开机时间都是错的,导致日志时间戳混乱。软件启动时要检查系统时间,如果明显不对(比如比编译时间还早),要提示用户校准。
屏幕分辨率适配。现场工控机的分辨率五花八门,从1024x768到1920x1080都有。界面布局要用布局管理器,不要用绝对坐标。字体大小要能自适应,或者提供缩放选项。
6.3 长期运行的稳定性保障
工业软件要求7x24小时运行,稳定性是第一位。几个关键点。
内存泄漏排查。Qt对象树机制能自动管理大部分内存,但new出来的没有parent的对象要手动delete。长期运行的程序,哪怕每次泄漏几字节,跑几个月也会出问题。用Valgrind(Linux)或者Dr. Memory(Windows)定期检查。
异常处理。C++异常在Qt里要小心用,跨信号槽抛异常是未定义行为。工业软件里我倾向于少用异常,多用错误码,每个可能失败的操作都要检查返回值。
看门狗机制。主线程起一个定时器,定期更新一个标志。如果某个操作卡死了,定时器不触发,就说明有问题,可以记录日志甚至重启。这个机制在无人值守的场景下很有用。
资源监控。CPU、内存、磁盘、句柄数,这些指标要定期记录。如果发现内存持续增长、句柄数只增不减,就是泄漏的信号,要提前处理,别等崩溃了再查。
7. 常见问题速查与避坑清单
7.1 编译与链接问题
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| undefined reference to vtable | 有虚函数没实现,或moc没跑 | 检查虚函数实现,确认AUTOMOC开启 |
| 找不到Qt头文件 | include路径没配 | 检查CMake的target_link_libraries |
| 链接第三方库报错 | 库版本不匹配(32/64位、Debug/Release) | 确认库的架构和编译模式一致 |
| 运行时提示缺少dll | 依赖库没打包 | 用windeployqt或手动拷贝 |
| 中文乱码 | 源码编码或运行环境编码问题 | 源码用UTF-8,Windows下设置QTextCodec |
7.2 通信类问题
| 问题现象 | 排查方向 | 经验技巧 |
|---|---|---|
| 串口打不开 | 被占用、权限、驱动 | Linux下检查/dev/ttyUSB*权限,加入dialout组 |
| 数据收不全 | 粘包断包处理不当 | 用缓冲区累积,按协议解析 |
| 通信时好时坏 | 电磁干扰、线缆质量 | 换屏蔽线,降低波特率,加校验重试 |
| Modbus读不到数据 | 地址基准、功能码、字节序 | 确认0-based还是1-based,确认大小端 |
| TCP连接频繁断开 | 心跳缺失、网络不稳 | 加心跳包,设置SO_KEEPALIVE |
7.3 界面与性能问题
| 问题现象 | 原因分析 | 优化方案 |
|---|---|---|
| 界面卡顿 | 主线程做耗时操作 | 耗时操作移到工作线程 |
| 内存持续增长 | 对象未释放、信号槽未断开 | 检查new的对象,用deleteLater |
| 图表刷新慢 | 数据点太多、重绘频繁 | 限制显示点数,降低刷新频率 |
| 启动慢 | 初始化太多、加载大资源 | 延迟初始化,资源按需加载 |
| 高DPI显示模糊 | 缩放适配问题 | 设置Qt::AA_EnableHighDpiScaling |
7.4 独家避坑心得
关于信号槽。信号槽是Qt的精华,但滥用会出问题。不要在信号槽里做耗时操作,因为默认是直连的话会阻塞发送方。跨线程的信号槽要注意对象的生命周期,接收方销毁了但信号还在发,会崩溃。用QPointer或者disconnect处理。
关于单例。工业项目里配置管理、日志、数据库连接这些常用单例。但单例的初始化顺序是不确定的,不要在单例构造函数里依赖另一个单例。我的做法是用instance()函数里做懒初始化,第一次调用时才创建。
关于配置文件。用QSettings还是自己解析JSON/INI?QSettings跨平台方便,但Windows下写注册表,现场部署时清理不干净。我倾向于用JSON文件,路径放在程序目录下的config文件夹,备份和迁移都方便。
关于升级。工业软件升级是个麻烦事,现场可能几十台设备要升级。做个自动更新模块,检查服务器上的版本,下载新版本,校验后替换。但要注意,升级过程中不能影响正在运行的控制逻辑,最好支持热更新或者分模块更新。
关于测试。工业软件没法像互联网那样频繁上线测试,所以单元测试和集成测试特别重要。通信协议解析、数据计算、状态机这些核心逻辑,一定要有测试用例。用Qt Test框架,CI里跑一遍,能避免很多低级错误。
关于文档。代码写得再好,没文档也是灾难。至少要有:部署文档(怎么装)、配置文档(怎么配)、操作手册(怎么用)、故障排查手册(出问题怎么办)。文档跟代码一起版本控制,别等到交付前才补。
工业控制系统的开发是个系统工程,Qt只是工具,真正决定项目成败的是对工业场景的理解和对细节的把控。我见过太多技术很牛但不懂现场的团队,做出来的东西操作工不愿意用。反过来,懂现场但技术粗糙的,做出来的东西不稳定。两者结合,才能做出真正好用的工业软件。