Qt优雅的组织项目结构二(基于Qt5使用CmakeList进行模块化配置)——————附带详细示例代码

文章目录

    • [0 背景](#0 背景)
      • [0.1 其他](#0.1 其他)
    • [1 使用cmake脚本](#1 使用cmake脚本)
      • [1.1 创建项目](#1.1 创建项目)
      • [1.2 简化cmakefile文件](#1.2 简化cmakefile文件)
      • [1.3 创建cmake文件](#1.3 创建cmake文件)
  • 附录

0 背景

因为Qt6抛弃了qmake,全面转向了CMake的怀抱。因此新项目开发,也需要使用到CMake来进行C++的项目管理。为了兼容win7和旧mac os系统,因此还是用Qt5.15.2(Qt最后一个5版本)来进行项目开发。

本文讲述使用CMake进行项目管理和模块化。

0.1 其他

这里顺带提及一下qt5与qt6的区别:

详细说就是:

  • 1,构建系统;
    • Qt5:使用.pro 文件和 qmake
    • Qt6:使用cmake
  • 2,图形架构(最本质);
    • Qt5:图形渲染主要依赖 OpenGL(在不同平台上的性能和兼容性(尤其是 Windows 上的 ANGLE)一直是个痛点);
    • Qt6:使用RHI (Rendering Hardware Interface,抽象层)。能根据运行环境自动选择最优的图形 API------在 Windows 上用 Direct3D 12,在 macOS 上用 Metal,在 Linux/Android 上用 Vulkan
  • 3, C++ 标准;
    • Qt5:最低支持 C++11 / C++14。
    • Qt6 : 强制要求编译器支持 C++17,移除了约 15% 的过时 API(如 Qt Script)。
  • 4,应式编程;
    • Qt5 :需要手动写 if (oldValue != newValue) 然后 emit signal
    • Qt6: 引入了 QProperty<T>。它实现了属性的自动绑定和通知。即,当修改一个属性时,所有依赖它的其他属性或 UI 元素会自动更新,无需手动触发信号槽。
  • 5, QML;
    • Qt6: JavaScript 引擎升级(支持 ECMAScript 7),增强了类型检查,Qt Multimedia 模块完全重写。

1 使用cmake脚本

CMake脚本文件(后缀名.cmake)功能与pri文件相似,以定义变量、设置编译选项等。之前写过一篇使用pri文件的文章,可以参考一下。

这是原始的项目结构:

这是使用.cmake后的项目结构:

1.1 创建项目

  • 1, 新建一个项目;
  • 2,构建系统选择Cmake for Qt 5 and 6;

如果选择下面的Camke,就支持Qt6,而不是兼容Qt5。

  • 3,构建完成后,将会生成如下的CMakeList.txt文件;
cpp 复制代码
cmake_minimum_required(VERSION 3.16)

project(TestCmake VERSION 0.1 LANGUAGES CXX)

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets)

set(PROJECT_SOURCES
        main.cpp
        mainwindow.cpp
        mainwindow.h
        mainwindow.ui
)

if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
    qt_add_executable(TestCmake
        MANUAL_FINALIZATION
        ${PROJECT_SOURCES}
    )
# Define target properties for Android with Qt 6 as:
#    set_property(TARGET TestCmake APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR
#                 ${CMAKE_CURRENT_SOURCE_DIR}/android)
# For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation
else()
    if(ANDROID)
        add_library(TestCmake SHARED
            ${PROJECT_SOURCES}
        )
# Define properties for Android with Qt 5 after find_package() calls as:
#    set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
    else()
        add_executable(TestCmake
            ${PROJECT_SOURCES}
        )
    endif()
endif()

target_link_libraries(TestCmake PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)

# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
# If you are developing for iOS or macOS you should consider setting an
# explicit, fixed bundle identifier manually though.
if(${QT_VERSION} VERSION_LESS 6.1.0)
  set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.TestCmake)
endif()
set_target_properties(TestCmake PROPERTIES
    ${BUNDLE_ID_OPTION}
    MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
    MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
    MACOSX_BUNDLE TRUE
    WIN32_EXECUTABLE TRUE
)

include(GNUInstallDirs)
install(TARGETS TestCmake
    BUNDLE DESTINATION .
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

if(QT_VERSION_MAJOR EQUAL 6)
    qt_finalize_executable(TestCmake)
endif()

项目结构如下所示:

1.2 简化cmakefile文件

  • 1,设置cmake的版本;
cpp 复制代码
# 设置cmake的最低版本
cmake_minimum_required(VERSION 3.16)
# 定义项目名称、项目版本、变成语言 
project(TestNewFramework VERSION 0.1 LANGUAGES CXX)
  • 2,设置camke可以自动识别并处理 Qt 的核心特性(信号槽、UI 文件、资源文件);

自动处理信号与槽:如果类继承自 QObject 并使用了 Q_OBJECT 宏(为了使用信号槽、属性等特性),编译器需要先通过 moc 工具生成一个额外的 C++ 文件(例如 moc_myclass.cpp),然后才能编译

cpp 复制代码
# 自动处理信号与槽
set(CMAKE_AUTOMOC ON)

自动处理界面文件:当使用 Qt Designer 画了一个界面(.ui 文件),编译时需要将其转换为对应的 C++ 头文件(ui_*.h),你的代码才能通过 ui->setupUi(this) 来加载界面。

cpp 复制代码
# 自动处理界面文件
set(CMAKE_AUTOUIC ON)

自动处理资源文件:当把图片、翻译文件等打包进 .qrc 资源文件时,这些资源实际上是被编译进了二进制文件中。这需要 rcc 工具将 .qrc 转换为一个 C++ 源文件(通常包含在 qrc_*.cpp 中)。

cpp 复制代码
# 自动处理资源文件
set(CMAKE_AUTORCC ON)
  • 3,强制指定C++的版本,并阻止编译器扩展;

指定 C++ 语言标准版本为C++17:

cpp 复制代码
# 指定 C++ 语言标准版本
set(CMAKE_CXX_STANDARD 17)

强制要求指定的标准必须被满足。如果不支持C++17,CMake 配置阶段会直接报错并终止,不会尝试降级编译。

cpp 复制代码
# 强制要求指定的标准必须被满足。如果不支持C++17,CMake 配置阶段会直接报错并终止,不会尝试降级编译
set(CMAKE_CXX_STANDARD_REQUIRED ON)

保证代码的跨平台可移植性。 禁止编译器使用 GNU 扩展(如 GCC 的 gnu++17)或其他非标准的编译器扩展,强制使用严格的 ISO C++ 标准(即 c++17)。

cpp 复制代码
# 保证代码的跨平台可移植性
set(CMAKE_CXX_EXTENSIONS OFF)
  • 4,寻找Qt版本,并加载具体的 Qt 版本配置;

尝试按照Qt6、Qt5的顺序去寻找配置文件,如果找到了,则会记录具体的版本,例如Qt6、Qt5。代码执行成功后,CMake 会自动生成一个变量 QT_VERSION_MAJOR,如果找到了 Qt6,QT_VERSION_MAJOR 的值就是 6。

cpp 复制代码
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets)

根据上面的结果,精确加载具体的 Qt 版本配置。

cpp 复制代码
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets)

没有只写成find_package(Qt6 ...) find_package(Qt5 ...),是为了兼容团队成员中既有人装了Qt6,又有人装了Qt5。

  • 5,定义一个变量,用来集中存储项目所需的源文件列表;
cpp 复制代码
set(PROJECT_SOURCES
        main.cpp
        mainwindow.cpp
        mainwindow.h
        mainwindow.ui
)
  • 6,调用编译器(如 g++, cl.exe)将源代码编译成目标文件(.o 或 .obj)
    然后调用链接器(Linker),将这些目标文件和依赖的库(如 Qt5Core, Qt5Widgets)链接在一起,生成一个完整的、操作系统可以直接加载的二进制文件exe
cpp 复制代码
add_executable(TestCmake
    ${PROJECT_SOURCES}
)

如果使用Qt6,可以使用下面的代码来编译(智能化"编译:自动处理 Qt6 的跨平台细节(如 RHI 图形后端、资源嵌入)):

cpp 复制代码
    qt_add_executable(TestCmake
        MANUAL_FINALIZATION
        ${PROJECT_SOURCES}
    )

# 完成最终的链接,比add_executable多一步
 qt_finalize_executable(TestCmake)

如果是Android程序,则使用如下的代码(将源代码编译成一个动态链接库【Android App 是由 Java 驱动的,C++ 代码只是作为"本地方法"提供支持,它必须被打包成 .so 动态库,等待 Java 代码在运行时通过 System.loadLibraryJNI),将其加载进内存使用。】):

cpp 复制代码
        add_library(TestCmake SHARED
            ${PROJECT_SOURCES}
        )
  • 7,连接目标与依赖库(将 Qt 的 Widgets 模块链接到你的可执行程序 TestCmake 中,且该依赖仅在内部使用);
cpp 复制代码
target_link_libraries(TestCmake PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)

具体:
TestCmake(项目名称);
PRIVATE(链接作用域:私有依赖 ,另一个库要调用 TestCmake 的功能时,不需要知道 Widgets 的存在,能减少不必要的依赖传递);
Qt $ {QT_VERSION_MAJOR}::Widgets(库的目标名);

  • 8, 为构建目标(如可执行文件、库)设置特定的属性(发布专业、规范的软件时,必不可少);

设置BUNDLE_ID_OPTION变量:

在 Qt 6.1.0 之前的版本中,MACOSX_BUNDLE_GUI_IDENTIFIER 是一个有效的 CMake 属性,用于设置 macOS 应用的唯一标识符(Bundle ID)。

从 Qt 6.1.0 开始,这个属性被移除或更改了(通常建议直接在 Info.plist 文件中设置,或者使用新的机制)。如果在 6.1.0+ 的版本中仍然传递这个属性,CMake 可能会报错或发出警告。

目的:这段代码确保了你的 CMakeLists.txt 既能兼容旧版 Qt(< 6.1.0),也能在新版 Qt(>= 6.1.0)中正常编译,不报错。

cpp 复制代码
if(${QT_VERSION} VERSION_LESS 6.1.0)
  set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.TestCmake)
endif()

设置macOS相关属性:

${BUNDLE_ID_OPTION}: macOS 应用的唯一标识符(例如 com.company.myapp),这在应用分发和沙盒机制中非常重要;
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}: 设置应用的构建版本号(Bundle Version),通常显示在 macOS 的"显示简介"中,例如0.1.0;
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}: 设置应用的用户显示版本号(Bundle versions string, short),用户在界面上看到的版本号;
MACOSX_BUNDLE TRUE: 告诉 CMake 这是一个 macOS Bundle 应用(即生成 .app 文件夹),而不是一个命令行工具。

