39. 10.3 项目三:图形应用(Qt + OpenGL)

引言:当”施工队长”接到一份精装图纸

在前两个项目中,我们的CMake”施工队长”先是盖了一栋独立小洋楼(命令行工具),又经营起了一家预制构件厂(跨平台共享库)。这些项目虽然实用,但终究是在”黑框框”里打交道。这一次,我们要挑战一个让无数开发者既兴奋又头疼的领域——图形界面应用

想象一下,客户这次不要毛坯房了,他要的是一座带全景落地窗、智能幕墙、内部精装修的商业综合体。在C++世界里,这意味着我们要请来两位重量级设计师:Qt(负责界面框架和装修标准)和OpenGL(负责高性能图形渲染的幕墙系统)。而CMake队长的任务,就是协调这两位脾气不小的专家,让他们在Windows、macOS、Linux三个工地上无缝合作,最后还要把整座大楼打包成用户开箱即用的”精装礼盒”。

这个项目会教会你:如何让CMake与Qt的元对象系统(MOC)和平共处,如何处理.qrc资源文件这些”特殊建材”,以及如何针对不同平台打出差异化的安装包。准备好,我们要开始盖地标建筑了!

Qt CMake集成:请两位设计师到场

Qt从5.15版本开始大力推广CMake作为官方构建系统,到了Qt6,CMake更是成为了”首席推荐”的构建工具。但想要让Qt和OpenGL在CMake项目中听话地工作,第一步就是正确地”发出邀请函”。

find_package(Qt6 …):按专业下订单

Qt是一个模块化巨头,它不像某些库那样一股脑地把所有功能塞给你。你需要什么组件,就得在CMake里明确点菜。在Modern CMake里,我们使用find_package配合COMPONENTS关键字来完成这项工作:

cmake_minimum_required(VERSION 3.16)
project(QtGLDemo VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)  # 自动调用MOC,稍后详述

# 发出邀请函:我们需要Qt6的Widgets(界面)、OpenGL(渲染)、以及OpenGLWidgets(Qt6新特性)
find_package(Qt6 REQUIRED COMPONENTS Core Widgets OpenGL OpenGLWidgets)

# 如果你的项目还需要网络、数据库,继续加COMPONENTS:
# find_package(Qt6 REQUIRED COMPONENTS Core Widgets OpenGL Network Sql)

add_executable(QtGLDemo
    main.cpp
    MainWindow.cpp
    MainWindow.h
    GLWidget.cpp
    GLWidget.h
)

# 链接库:注意使用Qt6::命名空间,这是Modern CMake的规范写法
target_link_libraries(QtGLDemo PRIVATE
    Qt6::Core
    Qt6::Widgets
    Qt6::OpenGL
    Qt6::OpenGLWidgets
)

这里有几个新手容易踩的坑,队长必须提前提醒你:

  • 版本号问题:如果你还在用Qt5,命令要改成find_package(Qt5 ...),且没有OpenGLWidgets这个组件(Qt5里OpenGL集成在Widgets里)。Qt6和Qt5的CMake模块命名并不完全兼容。
  • REQUIRED不能省:如果漏写了REQUIRED,当某个组件找不到时,CMake不会立刻报错,而是等到链接阶段才告诉你”找不到目标”,那时候排查问题就像在大楼里找一根接错的电线。
  • 大小写敏感:虽然CMake命令不区分大小写,但Qt的组件名(如OpenGLWidgets)是区分大小写的,写错一个字就会采购失败。

Qt6 vs Qt5 的CMake差异速查

考虑到很多项目还在Qt5到Qt6的过渡期,这里放一张”翻译表”:

功能 Qt5 写法 Qt6 写法
查找包 find_package(Qt5 COMPONENTS Widgets OpenGL) find_package(Qt6 COMPONENTS Widgets OpenGL OpenGLWidgets)
链接目标 Qt5::Widgets Qt6::Widgets
编译特性 手动处理或借助qt5_generate_moc 更完善的AUTOMOC/AUTOUIC/AUTORCC

