2. Qt工业项目工程化目录与构建体系
工业上位机项目往往从“能跑起来”开始,却在设备型号增多、通信协议扩展、界面改版和现场部署频繁之后迅速失控。根源通常不在业务代码,而在工程结构:源文件堆在一个目录、UI 与通信逻辑互相引用、资源路径写死、版本号散落在多个文件、发布时靠手工拷贝 DLL。工程化目录与构建体系要解决的正是这些问题——让代码边界清晰、构建可重复、发布可追溯。
一、按职责划分目录,而不是按文件类型
常见的错误做法是建立 widgets/、dialogs/、utils/ 这类按文件类型划分的目录,结果是通信代码和界面代码混在同一个 utils 里,依赖方向无法约束。工业上位机更适合按职责与依赖层次划分:
app/:程序入口、主窗口装配、启动参数解析、单实例检测。它是唯一允许同时依赖 core、comm、ui 的模块。core/:与界面无关的领域模型、设备状态机、数据缓存、报警判定、配置读写。不依赖 Qt Widgets,只依赖 QtCore。comm/:通信抽象层。定义ITransport接口,下挂 Modbus TCP、串口、OPC UA 等具体实现,向上只暴露“请求—响应”和“数据到达”信号。ui/:窗口、控件、视图模型。只依赖 core 暴露的数据结构和信号,不直接调用 comm 的套接字。test/:单元测试与集成测试,覆盖 core 的状态机、comm 的报文编解码。resources/、translations/、deploy/:资源、翻译与部署脚本。
依赖方向必须单向:app → ui → core,app → comm → core。core 不反向依赖任何上层模块。这条规则一旦确立,core 就可以脱离界面单独编译和测试,通信协议替换也不会波及界面代码。
二、用 CMake 组织多目标构建
Qt 6 项目推荐 CMake。核心思路是把每个目录编译成独立目标(库或可执行文件),再用 target_link_libraries 显式声明依赖,让编译器帮助检查边界。顶层 CMakeLists.txt 负责工程级设置:
cmake_minimum_required(VERSION 3.21)
project(IndustrialHMI VERSION 1.4.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
find_package(Qt6 REQUIRED COMPONENTS Core Widgets SerialPort Network LinguistTools)
add_subdirectory(core)
add_subdirectory(comm)
add_subdirectory(ui)
add_subdirectory(app)
enable_testing()
add_subdirectory(test)
各子目录用 add_library 生成静态库,并声明自己的依赖。例如 comm/CMakeLists.txt:
add_library(hmi_comm STATIC
modbus_tcp_transport.cpp
serial_transport.cpp
transport_factory.cpp)
target_include_directories(hmi_comm PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(hmi_comm
PUBLIC hmi_core
PRIVATE Qt6::SerialPort Qt6::Network)
这里 PUBLIC hmi_core 表示使用 comm 的目标也会自动获得 core 的头文件路径,而 PRIVATE Qt6::SerialPort 表示串口依赖不向外传播。这种 PUBLIC/PRIVATE 的区分正是工程化构建的价值:接口依赖与实现依赖被明确分开。
三、版本号、资源与翻译的统一管理
版本号只应在顶层 project() 中定义一次,然后通过 configure_file 生成头文件供 C++ 使用:
configure_file(
${CMAKE_SOURCE_DIR}/cmake/version.h.in
${CMAKE_BINARY_DIR}/generated/version.h)
# version.h.in 内容
#define APP_VERSION "@PROJECT_VERSION@"
#define APP_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
资源文件用 .qrc 集中管理,避免在代码里写相对路径。工业界面常需要多语言,翻译文件在 CMake 中声明后由 lupdate 提取、lrelease 生成 .qm:
qt_add_translations(IndustrialHMI
TS_FILES translations/hmi_zh_CN.ts
translations/hmi_en_US.ts)
运行时通过 QTranslator 加载对应 .qm,语言切换后调用 retranslateUi() 刷新界面文本。
四、部署脚本与发布检查
Windows 下用 windeployqt 收集 Qt 运行时,Linux 下用 linuxdeployqt 或手工打包。部署脚本应固定输出目录结构,便于现场升级:
# deploy/package.sh(示例)
cmake --build build --config Release
windeployqt --release --no-translations build/app/IndustrialHMI.exe
mkdir -p dist/IndustrialHMI/{bin,config,translations,logs}
cp build/app/IndustrialHMI.exe dist/IndustrialHMI/bin/
cp build/translations/*.qm dist/IndustrialHMI/translations/
cp config/default.ini dist/IndustrialHMI/config/
project() 一致、.qm 是否齐全、配置文件是否随包、日志目录是否可写。这四项是现场部署最常见的返工点。把目录边界、构建目标、版本来源和部署产物固定下来之后,后续增加一种通信协议只需在 comm/ 下新增实现并注册到工厂,界面改动只影响 ui/,而 core 的测试始终可以独立运行。工程化不是额外负担,而是让工业上位机在长期维护中保持可控的前提。