1.4 【程序员的自我修养】剖析项目构建流程
上一节中,我们在Qt Creator中使用向导创建了第一个Qt工程
点击按钮或者快捷键,就可以完成项目的构建和运行。
但是,这些看起来简单的过程,背后到底发生了什么呢?
作为一名合格的程序员,我们有必要探讨一番,这样即使以后出现了问题,编译报错,我们也能很快地定位到问题所在。
1. 项目文件说明
项目创建完毕后,包含5个文件,接下来做简要说明。
1.1 CMakeLists.txt — 构建配置文件
这是项目的构建配置文件,告诉CMake如何编译整个项目。关键指令如下:
| 关键指令 | 说明 |
|---|---|
| cmake_minimum_required | 要求CMake最低版本3.19 |
| project(HelloQt) | 定义项目名称为HelloQt |
| find_package(Qt6 …) | 查找并引入Qt6的Core、Widgets模块 |
| qt_add_executable(…) | 声明可执行目标,列出所有源文件 |
| target_link_libraries(…) | 链接Qt::Core和Qt::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)、标题等属性
- 定义控件布局,当前包含两个按钮:
btnStart和btnStop - 编译时由
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】,可以打开当前项目的构建设置界面,如下:
下面对三条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工具会去解析.ui的xml格式文件,依次读取其中的字段,比如width、height、property等,来生成对应的C++代码
为什么上面需要调用
ui->setupUi(this);才可以将界面上拖拽的按钮等控件显示出来
答:我们自己定义的MyWidget要显示,直接调用其show方法,如果子控件要随我们的窗口一起显示,那么子控件创建时的父窗口就要指定为我们的MyWidget
4. 运行设置
点击左侧的【Projects】,可以打开当前项目的运行设置界面,如下:
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会报错,如下:
之所以点击左下角的绿色按钮可以正常运行,是因为上图中指定了库文件的搜索路径,该路径是:C:\Qt\6.10.2\mingw_64\binQt6Core.dll、Qt6Widgets.dll等动态库都在这里
如果把该路径添加到系统的PATH环境变量,就可以双击运行了,请自行尝试~
但是如果要把开发完成的程序提供给客户,客户电脑上没有QT相关环境,如何运行呢?
有两种方式
4.2.1 windeployqt
windeployqt.exe是Qt官方提供的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
|
到此,项目构建流程就讲解完了,可能有些小伙伴有点蒙,建议大家,自己跳转下代码,多看几遍视频,相信你肯定就会恍然大悟的!