MOC/UIC/RCC自动处理:应对Qt的”特殊建材”

Qt框架有一个让很多初学者望而却步的特性——元对象系统(Meta-Object System)。简单来说,Qt在标准C++之上额外增加了信号槽(Signals & Slots)、属性系统、运行时类型信息等功能。为了实现这些魔法,Qt需要一种叫MOC(Meta-Object Compiler)的预处理器,它会在正式编译前扫描你的头文件,生成额外的C++代码(比如moc_MainWindow.cpp)。

除此之外,如果你用了Qt Designer画界面,会产生.ui文件,需要UIC(User Interface Compiler)把它转成C++头文件;如果你使用了.qrc资源文件(比如嵌入图标、着色器脚本),还需要RCC(Resource Compiler)把它编译成二进制数据。

在古老的qmake时代,这些步骤是透明的;但在CMake早期(以及某些旧教程里),开发者需要手动写qt5_wrap_cpp()qt5_wrap_ui()之类的命令,非常繁琐。幸运的是,Modern CMake为Qt提供了全自动解决方案

开启自动化:CMAKE_AUTOMOC 三剑客

在CMakeLists.txt的最顶部(find_package之前),加入这三行”魔法开关”:

set(CMAKE_AUTOMOC ON)   # 自动处理Q_OBJECT宏,生成moc_文件
set(CMAKE_AUTOUIC ON)   # 自动处理.ui文件,生成ui_头文件
set(CMAKE_AUTORCC ON)   # 自动处理.qrc文件,生成资源二进制

一旦开启这三个选项,CMake会在构建过程中自动

  1. 扫描你的头文件,发现含有Q_OBJECT宏的类,就调用MOC生成元对象代码;
  2. 发现.ui文件,就调用UIC生成对应的UI头文件;
  3. 发现.qrc文件,就调用RCC编译资源。

你的add_executable只需要老老实实地列出原始文件即可,不需要额外添加生成的中间文件:

add_executable(QtGLDemo
    main.cpp
    MainWindow.cpp
    MainWindow.h      # 含有Q_OBJECT,AUTOMOC自动处理
    MainWindow.ui     # AUTOUIC自动生成ui_MainWindow.h
    resources.qrc     # AUTORCC自动编译资源
    shaders.qrc       # 甚至可以多个qrc文件
)

常见问题:UIC生成的头文件找不到?

很多新手会遇到这样的报错:fatal error: ui_MainWindow.h: No such file or directory。这是因为C++编译器不知道去哪个”临时仓库”找UIC刚生成的头文件。

解决方法很简单:在CMake中,确保你的可执行目标(或库目标)能够找到构建目录下的生成的头文件。通常Modern CMake会自动处理,但如果遇到顽固问题,可以显式添加包含目录:

# 让编译器在构建目录中查找自动生成的ui_*.h文件
target_include_directories(QtGLDemo PRIVATE ${CMAKE_CURRENT_BINARY_DIR})

这行代码告诉编译器:”除了源码目录,也记得去构建生成的临时目录(比如build/)里翻翻看。”

资源文件管理:把”装修材料”封进墙体

图形应用离不开各种资源:图标、字体、3D模型、GLSL着色器代码、纹理图片。在Qt项目中,这些文件通常不会以独立文件形式放在可执行文件旁边(否则用户随手删掉一个图标,程序就崩了),而是通过.qrc(Qt Resource Collection)文件直接嵌入到可执行文件内部

编写qrc文件:资源清单

qrc文件本质上是一个XML清单,告诉Qt哪些文件需要打包进二进制。例如:

<RCC>
    <qresource prefix="/">
        <file>icons/app_icon.png</file>
        <file>shaders/vertex.glsl</file>
        <file>shaders/fragment.glsl</file>
        <file>textures/wood.jpg</file>
    </qresource>
</RCC>

在C++代码中,你可以通过":/shaders/vertex.glsl"这样的路径访问这些资源,无论程序最终部署到哪里,这些资源都”焊死”在可执行文件内部。

