(二)、《Qt事件循环:Qt为什么能“动”起来(1)》

  事件循环是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
    }
}

的确可以!不过看着有点杂交的感觉,但是窗口的确动起来了,当然还有很多细节之处,后续继续讲解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值