设置Windows相关属性:

WIN32_EXECUTABLE TRUE: 告诉 Windows 链接器,这是一个 GUI 应用程序,而不是控制台(Console)应用程序(当运行生成的 .exe 文件时,不会弹出黑色的 CMD 命令行窗口)。

合起来就是:

cpp 复制代码
set_target_properties(TestCmake PROPERTIES
    ${BUNDLE_ID_OPTION}
    MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
    MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
    MACOSX_BUNDLE TRUE
    WIN32_EXECUTABLE TRUE
)
  • 9,定义项目的安装规则(告诉构建系统(如 CMake),当你执行安装命令(例如 cmake --install 或 make install)时,应该把编译生成的文件复制到系统的哪个目录下)【主要用于 Linux、macOS 等类 Unix 系统以及动态链接库】;

引入 CMake 的标准模块,定义一套符合GNU 标准的安装路径变量,会自动根据当前操作系统和环境,设置好标准的目录名称。($ {CMAKE_INSTALL_BINDIR} 会被设置为 bin(存放可执行程序);
$ {CMAKE_INSTALL_LIBDIR} 会被设置为 lib 或 lib64(存放库文件);$ {CMAKE_INSTALL_DATAROOTDIR} 会被设置为 share(存放共享数据)。)

cpp 复制代码
include(GNUInstallDirs)

