Qt 6 CMake 项目搭建全攻略:从零到完整模板与避坑指南
这两年明显感觉到身边的开发者在从 Qt 5 往 Qt 6 迁移的时候卡住的第一个坎基本都是 CMake。倒不是说 CMake 本身有多难而是 Qt 6 把 CMake 从可选方案变成了官方主推方案之后很多原本靠 qmake 自动处理的东西现在需要你自己在构建脚本里显式声明。第一次接触这套流程确实容易懵AUTOMOC是干嘛的find_package为什么要指定组件为什么我照着网上的旧教程写编译就是过不去这篇文章就把我踩过的坑和最终沉淀下来的模板一起分享出来用 Qt 6 从零搭一个 CMake 项目每一步都给你说清楚为什么。这篇文章适合两类人一类是完全没接触过 CMake、想直接上手 Qt 6 的初学者另一类是从 Qt 5 / qmake 迁过来的老人想搞清楚 Qt 6 的 CMake 写法到底变了什么。我会从环境准备开始带你把一个带 UI 文件、资源文件、翻译文件的完整模板跑起来最后再把常见的编译错误和解决思路整理成对照表。1. 为什么要用 CMakeQt 6 时代的构建逻辑变了1.1 从 qmake 到 CMake 的必然性Qt 刚开始火的那几年qmake 是绝对的主流。它的语法简单写起来也快一个.pro文件几十行就能搞定一个项目。但随着 Qt 模块越来越多、跨平台需求越来越强、大型项目对构建系统的要求越来越高qmake 的局限性开始暴露自定义构建步骤比较别扭、对第三方库的集成不够灵活、IDE 支持也相对单一。Qt 6 发布的时候官方直接把 CMake 定为了主要构建系统。这不是拍脑袋做的决定社区里大量项目早就用 CMake 组织跨平台构建了配合 Ninja、Clangd、VSCode 这些工具链开发体验确实比 qmake 时代顺畅不少。而且 CMake 本身的生态足够成熟很多 C/C 项目都在用它学一次能用在多个地方。我个人的体会是qmake 适合一个人快速写小工具但一旦项目要接 CI、要跨平台打包、要引入复杂的外部依赖CMake 的优势就非常明显。Qt 6 的方向已经定了与其抵触不如直接把 CMake 的思维模式建立起来。1.2 依赖处理方式的升级qmake 时代你想用 Qt 的某个模块基本就是在.pro里写一行QT widgets然后 qmake 自动帮你搞定 include 路径、库文件、moc 工具调用什么的简单粗暴。CMake 下有一套更显式的逻辑find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) target_link_libraries(app PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets)这两行的意思很明确先让 CMake 找到 Qt 6 的安装位置声明你需要的组件再把具体的目标库链接到你的可执行文件上。好处是依赖关系非常透明——你这个项目到底用了 Qt 的哪些部分扫一眼就知道。坏处是如果你漏写了某个组件编译期就会报一堆找不到头文件或者链接错误新手容易在这里卡住。这里有一个非常容易忽略的细节Qt 6 的 CMake 包名是Qt6不是Qt。网上很多抄来的旧教程写的find_package(Qt5 ...)或者find_package(Qt ...)在 Qt 6 环境下一律失效。我见过太多人卡在这一行报错半天找不到原因。1.3 构建目录与源目录分离CMake 有个核心理念叫out-of-source build也就是在单独的构建目录里执行编译从不清洗源码目录。这点和 qmake 的那种在源码目录原地生成 Makefile 的方式很不一样。mkdir build cd build cmake .. -DCMAKE_BUILD_TYPEDebug cmake --build .好处相当实在源码目录始终干净不会被生成的临时文件污染。想切 Release / Debug 配置开两个构建目录就行互不干扰。删掉build目录就等于彻底清理不用担心残留文件影响下次构建。我习惯把构建目录直接命名为build-debug和build-release需要哪种配置就进哪个目录执行一次 CMake 配置。刚开始用 CMake 的同学可能会觉得多了一个步骤很麻烦但实际上这能避免很多清理重编的坑。2. 环境准备与 Qt 6 工具链安装要点2.1 版本选择与安装我目前的推荐组合是 Qt 6.5 LTS 或更高版本配 CMake 3.21。如果你的系统包管理器里有 Qt 6优先用系统包管理器装如果版本太旧或者没有就去 Qt 官方安装程序装。这里有一点要注意安装时务必勾选 CMake 和 Ninja 工具装完直接就能用省去一堆环境变量配置的麻烦。安装完成之后确认一下 Qt 的 CMake 目录是否被正确识别。Linux 下常见的位置是/opt/Qt/6.5.x/gcc_64/lib/cmake/Qt6Windows 下是C:\Qt\6.5.x\msvc2019_64\lib\cmake\Qt6。CMake 通过这个目录里的配置文件定位 Qt 的库和工具如果find_package找不到 Qt九成是路径没配对。2.2 编译器选择与工具链Qt 6 对编译器版本有明确要求太老的编译器编译出来的代码可能直接链接失败。以我常用的组合为例Windows 那边用 MSVC 2019 或 2022配 Qt 的 msvc2019_64 / msvc2022_64 前缀包。Linux 这边用 GCC 11 或 Clang 14配合 Qt 的 gcc_64 前缀包。macOS 那边用 Xcode 自带的 Clang 就行。这里最忌讳的是混用编译器。比如你用 GCC 编译 Qt 库然后试图用 Clang 链接你的项目大概率会报一堆乱七八糟的错误。装好 Qt 之后建议先跑一下 Qt Creator 自带的示例项目确认工具链能正常编译再开始自己的项目免得后面排查问题时怀疑人生。2.3 一个最小可运行模板为了验证环境没有问题我一般先建一个最最基础的项目跑通整个流程。这个模板结构非常简单myapp/ ├── CMakeLists.txt └── main.cppmain.cpp里面就写一个创建空窗口的代码#include QApplication #include QWidget int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget window; window.resize(800, 600); window.show(); return app.exec(); }CMakeLists.txt就三行核心配置cmake_minimum_required(VERSION 3.21) project(MyApp VERSION 1.0.0 LANGUAGES CXX) find_package(Qt6 REQUIRED COMPONENTS Widgets) qt_add_executable(myapp main.cpp) target_link_libraries(myapp PRIVATE Qt6::Widgets)配置和构建cmake -B build -DCMAKE_BUILD_TYPEDebug cmake --build build能弹出一个空白窗口说明你的 Qt 6 和 CMake 环境没问题可以往下走了。3. 第一个标准 Qt 6 CMake 项目从空窗口到完整模板3.1 项目结构设计一个标准的 Qt Widgets 项目我习惯组织成这样的目录结构myapp/ ├── CMakeLists.txt ├── src/ │ ├── main.cpp │ ├── mainwindow.cpp │ ├── mainwindow.h │ └── mainwindow.ui ├── resources/ │ └── resources.qrc └── translations/ └── myapp_zh_CN.tssrc目录放源代码和 UI 文件resources目录放资源文件translations目录放翻译文件。这样的好处很明显项目变大之后每个目录的职责非常清楚CMake 配置也可以针对性地添加规则。3.2 完整 CMakeLists.txt 逐行拆解这个是核心中的核心我把用了很久的模板完整贴出来然后一行一行解释cmake_minimum_required(VERSION 3.21) project(MyApp VERSION 1.0.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 Gui Widgets) qt_add_executable(myapp src/main.cpp src/mainwindow.cpp src/mainwindow.h src/mainwindow.ui resources/resources.qrc ) target_include_directories(myapp PRIVATE src) target_link_libraries(myapp PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets)前两行不用多说最低 CMake 版本保护和项目声明。接着是三个CMAKE_AUTO*变量这三个变量是 Qt 项目在 CMake 下能自动工作的关键。CMAKE_AUTOMOC会自动扫描源文件里的Q_OBJECT宏对涉及信号槽和元对象系统的头文件调用 moc 工具生成对应代码CMAKE_AUTOUIC会自动把.ui文件转换成可以编译的 C 代码CMAKE_AUTORCC会自动编译.qrc资源文件。这三项不开的话Qt 项目在 CMake 里会非常痛苦你需要手动去写自定义命令生成相关文件新手完全没必要碰。find_package不仅是在找 Qt 库也是在引入 Qt 的 CMake 辅助函数。只有调用了find_package(Qt6 ...)后面才可以用qt_add_executable这种 Qt 官方提供的函数。为什么这里用qt_add_executable而不是普通的add_executable核心原因是qt_add_executable比普通函数多做了一步——它会自动处理Q_OBJECT头文件的 moc。虽然CMAKE_AUTOMOC ON也能兜底但qt_add_executable能在 UI 文件、QML 文件等更多场景下给出正确的预设行为。按照 Qt 官方的推荐只要是 Qt 项目就用qt_add_executable不要退回去用原生的add_executable。最后是target_include_directories和target_link_libraries。前者告诉编译器src目录下的头文件在哪里找后者把 Qt 库链接进来。都是 PRIVATE 可见性因为它们只需要在这个可执行文件内部使用不需要暴露给依赖它的目标。3.3 一个带窗口和按钮的实操示例上面的模板跑通后我们来做一个稍微像样点的例子主窗口里放一个按钮点击后弹出一个消息框。需要三个文件mainwindow.h、mainwindow.cpp、main.cpp。mainwindow.h#ifndef MAINWINDOW_H #define MAINWINDOW_H #include QMainWindow QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent nullptr); ~MainWindow(); private slots: void on_helloButton_clicked(); private: Ui::MainWindow *ui; }; #endif这里要注意Q_OBJECT宏的位置。只要定义了Q_OBJECT这个头文件就必须经过 moc 处理这也是为什么我们前面要开CMAKE_AUTOMOC。如果你在 CMakeLists.txt 里没有把这个.h文件列进源文件列表有时 moc 还是会漏掉它所以最好的习惯是头文件也写进qt_add_executable的源文件列表里。mainwindow.cpp#include mainwindow.h #include ui_mainwindow.h MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui-setupUi(this); } MainWindow::~MainWindow() { delete ui; } void MainWindow::on_helloButton_clicked() { // logic here }main.cpp#include mainwindow.h #include QApplication int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow window; window.show(); return app.exec(); }编译运行可以看到窗口和按钮。这个例子虽然简单但已经覆盖了 Qt Widgets 项目最核心的一套结构UI 布局在.ui文件里槽函数逻辑在MainWindow类里入口统一在main.cpp。3.4 使用 Qt Designer 可视化编辑窗口布局既然谈到窗口就绕不开.ui文件。.ui文件本质上是 XML 格式的布局描述你用文本编辑器打开它看到的是一堆标签结构。平时开发中我更习惯用 Qt Designer 来可视化编辑它拖控件、拉布局、改对象名所见即所得。在 Qt Designer 里拖一个Push Button到窗口上设置objectName为helloButton然后回到代码里写一个名为on_helloButton_clicked的槽函数Qt 会自动把它和按钮的clicked信号连接起来。这就是 Qt 著名的自动连接机制无需手动connect。这里有个细节值得多说一句自动连接机制依赖的是槽函数名的命名规范必须严格遵循on_objectName_signalName的格式。如果objectName写成btnHello那么槽函数就要命名为on_btnHello_clicked两者不一致连接就会静默失败——不报错但按钮点了没反应。调试这种问题时非常容易绕晕。4. 资源文件、翻译文件与部署打包的完整配置4.1 引入资源文件的正确姿势Qt 项目里免不了要用图标、样式表、字体这些东西它们都应该被编译进可执行文件避免部署时还得带着一堆散落的文件。通过.qrc文件组织然后在 CMakeLists.txt 里列进去即可完成编译。resources/resources.qrc的示例RCC qresource prefix/ fileicons/app.png/file filestyles/style.qss/file /qresource /RCC引用方式ui-actionOpen-setIcon(QIcon(:/icons/app.png));注意:前缀这是 Qt 资源系统的固定语法。在 CMake 侧只要把.qrc文件写进了qt_add_executable的源列表并且CMAKE_AUTORCC ON已开启资源文件就会被自动编译进去。4.2 集成 Qt 翻译文件如果你的应用需要支持多语言翻译文件的管理也要在 CMake 里配置好。Qt 的翻译流程是.ts文件翻译源编译成.qm文件二进制翻译文件运行时加载.qm文件完成语言切换。CMake 里首先要开启CMAKE_AUTOUIC相关的翻译生成功能然后使用 Qt 提供的函数qt_add_translations(myapp TS_FILES translations/myapp_zh_CN.ts )这个函数会自动调用lupdate和lrelease工具lupdate从源文件提取可翻译字符串到.ts文件lrelease把.ts编译成.qm。运行时加载则是在代码里用QTranslatorQApplication app(argc, argv); QTranslator translator; if (translator.load(:/translations/myapp_zh_CN.qm)) { app.installTranslator(translator); }注意这里加载的路径要对应qt_add_translations生成的资源前缀如果你没在 CMake 里把翻译文件以资源方式嵌入就要改成文件系统路径。4.3 编译选项与生成器的选择CMake 支持多种生成器Unix Makefiles、Ninja、Visual Studio 解决方案、Xcode 工程等。我强烈推荐 Ninja。原因很简单快。Ninja 天生为并行构建设计增量编译速度比 Makefiles 快一个量级。指定生成器的方式cmake -B build -G Ninja -DCMAKE_BUILD_TYPEDebug默认生成的CMakeCache.txt像是项目的配置记忆只要你没有删掉build目录下次执行cmake --build build时会自动沿用之前的生成器和配置项不需要每次重新敲参数。4.4 部署如何让别人机器上也能跑起来本地编译运行没问题只是第一步接下来的部署才是真正容易踩坑的地方。由于 Qt 是一个庞大的框架直接把你编译好的二进制拷到别的机器上通常跑不起来因为缺 Qt 运行时库。Windows 上可以用windeployqt工具自动收集依赖windeployqt myapp.exe这个命令会把需要的 Qt DLL、平台插件如platforms/qwindows.dll全部拷贝到 exe 所在目录。Linux 上可以写一个简单的部署脚本把必要的 Qt 库用ldd检查依赖后复制到目标文件夹macOS 上则用官方提供的macdeployqt。这里有个实操细节部署最好在 Release 模式下进行因为 Debug 版本依赖的 Qt DLL 本身就带d后缀体积更大而且 mix 模式容易把调试库带到发布环境。另外windeployqt之后建议手工检查一次目录内容确认平台插件、样式插件都齐了否则目标机器启动时可能直接报could not find or load the Qt platform plugin。5. 常见编译错误与排查思路把我踩过的坑一次性说清楚5.1find_package找不到 Qt 6这是新手最常遇到的报错信息大概长这样By not providing FindQt6.cmake in CMAKE_MODULE_PATH排查顺序如下先确认 Qt 6 是否真的装了。然后在 CMakeLists.txt 中手动指定路径set(CMAKE_PREFIX_PATH C:/Qt/6.5.2/msvc2019_64)或者配置时传参cmake -B build -DCMAKE_PREFIX_PATH/opt/Qt/6.5.2/gcc_64CMAKE_PREFIX_PATH是 CMake 查找库的辅助路径前缀FindQt6.cmake通常就在prefix/lib/cmake/Qt6/目录下。把前缀指对了问题基本能解决。5.2 找不到ui_mainwindow.h文件这个错误几乎人人都遇到过尤其是在复制别人的示例代码时。这个头文件是由.ui文件在编译时自动生成的你在 IDE 里不会在源码目录看到它。生成的位置在构建目录对应路径下所以#include ui_mainwindow.h能成功的前提是 CMake 正确处理的.ui文件。常见的做法是把.ui文件列进qt_add_executable的源文件列表同时开启CMAKE_AUTOUIC ON。如果你确确实实把文件列进去了还是报错检查一下.ui文件的路径是否写错比如写成了src/mainwindow.ui但实际文件在forms/mainwindow.ui。提示ui_mainwindow.h是从mainwindow.ui生成的类名中的大写M和W也直接继承了文件名的首字母大写形式。如果你把.ui文件改成main_form.ui生成的头文件就是ui_main_form.h。5.3Q_OBJECT相关的 moc 错误链接时报undefined reference to vtable for MainWindow基本可以断定 moc 没有对这个类的头文件进行处理。在 Qt 6 CMake 环境下重新确认以下两点CMAKE_AUTOMOC是否打开。mainwindow.h是否写进了qt_add_executable的源文件列表。如果还没解决试试清空构建目录重新配置。AUTOMOC偶尔会在你添加新头文件后出现缓存不更新的情况把build目录删了重来是最省事的办法。5.4 链接错误找不到 Qt 库文件报错的形态多种多样比如LNK1104: cannot open file Qt6Widgetsd.libd后缀表示 Debug 版本的库说明你在 Debug 模式下编译项目CMake 自动去找带d的 Qt 库。这时候要检查你是否把对应 Debug 版本的 Qt 库也装上了。如果你的 Qt 安装只带了 Release 库那就用 Release 模式编译cmake -B build -DCMAKE_BUILD_TYPERelease另外确保target_link_libraries链接的是Qt6::Widgets而不是直接写库文件名。CMake 提供的Qt6::前缀的导入目标会自动处理 Debug/Release 后缀和传递依赖省心得多。5.5 常见问题的速查对照表问题现象主要原因解决思路find_package找不到 Qt6Qt 路径未设置设置CMAKE_PREFIX_PATH指向 Qt 安装目录找不到ui_mainwindow.h.ui文件未加入源列表或AUTOUIC未开启确认.ui文件路径开启CMAKE_AUTOUICundefined reference to vtablemoc 未处理含Q_OBJECT的头文件确认AUTOMOC开启头文件加入工程必要时清理目录链接时需要Qt6Widgetsd.lib找不到Debug 库未完整安装切换到 Release 模式或补装 Debug 库运行弹出 could not find platform plugin环境缺少平台 DLL在目标机器使用windeployqt收集依赖.qrc中资源在代码里访问不到前缀或路径写错检查资源路径前缀是否正确确认AUTORCC开启点击按钮没有反应自动连接命名不规范确认槽函数名符合on_objectName_signalName格式6. 一点个人体会我在最开始从 qmake 切到 CMake 时觉得最难受的就是配置脚本里那些自动变量和函数名满屏的新名词真让人头大。但坚持用下来之后发现CMake 的显式逻辑反而让项目变得更容易把控依赖关系一目了然构建目录想怎么换就怎么换CI 流水线配置起来也极其顺手。如果让我给刚上手的朋友一个建议那就是不要试图背下所有配置项先把一个最小项目跑起来然后按需添加资源、翻译、第三方库。等遇到问题了再去查对应模块的 CMake 写法理解起来会快得多。哪怕现在记不下来把文章中这份模板保存下来用的时候照着改跑通三次之后自然就有感觉了。再分享一个提高效率的小技巧在工程目录下放一个CMakePresets.json文件把常用的配置模式固化下来{ version: 3, configurePresets: [ { name: debug, generator: Ninja, binaryDir: ${sourceDir}/build-debug, cacheVariables: { CMAKE_BUILD_TYPE: Debug } }, { name: release, generator: Ninja, binaryDir: ${sourceDir}/build-release, cacheVariables: { CMAKE_BUILD_TYPE: Release } } ] }有了这个文件之后切配置就只需要cmake --preset debug cmake --build build-debug完全不用再去记那一长串的-DCMAKE_PREFIX_PATH和-DCMAKE_BUILD_TYPE参数了。这个习惯帮我省了非常多的时间强烈推荐。