前言
很多Qt工控开发者都有过这样的经历:项目启动时只有一个主窗口。刚开始需求很简单:串口采集数据、显示实时曲线、保存历史记录、控制设备启停。于是大家都会选择最直接的写法:
MainWindow├── SerialPort├── TcpClient├── Camera├── Database├── ChartWidget├── VisionAlgorithm└── ConfigManager
所有功能全部塞进 MainWindow。前期开发速度飞快——一个月完成Demo,三个月完成上线。半年以后……灾难开始了。
单体架构为什么会失控?
曾经接手过一个工业视觉项目,主程序代码量 MainWindow.cpp 达到 16874 行,整个软件只有一个入口,所有模块都直接互相调用(如 CameraWidget → VisionManager → DatabaseManager → SerialManager → AlarmWidget)。看起来方便,实际上形成了蜘蛛网式依赖,任何一个模块修改都牵一发而动全身。
问题一:修改一个Bug引发十个Bug
例如修改相机采集逻辑 CameraManager::capture(),结果视觉检测异常、数据库记录错误、UI刷新异常全来了——因为所有模块直接依赖,根本无法确定影响范围。
问题二:多人开发频繁冲突
项目5个开发人员,每天提交 MainWindow.cpp / .h,Git冲突成为日常,甚至出现 解决冲突时间 > 开发时间。
问题三:新增功能成本越来越高
客户提出新增AI缺陷检测模块,原本以为增加一个模块即可,结果发现需要修改主窗口、数据库、通信模块、参数配置、报警模块等 40+ 文件,风险极高。
问题四:长期运行不稳定
工业软件要求 7×24小时运行(连续一周、一月、半年不能崩溃),而单体程序最容易出现内存泄漏、资源未释放、线程失控、句柄耗尽——因为所有模块混在一起,无法隔离问题。
工控软件真正需要什么架构?
工业软件与互联网软件不同:互联网追求高并发、快速迭代;工业软件要求 稳定第一、可扩展第二、性能第三。因此优秀工控软件必须满足:
可插拔:新增功能不修改主程序,直接增加模块。
可隔离:某模块异常不影响其它模块。
易维护:开发5年后依然有人敢改代码。
支持多人协作:算法/设备/前端工程师各司其职,互不影响。
工业级插件架构设计
目前很多工业软件(海康VisionMaster、大华SmartPSS、LabVIEW、Qt Creator)底层都采用插件化架构:
┌─────────────────────┐│ 主程序Core │└─────────┬───────────┘│┌────────┴────────┐│ Plugin Manager │└────────┬────────┘│┌────────┼────────┐│ │ │▼ ▼ ▼通信插件 算法插件 UI插件Serial Vision Report▼ ▼ ▼DLL DLL DLL
主程序只负责插件加载/卸载、消息转发、生命周期管理,绝不包含业务逻辑。
第一层:核心基座 Core
Core 负责插件扫描、注册、加载、卸载、日志管理、配置管理。例如:
class PluginManager {public:voidloadPlugins();voidunloadPlugins();};
plugins/ 目录(Camera.dll, Vision.dll, Database.dll, Alarm.dll)并动态加载:QDir dir("plugins");QPluginLoader loader(path);
第二层:标准接口层
插件之间绝不能直接引用,必须统一接口:
class IPlugin {public:virtual QString name() = 0;virtualboolinitialize() = 0;virtualvoidshutdown() = 0;};class CameraPlugin : public QObject, public IPlugin { ... };
第三层:消息总线
很多项目失败是因为插件之间仍然互相调用(如 CameraPlugin 直接调用 vision->process())。正确做法是使用消息总线:
// 发布MessageBus::publish("ImageCaptured", image);// 订阅MessageBus::subscribe("ImageCaptured", callback);
视觉插件自动接收 void VisionPlugin::onImageCaptured(...)。这样相机不知视觉存在,视觉不知数据库存在,数据库不知报警存在,完全解耦。
设备层插件设计
工控软件最大的复杂度来自设备(PLC、相机、激光器、扫码枪、机械臂、IO板卡)。建议 一个设备一个插件(如 SiemensPLC.dll, HikCamera.dll),统一接口:
class IDevice {public:virtualboolconnect() = 0;virtualvoiddisconnect() = 0;virtual QVariant read() = 0;};
未来更换设备无需修改业务代码。
算法层插件设计
视觉项目算法最容易变化(模板匹配 → YOLO → OCR),必须插件化:
Algorithm/ OCR.dll, YOLO.dll, Measure.dll, Defect.dllclassIAlgorithm{public:virtual Result process(const cv::Mat&) = 0;};
实现热插拔。
UI层插件化
Qt Creator 就是典型案例——每个页面独立插件(Dashboard.dll, Alarm.dll, Report.dll, Config.dll),加载后动态生成:
QWidget* createWidget();stackWidget->addWidget(pluginWidget);
数据存储层设计
不要让业务模块直接访问数据库(错误:VisionPlugin 内写 QSqlQuery)。正确做法是统一 DataService 接口:
classIDataService{ virtual void saveResult(); };数据库插件(SQLite/MySQL/PostgreSQL/InfluxDB)负责实现,切换无需修改业务代码。
线程架构设计
工业软件第二大坑是线程乱飞(常见 30+ QThread 无人管理,导致内存泄漏、死锁、崩溃)。推荐使用 QThreadPool 统一管理,任务通过 QRunnable 提交执行。
日志系统设计
必须具备可追溯性,统一日志中心记录 时间、线程、模块、级别、内容,例如:
2026-06-18 [Vision] OCR Failed快速定位问题。
配置中心设计
不要到处使用 QSettings,统一 ConfigManager 支持 JSON/YAML/XML/数据库,所有模块统一读取。
一个成熟工控架构长什么样?
Application├── Core│ ├── PluginManager│ ├── MessageBus│ ├── LogManager│ └── ConfigManager├── Devices│ ├── PLC│ ├── Camera│ └── IO├── Algorithms│ ├── OCR│ ├── AI│ └── Measure├── UI│ ├── Dashboard│ ├── Alarm│ └── Report└── Data├── SQLite├── MySQL└── InfluxDB
所有模块独立开发、独立测试、独立部署、独立升级。
总结
很多Qt工控项目后期维护成本越来越高,不是因为Qt不好或代码能力不足,而是从第一天就选错了架构。单体架构初期虽快,但终会演变成无人敢改的“巨石系统”。对于需要长期运行、持续迭代的工业软件,更合理的选择是工业级插件化架构:
Core 基座负责系统管理
Plugin 实现业务功能
MessageBus 实现模块解耦
Interface 规范模块边界
ThreadPool 管理并发任务
LogCenter 保证问题可追踪
这样设计的软件,不仅能满足 7×24 小时稳定运行,也能在未来几年持续扩展,而非新增一个功能就推翻重构。优秀的工控软件从来不是功能堆出来的,而是架构设计出来的。
夜雨聆风