针对不同类型的构建产物,指定了不同的安装目的地(BUNDLE DESTINATION .:用于macOS (或 iOS) 平台,将这个 .app 包(macOS 上的应用程序,通常是一个文件夹)直接安装到安装前缀(CMAKE_INSTALL_PREFIX)的根目录下;LIBRARY DESTINATION $ {CMAKE_INSTALL_LIBDIR}: 指定动态库文件的安装位置,方便操作系统加载;RUNTIME DESTINATION $ {CMAKE_INSTALL_BINDIR} 的作用是指定可执行文件(以及 Windows 平台的动态库)在安装时的存放位置。);

cpp 复制代码
install(TARGETS TestCmake
    BUNDLE DESTINATION .
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
  • 10,把前面修改的部分汇总一下,得到如下的CMakeList文件;
cpp 复制代码
# 设置cmake的最低版本
cmake_minimum_required(VERSION 3.16)
# 定义项目名称、项目版本、变成语言
project(TestCmake VERSION 0.1 LANGUAGES CXX)

# 自动处理界面文件
set(CMAKE_AUTOUIC ON)
# 自动处理信号与槽
set(CMAKE_AUTOMOC ON)
# 自动处理资源文件
set(CMAKE_AUTORCC ON)

# 指定 C++ 语言标准版本为C++17
set(CMAKE_CXX_STANDARD 17)

# 强制要求指定的标准必须被满足。如果不支持C++17,CMake 配置阶段会直接报错并终止,不会尝试降级编译
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 保证代码的跨平台可移植性。 禁止编译器使用 GNU 扩展(如 GCC 的 gnu++17)或其他非标准的编译器扩展,强制使用严格的 ISO C++ 标准(即 c++17)。
set(CMAKE_CXX_EXTENSIONS OFF)


# 寻找Qt版本,并精确加载具体的 Qt 版本配置。
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets)