CMake中的资源处理

得益于前面开启的CMAKE_AUTORCC ON,CMakeLists.txt里的操作极简——只要把.qrc文件加入源文件列表即可:

add_executable(QtGLDemo
    main.cpp
    MainWindow.cpp
    GLWidget.cpp
    resources.qrc    # AUTORCC会自动调用rcc工具处理
)

但这里有个CMake进阶技巧:如果你的资源文件非常多,或者你想在不同的构建配置(Debug/Release)中嵌入不同的资源,可以用set_source_files_properties给qrc文件附加属性,甚至可以用生成器表达式(Generator Expressions)控制资源的编译方式。不过对于大多数项目,自动模式已经足够好用。

OpenGL着色器的特殊处理

在Qt + OpenGL项目中,GLSL着色器代码通常也是以资源形式嵌入。除了qrc方式,有些团队喜欢把着色器直接写成字符串常量放在C++代码里。两种方式各有千秋,但在CMake层面,如果你选择qrc方式,务必确保.glsl文件被列在qrc清单中,并且在C++里使用Qt的资源路径语法读取:

QFile vertShaderFile(":/shaders/vertex.glsl");
vertShaderFile.open(QIODevice::ReadOnly);
QString vertexShaderCode = QString::fromUtf8(vertShaderFile.readAll());

平台特定代码组织:同一套图纸,不同气候带

图形应用是对平台差异最敏感的软件类型之一。Windows的窗口句柄是HWND,macOS有自己的Cocoa框架,Linux则可能跑在X11或Wayland上。Qt虽然帮我们封装了大部分差异,但在涉及OpenGL上下文创建、原生窗口句柄获取、或者平台特定打包逻辑时,我们仍然需要写一些条件代码

在CMake中优雅地处理平台差异

最推荐的方式是把平台相关代码隔离到单独的文件,然后在CMake里根据平台”选材”:

# 公共源代码
set(SOURCES
    main.cpp
    MainWindow.cpp
    GLWidget.cpp
    Renderer.cpp
)

# 平台特定源代码
if(WIN32)
    list(APPEND SOURCES
        PlatformWindows.cpp
        NativeWindowWin32.cpp
    )
    target_compile_definitions(QtGLDemo PRIVATE PLATFORM_WINDOWS)
elseif(APPLE)
    list(APPEND SOURCES
        PlatformMacOS.mm        # .mm是Objective-C++,macOS特有
        NativeWindowCocoa.mm
    )
    target_compile_definitions(QtGLDemo PRIVATE PLATFORM_MACOS)
    # macOS上可能需要链接额外的框架
    target_link_libraries(QtGLDemo PRIVATE
        "-framework Cocoa"
        "-framework OpenGL"
        "-framework IOKit"
    )
elseif(UNIX AND NOT APPLE)  # Linux
    list(APPEND SOURCES
        PlatformLinux.cpp
        NativeWindowX11.cpp
    )
    target_compile_definitions(QtGLDemo PRIVATE PLATFORM_LINUX)
endif()

add_executable(QtGLDemo ${SOURCES})

这种做法比到处写#ifdef _WIN32要干净得多,就像施工时把南方用的防潮材料和北方用的保温材料分开放置,而不是堆在同一个仓库里让工人现场挑选。

针对Qt的OpenGL平台适配

Qt6在OpenGL支持上做了不少重构。比如在Windows上,Qt6默认使用动态加载的OpenGL实现,你可能需要确保opengl32sw.dll(软件光栅化后备)被正确部署;在macOS上,Apple已经废弃了OpenGL,但Qt6仍然通过兼容性层支持它,只是性能不如Metal。

在CMake中,你可以通过目标属性给不同平台设置不同的编译定义,来控制OpenGL的渲染后端:

if(WIN32)
    target_compile_definitions(QtGLDemo PRIVATE GL_BACKEND_ANGLE)
elseif(APPLE)
    target_compile_definitions(QtGLDemo PRIVATE GL_BACKEND_COMPATIBILITY)
