第一个 Qt 项目:Hello World 的完整拆解------从 main 函数到事件循环
引言
在上一篇文章中,我们成功搭建了 Qt 开发环境,点击运行按钮,看到了一个空白的窗口弹出。
但对于一个 C++ 开发者来说,仅仅"让程序跑起来"是远远不够的。
真正值得思考的问题是:
那几行自动生成的代码,到底在背后做了什么?
cpp
#include <QApplication>
#include "mainwindow.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
这段代码只有短短 7 行。但如果你把这几行代码当做一个"黑盒"来使用,你就错过了 Qt 中最核心、最基础的设计思想。
在这一篇中,我们不急着去写复杂的界面,而是把这几行代码彻底拆解清楚。
我们会依次回答以下几个问题:
argc和argv能做什么?------ 命令行参数与 Qt 应用。QApplication到底是什么?它和QCoreApplication有什么区别?w.show()真的把窗口"显示"出来了吗?a.exec()为什么不会让程序立刻退出?事件循环到底在做什么?- 这个过程中,MOC(元对象编译器) 悄然参与了哪一步?
当你理解了这几行代码的每一层含义之后,你才算真正入了 Qt 的门。
一、从 main 函数的参数说起:命令行应用与 GUI 应用
任何 C++ 程序的入口都是 main 函数。它的两个参数------argc(参数个数)和 argv(参数值数组)------经常被初学者忽略。
但在现实工程中,命令行参数非常重要。例如:
bash
# 以调试模式启动
./myapp --debug
# 打开特定文件
./myapp D:/Projects/demo.txt
# 无界面模式(用于自动化测试)
./myapp --headless
Qt 提供了非常优雅的方式来处理这些参数,而这就是 QApplication 构造函数的隐藏能力之一。
默认行为:Qt 自动处理标准命令行选项
当我们构造 QApplication 时,Qt 会自动解析一些标准命令行选项。例如:
| 命令行参数 | 作用 |
|---|---|
-style=windows |
强制使用 Windows 风格 |
-style=fusion |
强制使用 Fusion 风格 |
-reverse |
设置布局方向为从右到左 |
-qmljsdebugger=... |
启用 QML 调试 |
这些参数不需要你写任何额外代码,Qt 在构造 QApplication 时就已经帮你处理好了。
一个容易忽略的细节:Qt 可能会修改 argc 和 argv
QApplication 构造时会检查命令行参数,并处理 Qt 自己认识的参数。
因此,传入 QApplication 的 argc 和 argv 并不一定会保持原样。
这也是为什么 Qt 官方建议:如果程序自己还需要解析命令行参数,通常应该先构造 QApplication,再使用 QCommandLineParser 处理业务参数。
Qt 还提供 QCommandLineParser 处理自定义参数,这部分会在后续工程化章节介绍。
二、QApplication 和它的兄弟姐妹们
接下来,我们来看 QApplication a(argc, argv); 这一行。
很多人在写这一行代码时,只是把它当做"启动 Qt 的魔法咒语"。但实际上,QApplication 在一个 Qt 程序中承担着非常重要的职责。
QApplication 在做什么?
当构造 QApplication 对象时,Qt 会完成 QWidget 应用所需要的初始化工作,包括:
- 初始化应用程序相关的 GUI 环境;
- 根据系统设置初始化字体、调色板等;
- 初始化 QWidget 相关功能;
- 创建并准备应用程序级事件处理环境;
- 解析 Qt 支持的部分标准命令行参数;
- 管理应用程序中的顶层窗口、剪贴板、鼠标指针等全局 GUI 状态。
真正进入主事件循环,则是在后面的 app.exec() 中完成。
Qt 的三种"应用"类型
实际上,Qt 提供了三个层级不同的 Application 类,它们的继承关系如下:
QCoreApplication (无 GUI,纯命令行/服务)
↑
QGuiApplication (GUI 基础设施,但不提供 QWidget)
↑
QApplication (在 QGuiApplication 基础上增加 QWidget 支持)
这三者的选择非常简单:
| 类名 | 适用场景 |
|---|---|
| QCoreApplication | 纯控制台应用、后台服务、批处理程序(不需要任何 GUI 功能)。 |
| QGuiApplication | 使用 Qt Quick / QML 或纯 OpenGL 构建的应用(不依赖 Qt Widgets 模块)。 |
| QApplication | 使用 QWidget、QMainWindow 和传统 Qt 控件的桌面应用。 |
下面这种组合方式能使用吗?
c++
QCoreApplication app(argc, argv);
QWidget widget;
答案当然是不能。Widget 属于 Qt Widgets 模块,需要 QApplication 提供对应的初始化环境。
因此,对于 QWidget 应用程序,应该使用 QApplication,而不是 QCoreApplication。
可以简单理解为:
QCoreApplication
↓
基础事件循环和应用程序环境
QGuiApplication
↓
增加 GUI 基础能力
QApplication
↓
增加 QWidget 所需要的能力
生命周期问题:为什么通常把 QApplication 放在栈上?
你可能会想到:
QApplication *app = new QApplication(argc, argv);
这种写法并不是绝对错误。
但 Qt 示例以及实际工程中通常直接使用:
QApplication app(argc, argv);
原因是 C++ 的 RAII 机制可以自动管理它的生命周期。
当 main() 结束时:
app.exec()
↓
事件循环退出
↓
main() 返回
↓
QApplication 自动析构
↓
Qt 清理应用程序相关资源
因此,对于生命周期与 main() 一致的对象,栈对象是更加自然的选择
在 Qt 的官方规范中,QCoreApplication 及其子类必须 在 main 函数中构造,并且在 main 函数返回之前不能析构。栈对象自动满足了这一要求。
三、w.show() 到底做了什么?
很多人想当然地以为:w.show() 就是把窗口"画"到屏幕上。
从逻辑上说,没错。但从实现角度来说,show() 做了远比"绘图"复杂得多的事情。
show() 的核心工作流程如下:
- 创建底层窗口句柄 (Windows 上的
HWND,Linux X11 上的Window)。 - 将 Qt 窗口(
QWidget或QMainWindow)与操作系统原生窗口关联。 show()会使 QWidget 进入可见状态,并在需要时创建/显示对应的原生窗口资源。- 窗口显示后,Qt 的事件系统会根据需要产生并处理绘制事件 ,最终由 QWidget 的绘制机制完成界面绘制。
关键认知:show() 只是把窗口映射到屏幕上,并标记为"需要绘制"。真正的绘制动作,是在事件循环运行之后,由 QPaintEvent 触发的。
也就是说,如果在调用 w.show() 之后立即调用 return 0;(不进入事件循环),程序会直接退出,你甚至看不到窗口闪现------因为它还没来得及处理绘制事件。
对于一个正常的 Qt GUI 应用程序来说,show() 通常需要配合 app.exec() 使用。
show() 负责让窗口进入可见状态,而 exec() 负责进入应用程序的主事件循环,使程序能够持续处理用户输入、窗口系统事件、定时器等事件。
四、事件循环:深入理解 a.exec()
这是 Qt 初学者最难理解、但却最重要的概念。
什么是事件循环?
通俗地说,事件循环是一个无限循环,它不断地从"事件队列"中取出事件,并将其分发给对应的对象进行处理。
它的伪代码逻辑大致如下,以下代码只是帮助理解事件循环的伪代码,并不是 Qt
exec()的实际源码。
cpp
int exec()
{
while (!exitRequested) {
Event event = waitForNextEvent();
dispatchEvent(event);
}
return exitCode;
}
为什么需要事件循环?
如果没有事件循环,我们的程序会瞬间执行完毕并退出。
有了事件循环,程序会一直运行,等待并处理各种事件,例如鼠标点击、键盘输入、窗口缩放、定时器事件以及网络 I/O 相关事件。
用现实世界的例子来理解
例子一:餐厅服务员
想象一个餐厅服务员:
- 他站在门口,等待客人进来(等待事件);
- 客人进来后,他引导客人入座,并记录点单(处理事件);
- 点单完成后,他又回到门口等待下一位客人(回到循环)。
这个服务员不会因为"处理完一桌客人"就下班。他会持续不断地等待、处理、再等待。
操作系统的事件循环(或者说 Qt 的事件循环)就是这样一个"服务员"。它永远不会主动停止,除非你告诉它"打烊了"(调用 quit())。
例子二:邮件客户端
当你打开 Outlook 或 Foxmail 时:
- 程序不会运行 1 秒就退出;
- 它持续运行,等待你点击"收件箱";
- 它持续运行,等待邮件到达的通知(网络事件);
- 它持续运行,等待你双击一封邮件。
这种"一直在等着,随时准备响应"的机制,就是事件循环。
事件循环是阻塞的,但不是"死"的
初学者常常会疑惑:a.exec() 阻塞了主线程,那程序为什么还能响应点击和动画?
关键在于:a.exec() 的阻塞是一种"主动等待",而不是"忙等"。
- 忙等 :
while(true) { check(); }------ 持续占用 CPU,导致 CPU 使用率 100%。 - 主动等待 :
waitForEvent()------ 将线程挂起,操作系统在没有事件时不会分配给该线程 CPU 时间片;只有当鼠标移动、键盘敲击或定时器超时等事件发生时,操作系统才会唤醒该线程。
"阻塞"描述的是 main() 这一调用路径没有继续向下执行,并不意味着主线程停止工作。
例如:
c++
return app.exec();
qDebug() << "这里什么时候执行?";
答案是:
exec()返回之后才会执行。
但在 exec() 内部:
主线程
↓
事件循环
├── 鼠标事件
├── 键盘事件
├── Paint事件
├── Timer事件
├── Socket事件
└── ...
所以:主线程被 exec() 阻塞 ≠ 主线程什么都不干。
这种机制不仅高效,而且让 GUI 程序能够在单线程模式下平滑运行------所有 UI 操作在主线程串行执行,不需要复杂的锁机制,这也是 Qt 要求 UI 操作必须在主线程的根本原因之一。
exec() 什么时候退出?
exec() 会一直运行,直到以下情况发生:
- 调用了
QCoreApplication::quit(); - 当最后一个可见的顶层窗口关闭时,Qt 默认会根据
quitOnLastWindowClosed的设置退出应用程序事件循环。; - 用户强制终止进程。
exec() 的返回值是一个 int,代表退出代码。在脚本或自动化测试中,这个退出码可以用来判断程序是正常退出还是异常终止。
五、链接与 MOC:构建过程中看不见的工作
这一节虽然是入门篇,但既然我们第一篇就聊了 Qt 与 C++ 的关系,这里需要提一下构建过程中的幕后工作。
我们在第二篇中提到,使用 CMake 构建时,编译输出中会出现 autogen/moc_compilation.cpp.obj。
在 Hello World 中,MOC 参与了吗?
答案是:参与了,但只参与了一点点。
在我们前面使用 Qt Creator 创建的典型 Widgets 项目中,MainWindow 通常包含 Q_OBJECT,因此 CMake 的 AUTOMOC 会在构建过程中调用 MOC。
但需要注意:并不是所有 Qt 程序都必须使用 MOC。
MOC 是针对使用 Qt 元对象扩展的类而存在的,典型判断标志就是类声明中的 Q_OBJECT 等宏
MainWindow 类定义中包含了 Q_OBJECT 宏:
c++
class MainWindow : public QMainWindow
{
Q_OBJECT
// ...
};
这个宏告诉 MOC:这个类需要元对象信息。MOC 在预处理阶段读取 mainwindow.h,生成一个额外的 C++ 源文件(例如 moc_mainwindow.cpp),其中包含了 MainWindow 的元对象表(QMetaObject)和信号/槽的索引信息。
但在 Hello World 这个阶段,MainWindow 并没有定义任何自定义信号或槽。为什么还需要 MOC?
因为 QMainWindow 本身从 QObject 继承,而 Q_OBJECT 宏的另一个作用是启用动态属性、运行时类型信息(inherits()、metaObject() 等)。
有一个需要明确:QObject 本身并不要求你的每一个派生类都写 Q_OBJECT
例如:
c++
class MyObject : public QObject
{
};
这个类仍然是 QObject 派生类。
但是:
c++
class MyObject : public QObject
{
Q_OBJECT
};
才会让这个派生类本身拥有由 MOC 生成的完整元对象信息,例如:
- 自己声明的 signals
- slots
- properties
- 自己的元对象信息
- 动态类型相关能力等
Qt 官方对 MOC 的定位也是:读取包含 Q_OBJECT 的类声明,并生成元对象代码;这些元对象代码用于信号槽、运行时类型信息、动态属性系统等。
这也是为什么 Qt 强调:任何从 QObject 派生的类,只要需要信号槽、属性或运行时类型信息,就必须 在类声明中包含
Q_OBJECT宏,否则 MOC 将跳过它。
六、对象生命周期:从创建到析构
到这里,我们已经知道:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow w;
w.show();
return app.exec();
}
但还有一个非常重要的问题:
app 和 w 是什么时候创建的?什么时候销毁的?
这其实涉及 C++ 中非常基础、也非常重要的一个概念------对象生命周期。
QApplication 和 MainWindow 是什么时候创建的?
程序进入 main() 后,按照代码执行顺序:
c++
QApplication app(argc, argv);
首先创建 QApplication 对象。然后:
c++
MainWindow w;
创建 MainWindow 对象。因此,可以简单理解为:
进入 main()
↓
创建 QApplication
↓
创建 MainWindow
↓
显示 MainWindow
↓
进入事件循环
此时两个对象都处于存活状态:
main()
│
├── QApplication app
│
└── MainWindow w
exec() 并不会销毁这些对象
执行:
c++
return app.exec();
以后,程序进入事件循环。此时:
c++
MainWindow w;
这个对象仍然存在。
app.exec() 并不会创建一个新的 MainWindow,也不会销毁 MainWindow。
它只是让当前线程进入事件循环,不断处理:
鼠标事件
键盘事件
绘制事件
定时器事件
窗口事件
网络 I/O 事件
......
所以在事件循环运行期间,可以理解为:
QApplication app ────────────────┐
│
MainWindow w ───────────────┐ │
│ │
app.exec() │ │
事件循环运行 │ │
│ │
用户不断操作 │ │
│ │
exec() 返回以后发生什么?
当应用程序退出时,事件循环结束:
c++
app.exec()
返回一个整数退出码。例如:
c++
int exitCode = app.exec();
return exitCode;
这时候程序才会继续执行 main() 后面的代码。
对于:
c++
return app.exec();
可以理解成:
c++
int exitCode = app.exec();
return exitCode;
当 return 执行以后,main() 开始结束。
这时 C++ 会自动销毁 main() 中创建的局部对象。
C++ 对象是按照什么顺序析构的?
这里有一个非常重要的 C++ 规则:
局部对象按照创建顺序构造,按照创建顺序的逆序析构。
我们的代码是:
c++
QApplication app(argc, argv);
MainWindow w;
所以:
构造:
QApplication
↓
MainWindow
析构:
MainWindow
↓
QApplication
也就是说,MainWindow 是后创建的,所以它会先析构。
完整过程可以理解为:
程序启动
↓
创建 QApplication
↓
创建 MainWindow
↓
w.show()
↓
app.exec()
↓
事件循环运行
↓
用户关闭程序
↓
exec() 返回
↓
main() 返回
↓
MainWindow 析构
↓
QApplication 析构
↓
程序退出
为什么这个顺序很重要?
因为 MainWindow 属于 Qt GUI 对象,而 QApplication 是整个 Qt GUI 应用程序环境。
因此在正常的生命周期设计中:
QApplication
│
└── MainWindow
应该保证:
QApplication 的生命周期覆盖整个 GUI 对象的生命周期。
这也是为什么通常把:
c++
QApplication app(argc, argv);
放在 main() 的最前面,并让它一直存活到 main() 结束。
再结合对象树理解
Qt 还有一个非常重要的机制------QObject 父子对象树。
例如:
c++
QWidget parent;
QPushButton button(&parent);
这里 button 的父对象是 parent。
当 parent 被销毁时,Qt 的 QObject 对象树机制会自动处理它的子对象。
因此,Qt 中实际上存在两套需要同时理解的生命周期机制:
C++ 生命周期
↓
局部对象 / 栈对象 / 析构顺序
Qt 对象树
↓
QObject 父子关系 / 子对象自动销毁
这两个概念并不是一回事。
理解它们之后,后面学习:
- QWidget 父子关系
- QObject 对象树
- 动态创建控件
deleteLater()- Qt 内存管理
都会更加容易。
用一句话总结
对于最基本的 Qt Widgets 程序,可以记住这条生命周期:
创建 QApplication
↓
创建 MainWindow
↓
显示窗口
↓
进入事件循环
↓
用户持续操作
↓
事件循环退出
↓
MainWindow 析构
↓
QApplication 析构
↓
程序结束
这就是一个最简单 Qt GUI 程序从启动、运行到退出的完整生命周期。
七、为什么 main() 不能直接 return 0?
到这里,我们已经知道:
c++
MainWindow w;
w.show();
会让窗口进入可见状态。
那么一个很自然的问题来了:
既然窗口已经 show() 了,为什么不能直接 return 0?
例如下面这样的代码为什么不行?
c++
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow w;
w.show();
return 0;
}
代码确实可以编译,但程序几乎马上就结束
首先要明确一点:
这段代码本身并不一定编译报错。
真正的问题发生在运行时。
程序的执行过程是:
进入 main()
↓
创建 QApplication
↓
创建 MainWindow
↓
调用 w.show()
↓
窗口进入可见状态
↓
执行 return 0
↓
main() 结束
↓
w 析构
↓
app 析构
↓
进程退出
也就是说:
show()并不会让程序一直运行。
它只是让窗口进入可见状态。
为什么 show() 不能让程序一直运行?
因为 show() 和"程序持续运行"其实是两件不同的事情。
例如:
c++
w.show();
解决的是:
窗口应该显示出来。
而:
c++
app.exec();
解决的是:
程序接下来应该持续运行,并不断处理事件。
可以把它们理解成:
show()
↓
让窗口"出现"
exec()
↓
让程序"活着"
因此:
w.show();
return 0;
相当于:
把窗口打开
↓
立刻关掉整个程序
那么 return app.exec() 到底解决了什么?
正确的代码是:
c++
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow w;
w.show();
return app.exec();
}
这里最关键的是:
c++
app.exec();
它会进入 Qt 的主事件循环。
可以把它简单理解成:
c++
while (程序没有要求退出)
{
等待事件;
获取事件;
将事件分发给对应对象;
处理事件;
}
当然,这只是为了帮助理解的伪代码,并不是 Qt 内部实际源码。
运行起来以后到底发生了什么?
假设我们启动程序:
main()
↓
QApplication
↓
MainWindow
↓
w.show()
↓
app.exec()
进入 exec() 后,程序不会退出。
此时事件循环开始工作:
app.exec()
│
▼
┌──────────────┐
│ 等待事件 │
└──────┬───────┘
│
收到一个事件
↓
获取这个事件
↓
分发给目标对象
↓
对象处理事件
↓
回到事件循环
↓
等待下一个事件
例如用户移动鼠标:
用户移动鼠标
↓
操作系统产生鼠标相关事件
↓
Qt 事件循环获取事件
↓
找到对应的 QWidget
↓
调用相应的事件处理函数
↓
继续等待下一个事件
用户点击按钮:
用户点击按钮
↓
操作系统产生鼠标事件
↓
Qt 事件循环
↓
QPushButton
↓
处理鼠标事件
↓
可能发出 clicked() 信号
↓
连接的槽函数执行
所以 exec() 运行期间,主线程实际上一直在工作。
"exec() 阻塞了主线程",为什么窗口还能够响应?
这是初学者非常容易产生误解的地方。
我们经常说:
app.exec()会阻塞当前线程。
这里的"阻塞"并不是:
主线程什么都不做。
而是:
main()后面的代码不会继续执行,直到事件循环退出。
例如:
c++
qDebug() << "A";
int result = app.exec();
qDebug() << "B";
程序运行后:
输出 A
↓
进入事件循环
↓
程序持续运行
↓
用户操作窗口
↓
处理各种事件
↓
......
↓
事件循环退出
↓
exec() 返回
↓
输出 B
所以:
main() 的执行流程被暂停 ≠ 主线程停止工作
更准确地说:
main()
│
├── 前面的代码正常执行
│
├── app.exec()
│ │
│ └── 主线程进入事件循环
│ ├── 等待事件
│ ├── 处理事件
│ ├── 等待事件
│ └── 处理事件
│
└── exec() 返回后继续执行
那么什么时候 exec() 才会返回?
只有当应用程序收到退出请求时,事件循环才会结束。
例如:
QApplication::quit();
或者:
QCoreApplication::quit();
也可以通过:
QApplication::exit(0);
退出事件循环。
对于普通 Qt Widgets 程序,当最后一个顶层窗口关闭时,Qt 默认也会让应用程序退出。
因此:
用户关闭窗口
↓
关闭事件被处理
↓
应用程序请求退出
↓
事件循环结束
↓
exec() 返回
↓
main() 返回
↓
局部对象析构
↓
程序退出
用一个最直观的例子理解
错误示例:
c++
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow w;
w.show();
return 0;
}
可以理解成:
创建程序
↓
创建窗口
↓
显示窗口
↓
程序立即退出
正确示例:
c++
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow w;
w.show();
return app.exec();
}
则是:
创建程序
↓
创建窗口
↓
显示窗口
↓
进入事件循环
↓
等待用户操作
↓
处理事件
↓
继续等待
↓
......
↓
用户关闭程序
↓
退出事件循环
↓
main() 返回
↓
程序退出
记住 show() 和 exec() 的区别
这是理解 Qt 程序启动流程最重要的一组概念:
| 代码 | 主要作用 |
|---|---|
MainWindow w; |
创建窗口对象 |
w.show(); |
让窗口进入可见状态 |
app.exec(); |
进入 Qt 主事件循环 |
app.quit(); |
请求退出事件循环 |
exec() 返回 |
事件循环已经结束 |
因此,一个最基本的 Qt Widgets 程序才会写成:
c++
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow w;
w.show();
return app.exec();
}
这几行代码实际上分别解决了三个不同的问题:
QApplication
↓
建立 Qt GUI 应用环境
MainWindow + show()
↓
创建并显示窗口
app.exec()
↓
让程序进入事件循环,持续处理事件
这就是为什么 return 0 不能替代 return app.exec()。
完整声明周期流程图
让我们把所有的拼图组合起来,看看一个完整的 Hello World 程序,从启动到窗口关闭,经历了怎样的时序:

从这张图可以看出,QApplication、MainWindow 和事件循环三者之间是如何协同工作的。
八、总结:Hello World 教会我们的三件事
回顾这一篇的内容,一个看似简单的 Hello World 程序,实际上教会了我们三件非常重要的事情。
第一件事:Qt 应用是有"生命"的
QApplication 对象是程序的"心脏"。它在构造时启动整个框架,在析构时清理所有资源。
第二件事:GUI 程序的核心是事件循环
没有事件循环,窗口只是静态的图片;有了事件循环,窗口才有了"生命"------能够响应点击、输入、缩放和网络事件。
这也是为什么 a.exec() 是 Qt 程序与普通 C++ 控制台程序最大的不同之处。
第三件事:MOC 是 Qt 元对象系统的重要组成部分
MOC 为带有 Qt 元对象声明的类生成额外的 C++ 代码,从而支持信号与槽、运行时类型信息、属性系统等机制。
当你理解了这几行代码背后的机制,后续所有的 Qt 学习都会变得顺理成章。
- 学习信号与槽时,你会知道那是 MOC 生成的元对象在发挥作用;
- 学习多线程时,你会知道为什么不能在工作线程直接操作 UI(因为事件循环在主线程);
- 学习网络编程时,你会知道网络事件也是通过事件循环分发到
QTcpSocket的。
对于本篇的一些关键词非常容易混淆,下表可以先简单进行理解:
| 概念 | 负责什么 |
|---|---|
QApplication |
QWidget 应用程序环境 |
show() |
让窗口进入可见状态 |
exec() |
进入主事件循环 |
| Event | Qt 中传递/处理的事件 |
| Signal | Qt 对象之间的通知机制 |
| Slot | 接收信号并执行相应逻辑 |
Q_OBJECT |
为类启用 Qt 元对象相关能力 |
| MOC | 为 Q_OBJECT 等 Qt 元对象声明生成代码 |
QObject |
Qt 对象模型的基础类 |
这就是"从根源理解"和"只背 API"之间的区别。
从下一篇开始,我们将正式进入 Qt 的核心控件和基础组件学习。
但我们不会像普通教程那样,简单地罗列 "QPushButton 有哪些属性"。
我们会带着一个更深入的问题去学习:
这些控件,是如何与事件循环、对象树和元对象系统协同工作的?
下一篇预告:《Qt 项目构建流程深度解析------MOC、UIC、RCC 的作用与原理》
窗口才有了"生命"------能够响应点击、输入、缩放和网络事件。
这也是为什么 a.exec() 是 Qt 程序与普通 C++ 控制台程序最大的不同之处。
第三件事:MOC 是 Qt 元对象系统的重要组成部分
MOC 为带有 Qt 元对象声明的类生成额外的 C++ 代码,从而支持信号与槽、运行时类型信息、属性系统等机制。
当你理解了这几行代码背后的机制,后续所有的 Qt 学习都会变得顺理成章。
- 学习信号与槽时,你会知道那是 MOC 生成的元对象在发挥作用;
- 学习多线程时,你会知道为什么不能在工作线程直接操作 UI(因为事件循环在主线程);
- 学习网络编程时,你会知道网络事件也是通过事件循环分发到
QTcpSocket的。
对于本篇的一些关键词非常容易混淆,下表可以先简单进行理解:
| 概念 | 负责什么 |
|---|---|
QApplication |
QWidget 应用程序环境 |
show() |
让窗口进入可见状态 |
exec() |
进入主事件循环 |
| Event | Qt 中传递/处理的事件 |
| Signal | Qt 对象之间的通知机制 |
| Slot | 接收信号并执行相应逻辑 |
Q_OBJECT |
为类启用 Qt 元对象相关能力 |
| MOC | 为 Q_OBJECT 等 Qt 元对象声明生成代码 |
QObject |
Qt 对象模型的基础类 |
这就是"从根源理解"和"只背 API"之间的区别。
从下一篇开始,我们将正式进入 Qt 的核心控件和基础组件学习。
但我们不会像普通教程那样,简单地罗列 "QPushButton 有哪些属性"。
我们会带着一个更深入的问题去学习:
这些控件,是如何与事件循环、对象树和元对象系统协同工作的?
下一篇预告:《Qt 项目构建流程深度解析------MOC、UIC、RCC 的作用与原理》