Qt的事件循环与信号-槽

注:以下内容基于Qt5。

事件循环和信号-槽机制是Qt的重要特色。

1. 事件循环(Event Loop)

很多语言、平台、框架和库中都存在事件循环机制,一个典型的例子是Node.js。在涉及GUI编程的场合,通常也会使用事件循环。事件循环简化了编程模型,在只有一个用户线程的情况下,就能够实现非阻塞异步编程。

事件循环通常是通过一个队列(Event Queue)实现的。不太严谨地讲,在这个队列中保存着待执行的函数,运行时环境(可以看做是一个背后的线程)会不断从这个队列取出函数并执行,当队列变空时,会阻塞并等待新的函数放入。这些函数的执行,都发生在事件循环所在的线程。我们把从事件队列中取出一个函数并执行的过程称为一个循环,把当前正在执行的循环称为当前循环

因此,通过事件循环,在只使用一个线程(例如Qt的主线程/UI线程)的情况下就能够实现各种复杂的效果,由于只有一个线程,所以避免了多线程带来的复杂性。可以这样认为:你在Qt中写的所有代码,都是被放到事件队列,然后被事件循环取出执行的。

事件队列中的函数,包括事件处理函数和槽函数(某些情况下,后面具体讨论)。用户重写的事件处理函数(例如paintEvent(...))和通过connect(...)函数连接的槽函数,都通过事件循环执行。

事件处理函数

以鼠标点击事件为例:假设我们在某个组件上点击了鼠标,Qt会将从系统获取到的鼠标点击事件,封装成为一个QMouseEvent对象,并将其(包括产生此事件的组件对象)放入事件队列中。事件循环从事件队列中取出此事件对象后,将其发送给产生这个事件的组件对象,导致后者的mousePress(...)函数被调用。除了由系统产生事件外,用户还可以通过下面两个函数自主构造事件,并派发给对应的处理对象:

void QCoreApplication::postEvent(QObject *receiver, QEvent *event, ...);
bool QCoreApplication::sendEvent(QObject *receiver, QEvent *event)

区别:前者是通过事件循环机制间接派发的,首先将事件对象放入事件队列,再派发给receiver,因此是异步的;后者是直接将事件对象派发给receiver,因此是同步的。这一区别导致了它们的返回值类型不同;也导致了前者必须将QEvent对象放在堆上,而后者通常将QEvent对象放在栈上。共同点是它们都间接使用notify函数实现事件派发。

槽函数

TODO

启动事件循环

每个EventLoop都属于某个线程。主线程的事件循环通过app.exec()启动,其中的appQApplicationQCoreApplication;其他线程的事件循环通过QThread::exec()启动。

Qt的事件循环是可嵌套的

Qt的事件循环有一个很大的特色:它是可以嵌套的。启动一个子事件循环后,程序流程将会阻塞在当前位置,子EventLoop将会取代父EventLoop来执行事件队列中的函数(也就是说,子EventLoop和父EventLoop共享一个事件队列),直到子EventLoop退出,才继续执行(当前循环的)后面的代码。

前面说过,事件循环实现了非阻塞编程,为什么这里又出现了阻塞呢?实际上,这里的阻塞并非阻塞了线程,而只是改变了程序流程的执行路径。简单地说,是暂停了当前这一轮的函数的执行,转而从事件队列中取下一个函数来执行,直到子EventLoop退出后,才执行剩下的代码。利用这个特性,我们可以实现一个等待函数,即等待指定的时间。代码如下:

void wait(int msec)
{
    QEventLoop loop;
    QTimer::singleShot(msec, &loop, SLOT(quit()));
    loop.exec();
}

使用方法很简单,例如:

dosth1();
wait(5000);
dosth2();

这个wait(...)函数非常神奇!!!使用它的方式是同步的,但是它却实现了异步非阻塞的等待,这与QThread::sleep(...)函数是不同的,后者是阻塞的,会挂起当前线程。这在其他没有嵌套Event Loop的语言中是没法做到的。例如在JavaScript中,类似的等待功能,只能写成回调函数的形式,例如:

dosth1();
setTimeout(function() {
    dosth2();
}, 5000);

这在形式上是很丑陋的,而且如果需要等待多次的话,就会面临让人崩溃的回调地狱问题。当然,新版本的JavaScript有了async/await后,缓解了这个问题。

使用 Hugo 构建
主题 StackJimmy 设计