Qt Widgets 速通:事件循环、信号槽、对象树与布局

背景:先从能写小 GUI 开始

学习 Qt 时不一定要一开始就系统啃完整文档。更实用的路线是:先写出一个可运行的小 GUI 程序,再逐步理解常见代码为什么这么写。

这篇文章采用 Qt Widgets + C++ 作为入口。原因很简单:Qt Widgets 更适合快速理解传统桌面程序结构,也能直接暴露 Qt 的几个核心概念:事件循环、信号槽、QObject 对象模型和布局管理器。

一个最小 Qt 程序由什么组成

最小 Qt Widgets 程序大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <QApplication>
#include <QWidget>

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

QWidget window;
window.resize(400, 300);
window.setWindowTitle("Hello Qt");
window.show();

return app.exec();
}

这段代码里有三个关键对象或动作。

QApplication 表示整个 GUI 应用程序对象。它负责初始化 Qt GUI 环境、处理命令行参数、管理事件循环,并把鼠标、键盘、窗口等事件分发给对应对象。

QWidget 是 Qt Widgets 体系中最基础的界面类。它既可以表示普通控件,也可以作为一个窗口。

show() 负责真正显示窗口。只创建对象不会自动显示界面。

GUI 程序的核心是事件循环

普通控制台程序通常是:

1
2
3
4
进入 main
执行逻辑
return
程序结束

GUI 程序不一样。窗口显示出来之后,程序不能立刻结束。它要持续等待用户和系统事件:

1
2
3
4
5
鼠标点击
键盘输入
窗口关闭
按钮按下
系统重绘

所以 Qt 程序最后通常写:

1
return app.exec();

app.exec() 会启动事件循环。程序会停在这里不断等待事件、分发事件、处理事件,直到应用退出。这样看,Qt 程序不是“一口气执行完”,而是“创建界面后进入等待和响应状态”。

信号与槽:把用户动作连接到代码

GUI 程序不能一直主动问用户“你点按钮了吗”。更自然的模式是:

1
2
3
先告诉 Qt:如果按钮被点击,就执行某段代码。
然后进入事件循环。
用户真的点击时,Qt 自动调用那段代码。

这就是信号与槽。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <QApplication>
#include <QPushButton>
#include <QDebug>

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

QPushButton button("Click me");
button.resize(200, 80);
button.show();

QObject::connect(&button, &QPushButton::clicked, []() {
qDebug() << "button clicked";
});

return app.exec();
}

这句最关键:

1
2
3
QObject::connect(&button, &QPushButton::clicked, []() {
qDebug() << "button clicked";
});

它的意思是:当 button 发出 clicked 信号时,执行后面的 lambda。

信号表示某个对象发生了一件事或状态变化,例如按钮点击、输入框文字变化、定时器超时。槽表示信号发生后要执行的函数或代码。现代 Qt 中,lambda 经常可以直接作为槽使用。

如果槽里要使用外部变量,需要捕获:

1
2
3
QObject::connect(&button, &QPushButton::clicked, [&button]() {
button.setText("Clicked");
});

这里 [&button] 表示按引用捕获外部变量 button。按引用捕获时要确认对象在槽执行时仍然存在。

成员函数作为槽:真实项目更常见

实际项目里,界面通常会封装成自己的窗口类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <QApplication>
#include <QPushButton>
#include <QWidget>
#include <QVBoxLayout>

class MainWindow : public QWidget {
private:
QPushButton* button;

public:
MainWindow() {
button = new QPushButton("Click me", this);

QVBoxLayout* layout = new QVBoxLayout(this);
layout->addWidget(button);

connect(button, &QPushButton::clicked,
this, &MainWindow::onButtonClicked);
}

void onButtonClicked() {
button->setText("Clicked");
}
};

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

MainWindow window;
window.show();

return app.exec();
}

这里的 connect 表示:按钮点击时,调用当前窗口对象 thisonButtonClicked() 成员函数。

QObject 父子对象:为什么 new 了不用 delete

Qt 代码里经常看到:

1
button = new QPushButton("Click me", this);

如果只从普通 C++ 的 new/delete 角度看,会自然追问:为什么没有 delete button

Qt 的答案是 QObject 父子对象机制。很多 Qt 类都继承自 QObject,可以形成父子对象树。当父对象销毁时,Qt 会自动销毁它的子对象。

例如:

1
2
3
MainWindow
├── QPushButton button
└── QVBoxLayout layout

MainWindow 销毁时,buttonlayout 会被一起释放。所以对于已经指定父对象的 Qt 对象,通常不要再手动 delete,否则可能破坏对象树管理,甚至带来重复释放风险。

需要注意的是,Qt 的父子机制只适用于 QObject 体系。普通 C++ 类不会因为写了类似关系就被 Qt 自动管理。

父对象也影响界面层级

QWidget 控件来说,父对象不仅影响生命周期,也常常影响界面归属。

1
2
QWidget window;
QPushButton* button = new QPushButton("Click", &window);

这里按钮属于窗口。如果按钮没有父窗口,它可能成为一个独立顶层窗口:

1
2
QPushButton* button = new QPushButton("Click");
button->show();

速通阶段可以先记住:在窗口类里写 new QPushButton("OK", this),通常表示“创建按钮,并交给当前窗口管理”。

布局管理器:不要手动摆坐标

Qt 中不推荐大量使用:

1
2
button->move(20, 20);
button->resize(100, 30);

手动坐标的问题很多:窗口缩放后不自然,不同系统字体大小可能挤压控件,翻译后文字变长也容易溢出。更常规的做法是把控件交给 layout。

常见布局有:

1
2
3
4
QVBoxLayout:垂直排列控件。
QHBoxLayout:水平排列控件。
QGridLayout:按行列排列控件。
QFormLayout:表单,左边标签右边输入框。

垂直布局例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QVBoxLayout>

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

QWidget window;

QPushButton* ok = new QPushButton("OK");
QPushButton* cancel = new QPushButton("Cancel");

QVBoxLayout* layout = new QVBoxLayout(&window);
layout->addWidget(ok);
layout->addWidget(cancel);

window.setWindowTitle("VBoxLayout");
window.show();

return app.exec();
}

复杂界面可以嵌套布局。例如外层垂直布局,上面放输入框,下面放一排按钮:

1
2
3
4
5
6
7
8
9
10
11
QLineEdit* edit = new QLineEdit;
QPushButton* ok = new QPushButton("OK");
QPushButton* cancel = new QPushButton("Cancel");

QHBoxLayout* buttonLayout = new QHBoxLayout;
buttonLayout->addWidget(ok);
buttonLayout->addWidget(cancel);

QVBoxLayout* mainLayout = new QVBoxLayout(&window);
mainLayout->addWidget(edit);
mainLayout->addLayout(buttonLayout);

布局的核心价值是把“控件如何摆放、如何随窗口变化调整”交给 Qt 处理,而不是自己手算坐标。

阶段性结论

Qt Widgets 入门可以先抓住五句话:

  1. Qt GUI 程序需要 QApplication 和事件循环。
  2. QWidget 是最基础的窗口/控件类,创建后要 show() 才显示。
  3. 信号表示对象发生事件,槽表示事件发生后要执行的代码。
  4. QObject 父子对象机制负责管理很多 Qt 对象的生命周期。
  5. 真实界面优先使用布局管理器,不要依赖手动坐标。

理解这五件事后,再继续学习菜单、对话框、文件读写、资源文件和多窗口结构,会顺很多。