上一节中,我们在Qt Creator中使用向导创建了第一个Qt工程
点击按钮或者快捷键,就可以完成项目的构建和运行。

但是,这些看起来简单的过程,背后到底发生了什么呢?
作为一名合格的程序员,我们有必要探讨一番,这样即使以后出现了问题,编译报错,我们也能很快地定位到问题所在。

1. 项目文件说明

项目创建完毕后,包含5个文件,接下来做简要说明。

1.1 CMakeLists.txt — 构建配置文件

这是项目的构建配置文件,告诉CMake如何编译整个项目。关键指令如下:

关键指令 说明
cmake_minimum_required 要求CMake最低版本3.19
project(HelloQt) 定义项目名称为HelloQt
find_package(Qt6 …) 查找并引入Qt6CoreWidgets模块
qt_add_executable(…) 声明可执行目标,列出所有源文件
target_link_libraries(…) 链接Qt::CoreQt::Widgets
install(…) 定义安装规则,支持打包部署

1.2 main.cpp — 程序入口

这是整个应用程序的入口点,包含main()函数。

int main(int argc, char *argv[]) {

    // 1. 创建Qt应用程序对象,管理事件循环和全局资源
    QApplication a(argc, argv);

    // 2. 创建主窗口对象,并调用其 show 方法,将窗口显示出来
    MyWidget w;
    w.show();

    // 3. 调用 QApplication 类的 exec 方法
    // 程序并不会退出,而是进入事件循环, 等待用户交互,直到窗口关闭(比如点击了窗口右上角的关闭按钮)
    return a.exec();
}

1.3 mywidget.h — 窗口类头文件

这是MyWidget类的声明文件,定义类的结构。

QT_BEGIN_NAMESPACE
// 在此声明一个 MyWidget 类,这个类定义在 Ui 命名空间中
// 因为下面会定义一个 Ui::MyWidget 类型的指针 *ui
namespace Ui
{
    class MyWidget;
}
QT_END_NAMESPACE

// 自定义的 MyWidget 类,要继承自Qt框架提供的QMainWindow/QDialog/QWidget这三个类其中之一,才可以正常显示出来
class MyWidget : public QWidget
{
    // 启用 Qt 的信号/槽机制(MOC 元对象系统必须)
    Q_OBJECT 

public:
    // 声明构造函数、析构函数
    MyWidget(QWidget *parent = nullptr);
    ~MyWidget();

private:
    // 定义一个 Ui::MyWidget 类型的指针 *ui
    // Ui::MyWidget 这个类定义在 ui_mywidget.h 中(可以 Ctrl+单击 跳转过去)
    // 这个 Ui::MyWidget 类,本身是空实现,但是它继承自 Ui_MyWidget 类
    
    // Ui_MyWidget 类,是和UI设计界面一一对应的,也就和mywidget.ui这个xml文件一一对应的
    // 至于为什么一个C++代码中类的对象,会和mywidget.ui这个xml文件一一对应,本节课下面就会讲到,暂且知道即可
    Ui::MyWidget *ui;
};

1.4 mywidget.cpp — 窗口类实现文件

这是MyWidget类的逻辑实现文件,编写具体功能代码的地方
所有按钮点击、信号槽连接、业务逻辑都写在这个文件里

// 引入由 mywidget.ui 自动生成的界面头文件
#include "ui_mywidget.h"

// 在初始化列表中,对ui指针进行初始化赋值
MyWidget::MyWidget(QWidget *parent) : QWidget(parent), ui(new Ui::MyWidget) {

    // 把当前 MyWidget 窗口,也就是this指针,作为根容器,设置到ui对象中
    // 我们 在ui 界面中拖拽的控件比如按钮、文本框等,都会以当前窗口作为父窗口进行创建
    // 只有这样,ui 界面中拖放的控件才会在随 MyWidget 一起显示出来。
    ui->setupUi(this);
}

MyWidget::~MyWidget() {
    // 既然在构造函数中 new 了ui对象,在析构中要delete销毁
    delete ui;
}

1.5 mywidget.ui — 界面设计文件

这是用Qt Designer可视化设计的界面描述文件,格式为XML

  • 描述窗口尺寸(800×600)、标题等属性
  • 定义控件布局,当前包含两个按钮:btnStartbtnStop
  • 编译时由uic工具自动转换为ui_mywidget.h,开发者无需手动编辑此文件

2. 基本概念扫盲

新建工程时,默认会创建一个.ui文件,双击该文件,就可以跳转到设计模式,在左侧的工具栏,可以拖拽各种控件比如按钮、文本框等到窗口上
ui文件其实是一个xml格式的文件, 每当我们拖拽一个控件, 都可以查看到该文件的变化