#  定义一个变量,用来集中存储项目所需的源文件列表。
set(PROJECT_SOURCES
        main.cpp
        mainwindow.cpp
        mainwindow.h
        mainwindow.ui
)

#  调用编译器(如 `g++, cl.exe`)将源代码编译成目标文件`(.o 或 .obj)`,
# 然后调用链接器将这些目标文件和依赖库链接成一个完整的、操作系统可以直接加载的二进制文件`exe`
add_executable(TestCmake
    ${PROJECT_SOURCES}
)

# 把库链接到目标执行程序上
target_link_libraries(TestCmake PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)

# 为构建目标设置特定的属性(发布专业、规范的软件时,必不可少)
set_target_properties(TestCmake PROPERTIES
    WIN32_EXECUTABLE TRUE
)

1.3 创建cmake文件

  • 1,创建form文件夹,把UI相关的文件移到文件夹内;

原始的文件夹:

完成操作后:

把main函数代码改为如下内容:

cpp 复制代码
// #include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    // MainWindow w;
    // w.show();
    return a.exec();
}

修改CMakeList.txt中的部分内容为:

cpp 复制代码
#  定义一个变量,用来集中存储项目所需的源文件列表。
set(PROJECT_SOURCES
        main.cpp
        # mainwindow.cpp
        # mainwindow.h
        # mainwindow.ui
)

重写构建后,得到如下项目结构:

  • 2,在根目录下创建form.cmake文件;

    pro文件中,添加如下代码:
cpp 复制代码
# 包含子项目配置
include(form.cmake)

重写构建后,得到如下的项目结构:

  • 3,在form.cmake中编写如下内容;
cpp 复制代码
# comm.cmake(类似.pri文件)
set(FORM_SOURCS
        form/mainwindow.cpp
        form/mainwindow.h
        form/mainwindow.ui
)

# 设置一个自定义变量来保存当前路径
set(FORM_INCLUDE_DIRS  ${CMAKE_CURRENT_SOURCE_DIR}/form/)
  • 4,修改CMakeList.txt文件,重写构建;

