实战案例二:构建一个跨平台桌面应用

说实话,很多人在学 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 的路径配对——后来发现用 vcpkginstall 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 构建流程

下面这张图展示了整个构建系统的数据流和依赖关系:

QuickSnap 构建系统流程图 源码 (src/) 第三方依赖 CMake 配置 CI 脚本 CMake 配置阶段 (cmake -B build) find_package / add_subdirectory / target_link_libraries 构建阶段 (cmake --build build) 编译 capture_lib / ui_lib / 链接 quicksnap 测试 (ctest) 打包 (cpack) 触发 输入 配置 构建 测试 打包

总结一下

这个案例其实把 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 会找不到包。我见过太多人在这上面浪费半天时间了。


公众号:蓝海资料掘金营,微信deep3321