到此,我们基本了解了窗口是如何显示的,其实还没有百分百了解,比如你还不知道:

  • ui_mywindow.h这个文件是怎么来的?
  • 为什么说Ui::MyWidget这个C++中的类,是和xml格式的mywidget.ui文件一一对应的?
  • 为什么上面需要调用ui->setupUi(this);才可以将界面上拖拽的按钮等控件显示出来

别着急,下面马上就为你揭秘Qt项目的构建流程,在这之前有必要先看几个概念:

  • gcc/g++
  • Make/Ninja
  • CMake

作为一名Qt开发者,需要有C/C++基础,想必对这几个概念并不陌生,在此抛砖引玉,做一个总结

2.1 gcc/g++

对于一个源文件main.cpp,如何编译成可执行文件呢?

g++ -o main main.cpp 

2.2 make/ninja

以上只有一个源文件main.cpp,然而一个实际的工程中,其源文件一般有很多,并且通常按照功能模块,放在若干个目录中
此时如果每次都手动一条条执行编译命令,很显然是不现实的,因此就有了Makefile文件,如下:

main: main.cpp
    g++ -o main main.c

此时,在命令行直接执行make命令,就可以读取Makefile,进而执行g++ -o main main.c来编译出可执行文件main
可见,引入了Makefile文件之后,只需执行make命令即可

Makefile文件用于描述整个工程的编译、链接的规则。
比如,工程中的哪些源文件需要编译(根据时间戳,只编译修改过的文件)以及如何编译、编译顺序,最终链接成可执行文件或库文件
Makefile有自己的书写格式、关键字、函数,像 C 语言有自己的格式、关键字和函数一样。
Makefile文件编写完毕之后,编译整个工程你所要做的唯一的一件事就是执行make命令,就可以将整个工程进行 “自动化编译”,极大提高了效率。

2.3 CMake

然而,手写Makefile是比较困难而且容易出错,尤其在进行跨平台开发时,必须针对不同平台分别编写Makefile,会增加跨平台开发难度。
CMake会根据项目文件CMakeLists.txt文件内容,自动生成Makefile或者build.ninja

3. Qt 项目构建流程

QT抛弃QMake,全面拥抱CMake之后,HelloQt项目的构建流程:

┌─────────────────┐
│ CMakeLists.txt  │ (用户编写)
└────────┬────────┘
         ↓
┌─────────────────┐
│     cmake       │ (配置和生成)
└────────┬────────┘
         ↓
┌─────────────────┐
│  Makefile       │ (生成的构建文件)
│  build.ninja    │
│  .sln/.vcxproj  │
└────────┬────────┘
         ↓
┌─────────────────┐
│  make/ninja/    │ (实际构建工具)
│  msbuild        │
└────────┬────────┘
         ↓
┌─────────────────┐
│  可执行文件/库   │ (最终产物)
└─────────────────┘

3.1 3条CMake命令

点击左侧的【Projects】,可以打开当前项目的构建设置界面,如下:
qt-base

下面对三条cmake命令详解:

  • 配置
# -S F:/qt_project/HelloQt 
# 指定源码目录(Source)
# 
# -B F:/qt_project/HelloQt/build/Desktop_Qt_6_10_2_MinGW_64_bit-Debug
# 指定构建目录(Build)
# 
# 作用:CMake 读取 CMakeLists.txt,并检测编译器(MinGW g++)、查找 Qt6 库,在构建目录中生成 build.ninja 等构建文件
cmake.exe -S F:/qt_project/HelloQt -B F:/qt_project/HelloQt/build/Desktop_Qt_6_10_2_MinGW_64_bit-Debug
  • 构建
# --build	进入编译阶段(区别于 -S/-B 的配置阶段)
# F:\qt_project\HelloQt\build\Desktop_Qt_6_10_2_MinGW_64_bit-Debug  指定构建目录
# --target all	指定构建目标为 all(编译所有目标:CMakeLists.txt 中定义的所有可执行文件和库)
#
# 这条命令是编译阶段,CMake 本身并不直接编译,而是作为统一入口,自动调用底层构建工具:
# cmake --build  →  检测构建目录使用的构建系统
#                              ↓
#               发现 build.ninja(Ninja 构建系统)
#                              ↓
#               自动调用:ninja.exe -f build.ninja all
# 你无需关心底层是 Ninja 还是 Make,cmake --build 统一封装了调用。
cmake.exe --build F:/qt_project/HelloQt/build/Desktop_Qt_6_10_2_MinGW_64_bit-Debug --target all
  • 清理