endif()

打包为可分发应用:把大楼装进礼盒

图形应用最难的环节往往不是开发,而是部署。你不可能要求最终用户先安装Qt开发环境、再配置PATH变量。我们需要的是像专业软件那样:Windows上双击setup.exe安装,macOS上拖入Applications,Linux上双击运行一个独立文件。

Windows:从exe到Installer

在Windows上,Qt应用依赖大量的DLL:Qt6Core.dll、Qt6Widgets.dll、Qt6OpenGL.dll,再加上编译器运行时(如MSVC的vcruntime140.dll)和平台插件(platformsqwindows.dll)。手动复制这些文件简直是噩梦。

Qt官方提供了windeployqt工具,它会自动分析你的可执行文件,把所有依赖的Qt库、插件、翻译文件复制到目标目录。我们可以把它集成到CMake的安装流程中:

# 安装可执行文件
install(TARGETS QtGLDemo
    RUNTIME DESTINATION bin
)

# 使用windeployqt自动收集依赖
if(WIN32)
    find_program(WINDEPLOYQT_EXECUTABLE windeployqt HINTS "${Qt6Core_DIR}/../../../bin")
    
    install(CODE "
        execute_process(
            COMMAND "${WINDEPLOYQT_EXECUTABLE}"
                    --dir ${CMAKE_INSTALL_PREFIX}/bin
                    "$<TARGET_FILE:QtGLDemo>"
        )
    ")
endif()

这段代码的含义是:在执行cmake --install时,CMake会自动调用windeployqt,把Qt相关的DLL全部”搬运”到安装目录的bin文件夹下。之后,再用CPack生成NSIS安装包(见6.3节),用户就能得到标准的Windows安装向导了。

macOS:构建Bundle

macOS的应用不是单个可执行文件,而是一个以.app结尾的Bundle(目录),内部有严格的层级结构:MyApp.app/Contents/MacOS/MyApp(可执行文件)、MyApp.app/Contents/Frameworks/(依赖库)等。

CMake对macOS Bundle有原生支持,只需要在add_executable时加上BUNDLE关键字,并设置一些MACOSX_BUNDLE属性:

add_executable(QtGLDemo MACOSX_BUNDLE
    main.cpp
    MainWindow.cpp
    resources.qrc
)

set_target_properties(QtGLDemo PROPERTIES
    MACOSX_BUNDLE_GUI_IDENTIFIER "com.example.QtGLDemo"
    MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}"
    MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}"
    MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/cmake/Info.plist.in"
)

同样,Qt提供了macdeployqt工具来自动把Frameworks打包进Bundle。在CMake安装脚本中调用它:

