事件循环是qt最为核心也是最重要的概念,本篇将从头开始讲解事件循环的本质,废话不多说,我们开始吧。
0、导引
先给个简单代码,
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget w;
w.show();
while (1) {}
}
程序运行了吗?运行了!
有窗口吗?有!
窗口能动吗?不能!
那么,问题来了,为什么不能?gpt会说没有事件循环,很好,那接下来就讲事件循环是怎么让窗口动起来的。
1、事件是什么
众所周知,Windows 是一个事件驱动的操作系统。所谓事件驱动,就是操作系统给程序发个“消息”,就像发短信一样,比如鼠标移动了,键盘按下了,计时结束了等等,操作系统把这个消息发给应用程序,应用自己来处理。所以,要先讲解消息是什么。
如下是一个典型的windows桌面程序,不用全部都看,注意最后有个while循环,一直GetMessage然后DispatchMessage,直到程序退出。
- GetMessage:windows系统会有个消息队列,GetMessage就是从消息队列取出来一个消息。
- DispatchMessage:取出来消息然后呢?然后当然是分发给窗口了,DispatchMessage就干这个事情。
#include <windows.h>
// 窗口过程函数:负责处理窗口的消息
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_DESTROY:
PostQuitMessage(0); // 通知主循环退出
return 0;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow) {
// 定义窗口类
WNDCLASS wc = { 0 };
wc.lpfnWndProc = WndProc; // 消息处理函数
wc.hInstance = hInstance;
wc.lpszClassName = TEXT("MyWindowClass");
RegisterClass(&wc); // 注册窗口类
// 创建窗口
HWND hwnd = CreateWindow(
TEXT("MyWindowClass"), TEXT("Hello Windows"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 400, 300,
NULL, NULL, hInstance, NULL);
ShowWindow(hwnd, nCmdShow); // 显示窗口
// 消息循环
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg); // 翻译键盘消息
DispatchMessage(&msg); // 分发消息到 WndProc
}
return 0;
}
所以,消息是什么?
消息就是上面的MSG结构,windows下定义如下,说白了,就是一个消息编号(MSG.message字段)告诉是啥消息,然后加上一些额外的参数让消息携带一些信息。so easy!
typedef struct tagMSG {
HWND hwnd; //窗口句柄,标识消息要给哪个窗口处理
UINT message; //关键,标记到底啥消息,是鼠标消息还是键盘消息呀
WPARAM wParam; //消息附带的参数
LPARAM lParam; //消息附带的参数
DWORD time; //消息产生时间
POINT pt; //鼠标消息,要记录鼠标的位置坐标
} MSG;
很好,现在我们知道了 消息 = 编号+携带的数据。程序会一直while循环从消息队列取出数据然后进行处理。
那么消息等同于事件吗?对于windows可以这么说,既然如此,qt直接用这个MSG就可以了吗?当然也不太行,因为QT是跨平台的,是面向对象的,MSG太低级了,不是很好用。所以在QT就需要封装一个"事件"来传递"消息"。
这个事件就是QEvent,代码如下,对象在x86下也仅是12字节而已,和MSG原理一样,有个t字段来标识事件的类型。
class Q_CORE_EXPORT QEvent // event base class
{
public:
enum Type {
None = 0, // invalid event
Timer = 1, // timer event
User = 1000, // first user event id
MaxUser = 65535 // last user event id
};
explicit QEvent(Type type);
virtual ~QEvent();
protected:
QEventPrivate *d;// 保留字段,当前还没有实现private
ushort t; //事件类型
private:
ushort posted : 1;
ushort spont : 1; //为true则是自发事件,是用户或系统产生的外部事件。否则是qt内部调用。
ushort m_accept : 1;
ushort reserved : 13;
};
但是,竟然发现没有携带的数据?那当然不是,由于C++面向对象的,各个不同的事件继承一下QEvent,然后自己附加数据就行了。
比如QTimerEvent事件,继承QEvent后自己加个id成员变量自己用就行了。QChildEvent事件也是加了个成员变量对象指针。
至此,我们知道了事件是什么,事件非常简单,就是一小块数据,里面有个编号,有一些该事件额外附带的数据信息。仅此而已,so so easy!
2、事件循环
开始进入主题,上节说过,事件循环就是QEventLoop。
接口也非常简单。
- exec()开始进入事件循环,
- exit()结束事件循环
-
processEvents()处理事件
exec()开启进入事件循环,代码如下,所谓循环,实际是个while循环,一直在while中调用processEvents。
虽然名字叫做processEvents,处理事件,但是实际上这时候还没看到事件,processEvents中其实包含了获取事件,分发事件,处理事件。
这里给出一个实际代码,我在一个窗口event函数下个断点,看调用栈如下。看程序流程在main函数中执行QApplication::exec()实际会执行到QEventLoop::exec(),之前说过,这是开启事件循环,开启之后将一直循环processEvents。
从上面栈帧看到,QEventLoop::processEvents实际还会调用了QEventDispatcherWin32::processEvents,这里才会执行分发事件操作。看如下代码,实际上QEventLoop::processEvents不是自己直接操作事件,而是先调起来eventDispatcher,然后让eventDispatcher来处理事件。
这是因为,各个不同的平台对于事件是不一样的,比如上面说到windows的事件就是MSG消息,对事件处理流程是获取消息,分发消息。对于别的平台有别的处理消息的方式。因此QEventLoop::processEvents只是个中间层,底层在不同的平台上使用不同的事件处理方式。
以windows为例,QEventDispatcherWin32::processEvents负责处理windows下事件。这里给出精简一下代码,发现是不是和之前的windows原生程序非常类似?
可以暂时把PeekMessage理解成GetMessage,那和前文给出的windows原生代码相比,只能说基本一摸一样。
bool QEventDispatcherWin32::processEvents(QEventLoop::ProcessEventsFlags flags)
{
while (!d->interrupt)
{
MSG msg;
bool haveMessage;
haveMessage = PeekMessage(&msg, 0, 0, 0, PM_REMOVE);
d->queuedSocketEvents.append(msg);
if (haveMessage)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
3、分发事件
紧接上文,所谓消息循环就是一直从系统的消息队列里面得到消息,然后DispatchMessage分发消息。那到底分发给谁呢,怎么分发的呢?
还是看调用链,DispatchMessage最后调用到QtCore模块的qt_internal_proc,这又是什么呢
qt_internal_proc是一个窗口回调函数,windows下在创建窗口时候要执行了个窗口回调函数,看之前的代码,也就是wc.lpfnWndProc消息处理函数。顾名思义,分发的消息应该由这个消息处理函数,也是回调函数来处理,QT内部提前注册了窗口回调函数是qt_internal_proc,所以分发消息后就会调用到这里。
// 定义窗口类
WNDCLASS wc = { 0 };
wc.lpfnWndProc = WndProc; // 消息处理函数
wc.hInstance = hInstance;
wc.lpszClassName = TEXT("MyWindowClass");
RegisterClass(&wc); // 注册窗口类
// 创建窗口
HWND hwnd = CreateWindow(
TEXT("MyWindowClass"), TEXT("Hello Windows"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 400, 300,
NULL, NULL, hInstance, NULL);
目前为止,这里还是windows的MSG消息,那要封装成QT的QEvent事件,会有一些函数来将其封装成QEvent,然后封装后,调用 QCoreApplication::sendEvent,这里通过一些步骤最终调用到自己重写的QWidget::event函数,所以,事件经过层层传递,最终传递到了窗口的event函数里面了。
4、总结
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget w;
w.show();
while (1) {}
}
开头的这个代码创建的窗口为什么不动,因为当拖动窗口时,windows下会产生消息,消息转成qt的事件,层层传递,最后传递到QWidget的event函数里,然后根据事件类型做出反应,现在while(1)一直死循环了,尽管在系统消息队列产生了消息,但是没有去获取,没有处理,那窗口就一直卡死了。
那下面增加获取消息,分发消息的代码,这样能动起来了吧?
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QtWidgetsApplication9 w;
w.show();
// 消息循环
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg); // 翻译键盘消息
DispatchMessage(&msg); // 分发消息到 WndProc
}
}
的确可以!不过看着有点杂交的感觉,但是窗口的确动起来了,当然还有很多细节之处,后续继续讲解。