# --build	进入编译阶段操作
# F:\qt_project\HelloQt\build\Desktop_Qt_6_10_2_MinGW_64_bit-Debug  指定构建目录
# --target clean	执行 清理目标,删除编译产物
#
# CMake 将此命令转交给底层 Ninja 执行:
# cmake --build ... --target clean
#                ↓
#   ninja.exe -f build.ninja clean
#                ↓
#   删除所有由编译产生的中间文件和输出文件
cmake.exe --build F:/qt_project/HelloQt/build/Desktop_Qt_6_10_2_MinGW_64_bit-Debug --target clean

点击【Build】->【Build Project “HelloQt”】,在底部的 “Compile Output” 窗口显示的编译信息:

00:18:12: Starting: "C:\Program Files\CMake\bin\cmake.exe" --build F:/qt_project/HelloQt/build/Desktop_Qt_6_10_2_MinGW_64_bit-Debug --target all
[1/5 0.9/sec] Automatic MOC and UIC for target HelloQt
[2/5 0.4/sec] Building CXX object CMakeFiles/HelloQt.dir/main.cpp.obj
[3/5 0.7/sec] Building CXX object CMakeFiles/HelloQt.dir/HelloQt_autogen/mocs_compilation.cpp.obj
[4/5 0.9/sec] Building CXX object CMakeFiles/HelloQt.dir/mywidget.cpp.obj
[5/5 1.0/sec] Linking CXX executable HelloQt.exe
00:18:17: The command "C:\Program Files\CMake\bin\cmake.exe --build F:/qt_project/HelloQt/build/Desktop_Qt_6_10_2_MinGW_64_bit-Debug --target all" finished successfully.
00:18:17: Elapsed time: 00:05.

3.2 MOC 与 UIC

我们知道在编译项目时,编译器编译的是cpp/.h文件,而ui文件本身是xml格式的,如何参与到编译过程中的呢?
答:以上的编译输出的第1/5步已经给出了答案 – UIC

a、AUTOMOC 处理
┌─────────────────────────────────────────────┐
│            moc.exe 编译阶段                  │
│  moc.exe mywidget.h                         │
│       → 生成 EWIEGA46WW/moc_mywidget.cpp    │
│  moc.exe xxx.h                              │
│       → 生成 EWIEGA46WW/moc_xxx.cpp         │
└─────────────────────────────────────────────┘
                       ↓
┌─────────────────────────────────────────────┐
│        生成 mocs_compilation.cpp            │
│  // This file is autogenerated.             │
│  #include "EWIEGA46WW/moc_mywidget.cpp"     │
│  #include "EWIEGA46WW/moc_xxx.cpp"          │
└─────────────────────────────────────────────┘
EWIEGA46WW 是根据源文件路径哈希生成的目录名,用于避免不同目录下同名文件冲突


b、AUTOUIC 处理
uic.exe mywidget.ui
       ↓
ui_mywidget.h(包含 setupUi() 函数的完整界面代码)


c、编译源文件 → 目标文件
   main.cpp             ──► main.cpp.obj
   mywidget.cpp         ──► mywidget.cpp.obj
   mocs_compilation.cpp ──► mocs_compilation.cpp.obj


d、链接
   main.cpp.obj
   mywidget.cpp.obj      
   mocs_compilation.cpp.obj   ──►   g++.exe 链接 ──► HelloQt.exe
   Qt6Core.dll
   Qt6Widgets.dll

3.3 详解 ui->setupUi(this)

一条规则:QT中,父窗口显示时,会将其子窗口一起显示出来。

按Ctrl键,点击setupUi函数,跳转到ui_mywidget.h文件中的实现:

class Ui_MyWidget
{
public:
    QPushButton *btnStart;
    QPushButton *btnStop;

    void setupUi(QWidget *MyWidget)
    {
        if (MyWidget->objectName().isEmpty())
            MyWidget->setObjectName("MyWidget");
        MyWidget->resize(800, 600);
        // 只有将 btnStart 的父窗口设置为传递过来的MyWidget,才能在MyWidget show出来时,才将 btnStart 也显示出来
        btnStart = new QPushButton(MyWidget);
        btnStart->setObjectName("btnStart");
        btnStart->setGeometry(QRect(230, 260, 131, 51));
        // 只有将 btnStop 的父窗口设置为传递过来的MyWidget,才能在MyWidget show出来时,才将 btnStop 也显示出来
        btnStop = new QPushButton(MyWidget);
        btnStop->setObjectName("btnStop");
        btnStop->setGeometry(QRect(370, 260, 131, 51));

        retranslateUi(MyWidget);

        QMetaObject::connectSlotsByName(MyWidget);
    } // setupUi

