实战案例二:构建一个跨平台桌面应用
说实话,很多人在学 CMake 的时候,最头疼的就是「学了一堆语法,真到实战就懵了」。尤其是要处理第三方依赖、跨平台编译、还要搞打包和 CI,光想想就头大。
这一章,我们就拿一个真实的桌面应用来练手。我会带着你一步步搭起来,把之前学的知识点串成一条线。
项目背景:一个简单的截图工具
我们要构建的是一个跨平台截图工具,叫 QuickSnap。它依赖三个第三方库:
- Qt6:图形界面框架
- OpenCV:图像处理(截图后做边缘检测)
- spdlog:日志库
项目结构是这样的:
QuickSnap/
├── CMakeLists.txt
├── src/
│ ├── main.cpp
│ ├── capture/
│ │ ├── CMakeLists.txt
│ │ ├── ScreenCapture.h
│ │ └── ScreenCapture.cpp
│ └── ui/
│ ├── CMakeLists.txt
│ ├── MainWindow.h
│ └── MainWindow.cpp
├── third_party/
│ └── spdlog/ # 直接引入源码
├── cmake/
│ ├── FindOpenCV.cmake
│ └── Packaging.cmake
├── .github/
│ └── workflows/
│ └── ci.yml
└── tests/
└── CMakeLists.txt
嗯,这里要注意:第三方依赖的处理方式不同。Qt 和 OpenCV 是系统级安装的,我们用 find_package 来找。spdlog 是 header-only 的,直接扔到项目里用 add_subdirectory 引入。
顶层 CMakeLists.txt:搭骨架
先看顶层文件。我个人习惯把项目信息、C++标准、编译选项都放在这里:
cmake_minimum_required(VERSION 3.20)
project(QuickSnap VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON) # Qt 的 MOC 处理
# 第三方依赖
find_package(Qt6 REQUIRED COMPONENTS Widgets)
find_package(OpenCV REQUIRED)
add_subdirectory(third_party/spdlog)
add_subdirectory(src/capture)
add_subdirectory(src/ui)
# 主可执行文件
add_executable(quicksnap src/main.cpp)
target_link_libraries(quicksnap PRIVATE
capture_lib
ui_lib
Qt6::Widgets
${OpenCV_LIBS}
spdlog::spdlog
)
# 安装规则
install(TARGETS quicksnap RUNTIME DESTINATION bin)
install(DIRECTORY resources/ DESTINATION share/quicksnap)
这里有个坑,我曾经踩过:Qt6 的 AUTOMOC 必须开启,否则你继承 QObject 的类编译不过。还有,find_package 的顺序也有讲究——先找 Qt,再找 OpenCV,因为 OpenCV 的 Find 模块可能会污染一些变量。
子模块的 CMakeLists.txt
每个子模块独立一个 CMakeLists.txt,这样职责清晰。以 src/capture/CMakeLists.txt 为例:
add_library(capture_lib STATIC
ScreenCapture.cpp
)
target_include_directories(capture_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(capture_lib PUBLIC Qt6::Widgets ${OpenCV_LIBS})
为什么用 STATIC 库?因为我们的项目不大,静态链接可以减少部署时的 DLL 依赖。你想想看,要是动态库,Windows 上光 Qt 的 DLL 就得带十几个,烦不烦?
src/ui/CMakeLists.txt 类似:
add_library(ui_lib STATIC
MainWindow.cpp
)
target_include_directories(ui_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(ui_lib PUBLIC capture_lib Qt6::Widgets)
注意这里 ui_lib 链接了 capture_lib,因为主窗口需要调用截图功能。这种依赖关系在 CMake 里用 target_link_libraries 传递,非常清晰。
打包:让用户能直接安装
光编译出来不行,得让用户能一键安装。我用了 CPack,配合 cmake/Packaging.cmake:
include(CPack)
set(CPACK_PACKAGE_NAME "QuickSnap")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_GENERATOR "NSIS" CACHE STRING "打包格式")
if(WIN32)
set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64")
set(CPACK_NSIS_DISPLAY_NAME "QuickSnap ${CPACK_PACKAGE_VERSION}")
install(FILES ${CMAKE_BINARY_DIR}/quicksnap.exe DESTINATION bin)
endif()
在顶层 CMakeLists.txt 末尾加上一句 include(cmake/Packaging.cmake),然后运行:
cmake --build . --target package
就能生成安装包了。Windows 下是 NSIS 安装程序,Linux 下可以生成 .deb 或 .rpm。我个人习惯在 macOS 上用 BundleUtilities 打包成 .dmg,不过那又是另一个话题了。
CI:自动化构建与测试
没有 CI 的项目是不完整的。我们写一个 GitHub Actions 配置,在 .github/workflows/ci.yml:
name: CI
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y qt6-base-dev libopencv-dev
- name: Install dependencies (macOS)
if: runner.os == 'macOS'
run: |
brew install qt@6 opencv
- name: Configure
run: cmake -B build -DCMAKE_BUILD_TYPE=Release
- name: Build
run: cmake --build build --config Release
- name: Test
run: ctest --test-dir build --output-on-failure
- name: Package
run: cmake --build build --target package
这里有个细节:不同平台的依赖安装命令不同。Linux 用 apt,macOS 用 brew,Windows 则用 vcpkg 或者直接预装。我曾经在 Windows CI 上折腾了一整天,就为了把 Qt 的路径配对——后来发现用 vcpkg 的 install qt6 最省事。
避坑指南:跨平台编译的常见问题
做跨平台项目,有些坑是绕不开的。我列几个常见的:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Qt 找不到 | Qt 安装路径未加入 CMAKE_PREFIX_PATH | 设置环境变量 CMAKE_PREFIX_PATH=/path/to/qt |
| OpenCV 链接失败 | OpenCV 版本不匹配或缺少组件 | 用 find_package(OpenCV REQUIRED COMPONENTS core imgproc) 精确指定 |
| Windows 下 DLL 缺失 | 运行时找不到 Qt/OpenCV 的 DLL | 用 windeployqt 工具自动拷贝依赖 |
| macOS 下签名问题 | 未对应用进行 codesign | 在打包脚本中加入 codesign --deep --force |
嗯,这里要特别提醒:Windows 上的路径分隔符是反斜杠,CMake 虽然会自动处理,但如果你在脚本里硬编码路径,很容易出问题。我建议统一用正斜杠,CMake 会帮你转换。
SVG 结构图:QuickSnap 构建流程
下面这张图展示了整个构建系统的数据流和依赖关系:
总结一下
这个案例其实把 CMake 的核心功能都覆盖了:
- 多目录管理:用
add_subdirectory组织子模块 - 第三方依赖:区分系统安装和源码引入两种方式
- 跨平台编译:通过条件判断处理不同 OS 的差异
- 打包与 CI:CPack + GitHub Actions 实现自动化交付
说实话,这套模式我用了好几年,从个人项目到公司产品都这么搭。你只要把顶层 CMakeLists.txt 和子模块的结构固定下来,后续加功能、换依赖都非常方便。
下次你接到一个跨平台桌面应用的需求,不妨直接拿这个模板改。相信我,比从零开始写 CMakeLists.txt 省心多了。
核心要点:跨平台项目的 CMake 配置,关键在于「分层」和「条件判断」。把平台相关的逻辑隔离到单独的文件里,主流程保持干净,这样维护起来才不头疼。
小技巧:在 CI 中可以用 cmake -E 命令来做一些跨平台的文件操作,比如 cmake -E copy 代替 cp,这样脚本在 Windows 和 Linux 上都能跑。
注意:如果你用 vcpkg 管理依赖,记得在 CMake 配置时加上 -DCMAKE_TOOLCHAIN_FILE 参数,否则 find_package 会找不到包。我见过太多人在这上面浪费半天时间了。