修改CMakeList.txt的部分内容为:

cpp 复制代码
add_executable(TestCmake
    ${PROJECT_SOURCES}
     ${FORM_SOURCS}
)

重写构建得到如下的项目结构:

  • 5,编写main函数;
cpp 复制代码
#include "form/mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}
  • 6,如果引入form中的文件,不想带form/前缀时,可以在CMakeList.txt中添加如下内容;
cpp 复制代码
# 核心功能【用于include】:为指定目标添加头文件搜索路径,确保编译器能够找到项目所需的头文件。
target_include_directories( TestCmake PRIVATE  ${FORM_INCLUDE_DIRS})

完整的CMakeList.txt文件内容为:

cpp 复制代码
# 设置cmake的最低版本
cmake_minimum_required(VERSION 3.16)
# 定义项目名称、项目版本、变成语言
project(TestCmake VERSION 0.1 LANGUAGES CXX)

# 自动处理界面文件
set(CMAKE_AUTOUIC ON)
# 自动处理信号与槽
set(CMAKE_AUTOMOC ON)
# 自动处理资源文件
set(CMAKE_AUTORCC ON)

# 指定 C++ 语言标准版本为C++17
set(CMAKE_CXX_STANDARD 17)

# 强制要求指定的标准必须被满足。如果不支持C++17,CMake 配置阶段会直接报错并终止,不会尝试降级编译
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 保证代码的跨平台可移植性。 禁止编译器使用 GNU 扩展(如 GCC 的 gnu++17)或其他非标准的编译器扩展,强制使用严格的 ISO C++ 标准(即 c++17)。
set(CMAKE_CXX_EXTENSIONS OFF)


# 寻找Qt版本,并精确加载具体的 Qt 版本配置。
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets)


#  定义一个变量,用来集中存储项目所需的源文件列表。
set(PROJECT_SOURCES
        main.cpp
        # mainwindow.cpp
        # mainwindow.h
        # mainwindow.ui
)


# 包含子项目配置
include(form.cmake)

#  调用编译器(如 `g++, cl.exe`)将源代码编译成目标文件`(.o 或 .obj)`,
# 然后调用链接器将这些目标文件和依赖库链接成一个完整的、操作系统可以直接加载的二进制文件`exe`
add_executable(TestCmake
    ${PROJECT_SOURCES}
     ${FORM_SOURCS}
)

# 把库链接到目标执行程序上
target_link_libraries(TestCmake PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)

# 核心功能【用于include】:为指定目标添加头文件搜索路径,确保编译器能够找到项目所需的头文件。
target_include_directories( TestCmake PRIVATE  ${FORM_INCLUDE_DIRS})

# 为构建目标设置特定的属性(发布专业、规范的软件时,必不可少)
set_target_properties(TestCmake PROPERTIES
    WIN32_EXECUTABLE TRUE
)

然后main函数可以如下的编写:

cpp 复制代码
#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

附录

完整的项目代码见此

相关推荐
ChineHe2 小时前
Golang并发编程篇_context包详解
开发语言·后端·golang
肥大毛2 小时前
C++入门学习---指针
开发语言·c++·学习
软件开发技术深度爱好者2 小时前
Python + Ursina设计一个有趣的3D小游戏
开发语言·python·3d
hnlgzb2 小时前
好像kotlin class和kotlin file都可以是activity?
android·开发语言·kotlin
草莓熊Lotso2 小时前
哈希表封装 myunordered_map/myunordered_set 实战:底层原理 + 完整实现
服务器·开发语言·数据结构·c++·人工智能·哈希算法·散列表
Data_agent2 小时前
Python高效实现Excel与TXT文本文件数据转换指南
开发语言·python·excel
a努力。2 小时前
阿里Java面试被问:如何分析Full GC的原因?jmap -histo和jmap -dump区别?
java·开发语言·后端·面试·架构
edjxj3 小时前
解决QT可执行文件在不同缩放大小的电脑上显示差异
服务器·数据库·qt
tang&5 小时前
【Python自动化测试】Selenium常用函数详解
开发语言·python·selenium