    void retranslateUi(QWidget *MyWidget)
    {
        MyWidget->setWindowTitle(QCoreApplication::translate("MyWidget", "MyWidget", nullptr));
        btnStart->setText(QCoreApplication::translate("MyWidget", "START", nullptr));
        btnStop->setText(QCoreApplication::translate("MyWidget", "STOP", nullptr));
    } // retranslateUi

};

namespace Ui {
    // 这个Ui命名空间中的 MyWidget 类,本身是空实现,但是它继承自 Ui_MyWidget 类
    // Ui_MyWidget 类就是和 mywidget.ui 这个xml文件一一对应的。
    
    // 比如在ui设计界面拖放两个按钮,可以在其xml文件中看到两个按钮的属性
    // uic 工具就会根据xml文件中的属性,在 ui_mywidget.h 文件中,生成对应的代码
    // 比如在ui设计界面设置两个按钮的文本为“START”和“STOP”,调整坐标,那么在生成的这个.h文件中,就会生成对应的代码,如上↑
    class MyWidget: public Ui_MyWidget {};
} // namespace Ui

至此,我们重新回到上面的三个问题,应该就可以回答了:

  • ui_mywindow.h这个文件是怎么来的?
    答:通过Qt提供的uic工具,自动将.ui文件,转换为编译器可以编译的.h文件


  • 为什么说Ui::MyWidget这个C++中的类,是和xml格式的mywidget.ui文件一一对应的?
    答:uic工具会去解析.uixml格式文件,依次读取其中的字段,比如widthheightproperty等,来生成对应的C++代码


  • 为什么上面需要调用ui->setupUi(this);才可以将界面上拖拽的按钮等控件显示出来
    答:我们自己定义的MyWidget要显示,直接调用其show方法,如果子控件要随我们的窗口一起显示,那么子控件创建时的父窗口就要指定为我们的MyWidget

4. 运行设置

点击左侧的【Projects】,可以打开当前项目的运行设置界面,如下:
qt-base

4.1 命令行参数

如果指定了命令行参数,可以在main.cpp中,添加如下代码来打印命令行参数,如下:

qDebug() << "参数个数:" << argc;
qDebug() << "参1:" << argv[0];
qDebug() << "参2:" << argv[1];
qDebug() << "参3:" << argv[2];

执行结果:

参数个数: 3
参1: F:\qt_project\HelloQt\debug\HelloQt.exe
参2: 123
参3: 456

4.2 独立运行

直接双击HelloQt.exe会报错,如下:
qt-base

之所以点击左下角的绿色按钮可以正常运行,是因为上图中指定了库文件的搜索路径,该路径是:C:\Qt\6.10.2\mingw_64\bin
Qt6Core.dllQt6Widgets.dll等动态库都在这里
如果把该路径添加到系统的PATH环境变量,就可以双击运行了,请自行尝试~

但是如果要把开发完成的程序提供给客户,客户电脑上没有QT相关环境,如何运行呢?
有两种方式

4.2.1 windeployqt

windeployqt.exeQt官方提供的Windows部署工具
它会自动收集一个Qt应用程序运行所需的所有DLL、插件、QML块等依赖文件,将它们复制到可执行文件所在目录
使程序可以在没有安装Qt的机器上独立运行

工具位置:C:/Qt/6.10.2/mingw_64/bin/windeployqt.exe
基本用法:

windeployqt.exe  HelloQt.exe

4.2.2 制作精美安装包

程序开发完成后,需要提供给客户使用。
通常是制作一个单独的安装包文件给客户,这样客户拿到安装文件后,直接双击即可安装到自己的电脑上使用。

为什么要制作安装包?

  • 便于分发和部署
    安装包使得软件的分发变得简单快捷。客户只需下载或接收一个文件,即可安装软件。

  • 提升用户体验
    安装包通常提供定制化的安装选项,如选择安装组件、设置安装路径等,满足客户的个性化需求。

  • 提升品牌形象
    在安装包中融入公司的品牌形象和软件特色,有助于提升公司的品牌知名度和专业形象,增强客户对公司和产品的信任与认可。

可以参考《软件打包实战》课程,模仿了腾讯QQ安装包、美图秀秀安装包

仿腾讯QQ效果:

示例图
QQ_Setup.exe
提取码: qq12

仿美图秀秀效果:

示例图
MeiTuXiuXiu_Setup.exe
提取码: meit

到此,项目构建流程就讲解完了,可能有些小伙伴有点蒙,建议大家,自己跳转下代码,多看几遍视频,相信你肯定就会恍然大悟的!