if(APPLE)
    find_program(MACDEPLOYQT_EXECUTABLE macdeployqt HINTS "${Qt6Core_DIR}/../../../bin")
    install(CODE "
        execute_process(
            COMMAND "${MACDEPLOYQT_EXECUTABLE}"
                    "$<TARGET_BUNDLE_DIR:QtGLDemo>"
                    -verbose=1
        )
    ")
endif()

生成出来的.app可以直接压缩成MyApp.dmg分发,用户下载后拖拽到Applications即可完成安装,这正是macOS用户熟悉的体验。

Linux:AppImage——一个文件跑遍所有发行版

Linux下的打包是”地狱难度”:Ubuntu用deb,Fedora用rpm,Arch有自己的pacman,库的路径和版本千差万别。对于图形应用,AppImage是目前最优雅的解决方案——它把整个应用和所有依赖打包成一个可执行文件,用户下载后chmod +x就能运行,无需root权限,不碰系统库。

构建AppImage的流程通常是:

  1. 先用cmake --install把应用安装到一个”干净的”AppDir目录结构(类似缩小的文件系统根目录);
  2. 使用linuxdeployqt(或linuxdeploy + appimagetool)把Qt库和依赖复制到AppDir;
  3. appimagetool把AppDir转换成单个.AppImage文件。

我们可以在CMake中准备一个专门的安装前缀(Install Prefix)来生成AppDir:

# 专门用于生成AppImage的安装路径
set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/AppDir" CACHE PATH "AppDir install prefix" FORCE)

install(TARGETS QtGLDemo
    RUNTIME DESTINATION bin
    BUNDLE DESTINATION .
)

# 安装桌面文件和图标(AppImage规范要求)
install(FILES ${CMAKE_SOURCE_DIR}/packaging/QtGLDemo.desktop DESTINATION share/applications)
install(FILES ${CMAKE_SOURCE_DIR}/packaging/QtGLDemo.png DESTINATION share/icons/hicolor/256x256/apps)

然后在外部脚本(或CI流水线)中执行:

cmake --build build
cmake --install build
# 使用 linuxdeployqt 填充依赖
./linuxdeployqt-continuous-x86_64.AppImage build/AppDir/share/applications/*.desktop -appimage
# 最终生成 QtGLDemo-x86_64.AppImage

这样,你的Qt+OpenGL应用就能以一个文件的形式,在绝大多数Linux发行版上”开箱即用”了。

完整CMakeLists.txt参考

为了让你有一个整体印象,这里给出一份经过实战检验的、精简版Qt+OpenGL项目CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)
project(QtGLDemo VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)

# 查找Qt6
find_package(Qt6 REQUIRED COMPONENTS Core Widgets OpenGL OpenGLWidgets)

set(SOURCES
    main.cpp
    MainWindow.cpp
    MainWindow.h
    MainWindow.ui
    GLWidget.cpp
    GLWidget.h
    Renderer.cpp
    Renderer.h
    resources.qrc
)

# 平台适配
if(WIN32)
    list(APPEND SOURCES NativeWindowWin32.cpp)
elseif(APPLE)
    list(APPEND SOURCES NativeWindowMacOS.mm)
    set_source_files_properties(NativeWindowMacOS.mm PROPERTIES COMPILE_FLAGS "-fobjc-arc")
elseif(UNIX)
    list(APPEND SOURCES NativeWindowLinux.cpp)
endif()

add_executable(QtGLDemo WIN32 MACOSX_BUNDLE ${SOURCES})

target_link_libraries(QtGLDemo PRIVATE
    Qt6::Core
    Qt6::Widgets
    Qt6::OpenGL
    Qt6::OpenGLWidgets
)

target_include_directories(QtGLDemo PRIVATE ${CMAKE_CURRENT_BINARY_DIR})

# 安装与打包配置(可根据平台细化)
install(TARGETS QtGLDemo
    RUNTIME DESTINATION bin
    BUNDLE DESTINATION .
)

# windeployqt / macdeployqt 的集成代码放在 cmake/install-rules.cmake 中
# 通过 include(cmake/install-rules.cmake) 引入,保持主文件整洁

小结:精装地标交付清单

通过这个项目,我们的CMake”施工队长”又掌握了一整套精装修技能:

  • 精准采购:用find_package(Qt6 COMPONENTS ...)按需引入Qt模块,避免拖入不必要的依赖;
  • 自动化预处理:开启AUTOMOCAUTOUICAUTORCC,让Qt的元对象系统、UI文件、资源文件在后台自动处理,不干扰主构建流程;
  • 资源内嵌:通过.qrc文件把图标、着色器等资源封进可执行文件,避免运行时路径混乱;
  • 平台隔离:用CMake的条件语句(if(WIN32)if(APPLE))把平台特定代码分仓管理,保持主干代码的纯净;
  • 精美打包:Windows用windeployqt+NSIS Installer,macOS用macdeployqt+Bundle,Linux用AppImage,实现”一次构建,多平台精装交付”。

在下一节(也是实战系列的最后一节),我们将挑战一个更现代、更复杂的场景——基于微服务架构的C++后端系统。届时,CMake队长要学会管理多仓库、多服务的依赖关系,并与Docker、Conan等现代DevOps工具链协同作战。准备好进入云端工地了吗?

请登录后发表评论

    没有回复内容

正在唤醒异次元光景……