
在写第一行代码之前,有两个决策会影响整个项目的走向,且后期极难更改。
第一,Widgets 还是 QML。
维度 | QWidget | QML/Qt Quick |
|---|---|---|
适合场景 | 桌面工具、表单密集、传统业务系统 | 触屏、动效丰富、嵌入式 HMI |
语言 | 纯 C++ | QML(声明式) + C++ 后端 |
复杂表格/树 | 成熟稳定 | 需额外投入 |
动画与自绘 | 需手写 paintEvent | 天然支持 |
团队门槛 | C++ 即可 | 需额外学 QML |
判断标准很直接:界面以"数据录入与展示"为主,选 Widgets;以"交互体验与动效"为主,选 QML。 不要因为 QML"看起来现代"就盲目选它——一个表单密集的管理系统用 QML 写,工作量可能是 Widgets 的两倍。
第二,构建系统选 CMake。
qmake 已进入维护状态,新项目没有理由再用。CMake 是 Qt 6 的官方推荐,与生态工具链(Conan、vcpkg、CI)集成更好。这个决策没有争议空间。
Qt 项目最常见的架构病是业务逻辑长在窗口类里。一个 MainWindow.cpp 动辄两千行,数据库查询、网络请求、界面刷新混在一起,既不能测,也不能复用。
合理的分层大致是四层:
关键约束只有一条:Domain 层不允许 include 任何 QtWidgets 或 QtQuick 头文件。 这条约束一旦守住,业务逻辑就能脱离界面单独测试,这是 Qt 项目可维护性的分水岭。
依赖方向应当单向向下,上层通过接口依赖下层,而不是直接 new 具体实现。下面是一个最小的接口骨架:
// domain/order_service.h —— 纯 C++,不依赖 GUI
class OrderService {
public:
virtual ~OrderService() = default;
virtual Result<void> submit(const Order& order) = 0;
virtual QVector<Order> query(const Filter& filter) = 0;
};
// ui/order_page.cpp —— UI 只依赖接口
class OrderPage : public QWidget {
Q_OBJECT
public:
explicit OrderPage(OrderService* svc, QWidget* parent = nullptr)
: QWidget(parent), m_svc(svc) {}
private slots:
void onSubmitClicked();
private:
OrderService* m_svc; // 不拥有,由上层注入
};这段代码的重点不在写法,而在依赖注入的方向:UI 持有接口指针,不关心背后是数据库还是 mock。测试时传一个假实现即可,不需要启动界面。
Qt 有一套独特的对象所有权模型:QObject 的父子关系会在父对象析构时自动删除子对象。 这套机制很方便,但和 C++ 的 RAII、智能指针混用时极易出事。
几条必须守住的纪律:
第一,明确每个对象的归属。 要么交给 Qt 父子树管理(new QWidget(this)),要么交给智能指针管理(std::unique_ptr),不要两种混用。一个对象既挂了 parent 又被 unique_ptr 持有,必然双重释放。
第二,栈对象不要设 parent。 QWidget w; w.setParent(p); 是典型的崩溃源头——栈对象析构时 Qt 已经把它从父子树里删了一次。
第三,跨线程传递对象要小心。 QObject 有线程亲和性,一个对象只能在它所属的线程被操作。跨线程通信用信号槽的队列连接,而不是直接调用方法。
第四,析构顺序很重要。 如果 A 在析构时还要访问 B,那 B 必须比 A 活得久。用父子关系时,Qt 的删除顺序是未定义的,不能依赖。
一个实用建议是:项目里统一约定所有权规则并写进规范,比如"所有 QObject 派生类一律用父子树管理,非 QObject 资源一律用 RAII 包装"。规则统一,崩溃就少一大半。
信号槽是 Qt 的灵魂,也是最容易被滥用的地方。常见病症:
几条实践原则:
连接类型要明确。 Qt::AutoConnection 在跨线程时会自动变成队列连接,但依赖"自动"容易埋雷。跨线程场景显式写 Qt::QueuedConnection,意图更清楚。
连接要可断开、可管理。 用 QMetaObject::Connection 保存连接句柄,在对象销毁或状态切换时主动断开,而不是依赖自动断开。
优先用新式语法。 函数指针式连接(connect(a, &A::sig, b, &B::slot))编译期就能检查签名,比字符串式 SIGNAL/SLOT 宏安全得多。
信号只承载"发生了什么事",不承载业务逻辑。 槽函数里做业务,信号本身保持轻量。
一段典型的跨线程骨架:
// 工作对象在子线程,结果通过队列连接回主线程
Worker* worker = new Worker; // 无 parent,稍后 moveToThread
QThread* thread = new QThread(this); // 由 this 管理生命周期
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Worker::run);
connect(worker, &Worker::finished, this, &Controller::onFinished,
Qt::QueuedConnection); // 显式跨线程
connect(worker, &Worker::finished, thread, &QThread::quit);
connect(thread, &QThread::finished, worker, &QObject::deleteLater);
thread->start();这段代码里几个细节值得注意:worker 不能有 parent(否则无法 moveToThread)、用 deleteLater 而非 delete(在正确的线程析构)、线程对象由 this 管理(避免泄漏)。
Qt 的跨平台是卖点,但工程上要真正做到"一次编写、处处构建",需要一些纪律。
用 CMake 统一构建。 一份 CMakeLists.txt 覆盖 Windows/macOS/Linux,平台差异用生成器表达式或条件分支处理,而不是维护多份工程文件。
依赖管理要显式。 Qt 6 的 find_package(Qt6 REQUIRED COMPONENTS Widgets Network Sql) 明确列出用到的模块,避免隐式依赖。第三方库建议用 Conan 或 vcpkg 管理,锁版本、可复现。
平台相关代码集中隔离。 文件路径、注册表、系统托盘、原生句柄这些差异,统一放进 platform/ 目录,用编译宏或接口隔离,业务代码里不出现 #ifdef Q_OS_WIN。
CI 覆盖所有目标平台。 至少做到三个平台都能编译通过、核心单元测试通过。很多"跨平台"项目实际只在开发者自己的机器上验证过,一旦进 CI 就暴露。
一个最小的 CMake 骨架:
cmake_minimum_required(VERSION 3.21)
project(myapp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON) # 元对象编译器
set(CMAKE_AUTORCC ON) # 资源文件
find_package(Qt6 REQUIRED COMPONENTS Widgets Network Sql)
add_library(domain STATIC src/domain/order_service.cpp) # 无 Qt GUI 依赖
target_link_libraries(domain PRIVATE Qt6::Core) # 仅需 Core
qt_add_executable(myapp src/main.cpp src/ui/order_page.cpp)
target_link_libraries(myapp PRIVATE domain Qt6::Widgets Qt6::Network Qt6::Sql)注意 domain 库只链接 Qt6::Core——这是分层约束在构建系统里的落地。编译期就能发现越界依赖,比代码评审更可靠。
资源文件。 图标、样式表、QML 用 .qrc 打进二进制,避免运行时找文件路径。但要注意:大资源会撑大可执行文件,且修改资源需要重新编译。频繁变动的资源(如可换肤的图片)应放外部目录。
国际化。 从一开始就用 tr() 包住所有用户可见文本,哪怕暂时只有一种语言。后期补 tr() 的成本远高于一开始就写。用 lupdate/lrelease 生成 .ts/.qm,切换语言时动态加载 QTranslator。
配置管理。 QSettings 适合存用户偏好,但不要用它存业务配置。业务配置应该有明确的 schema、版本号和校验,放在独立配置文件里。
QTableWidget 适合小数据量,上万行必须换 QTableView + QAbstractTableModel,只渲染可见区域。paintEvent 里做复杂计算;用 update() 而非 repaint();合理设置 WA_OpaquePaintEvent 等属性。blockSignals(true) 或用 QSignalBlocker,更新完再恢复。性能优化的前提是先测量再动手。Qt 自带 QElapsedTimer,配合各平台的 profiler(perf、Instruments、VTune),定位到真正的热点再优化,而不是凭感觉改代码。
Qt 项目的测试分三层:
QTest::mouseClick 等模拟交互。关键是Domain 层要能被测——这也是前面坚持分层隔离的原因。如果业务逻辑全在窗口类里,测试就只能靠手动点点点,重构时没人敢动代码。
Qt 应用的部署有几个固定动作:
windeployqt 收集依赖 DLL,注意 MSVC 运行时和平台插件目录。macdeployqt 生成 .app,签名与公证(notarization)是上架 App Store 或分发的必要步骤。版本号与更新机制要提前设计。 至少要有明确的版本号规则、升级包格式、回滚方案。桌面应用的自动更新比 Web 复杂得多,早规划省事。
崩溃收集。 接入 Breakpad/Crashpad 或平台自带方案,收集堆栈。Qt 应用的崩溃很多发生在事件循环里,没有堆栈几乎无法定位。
#ifdef 满天飞,跨平台成了笑话。tr():后期国际化成本翻倍。C++/Qt 项目的工程方法论可以浓缩成四句话:分层隔离、所有权明确、构建统一、测试先行。
Qt 提供了很好的基础设施——元对象、跨平台抽象、成熟的控件库。但这些只是原材料,能不能盖成能长期住的房子,取决于架构决策:UI 和业务分不分得开、对象归属清不清楚、跨平台是不是真的跨、重构时有没有测试兜底。
框架决定你能写多快,架构决定你能维护多久。 前者几个月就能学会,后者要靠一个个项目踩出来——而避开上面这些坑,能省掉其中大半。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。