再进一步学习 QT 事件处理系统的过程中看到其推荐了一篇挺不错的文章,本文为在自己理解的基础上进行翻译,原文:Another Look at Events - Jasmin Blanchette

什么是自发事件(spontaneous event) ?哪些事件类型可以被传播(propagated)或压缩(compressed)?投递(posting)和发送(sending)事件有什么区别?我什么时候应该在事件上调用 accept()ignore() ?如果你不知道所有这些答案,请继续阅读!

事件的起源

根据事件的创建方式和调度方式,事件可以分为三类:

  • 自发事件(Spontaneous events)由窗口系统生成。它们被放入系统队列中,并由事件循环一个接一个地处理。
  • 投递事件(Posted events)由 Qt 或应用程序生成。它们由 Qt 排队并由事件循环处理(异步)。
  • 发送事件(Sent events)由 Qt 或应用程序生成,但它们直接发送到目标对象(同步)。

当我们在main()函数末尾调用 QApplication::exec()时,该应用程序就进入了 Qt 的事件循环。事件循环在概念上与如下代码类似:

    while (!exit_was_called) {
        while (!posted_event_queue_is_empty) {
            process_next_posted_event();
        }
        while (!spontaneous_event_queue_is_empty) {
            process_next_spontaneous_event();
        }
        while (!posted_event_queue_is_empty) {
            process_next_posted_event();
        }
    }

首先,事件循环处理所有投递事件,直到队列为空。然后,它处理自发事件,直到没有剩余事件为止。最后,它处理在处理自发事件期间生成的任何投递事件。

发送事件不会通过事件循环处理,他们被直接发送到对象。

让我们看看这在实践中如何与绘制事件(paint events)一起工作。当一个小部件(Widget)首次变得可见,或在被遮挡后变得可见时,窗口系统生成一个(自发的)绘制事件,要求程序重绘小部件。事件循环最终会拾取该事件并将其分派到需要重绘的小部件。

并非所有绘制事件都是由窗口系统生成的。当调用 QWidget::update() 来重绘一个小部件时,该小部件向自己发布一个绘制事件。该绘制事件被排队,并最终由事件循环分派。

如果你不耐烦,无法等待事件循环重绘小部件,理论上可以直接调用 paintEvent() 来强制立即重绘。但在实践中,这并不总是可能的,因为 paintEvent() 是一个受保护的函数。这也会绕过任何现有的事件过滤器。因此,Qt提供了一种直接发送事件(sending events)而不是投递(posting)它们的机制。QWidget::repaint() 使用这种机制来强制立即重绘。

相对于发送事件,投递事件的一个优势是发布给了Qt压缩它们的机会。如果你在同一个小部件上连续调用 update() 十次而不返回事件循环,由 update() 生成的十个事件将自动合并为一个单一事件,其区域是所有 QPaintEvent 中指定区域的并集。可压缩的事件类型包括绘制事件、移动事件、调整大小事件、布局提示事件和语言更改事件。

最后,请注意,可以随时调用 QApplication::sendPostedEvents() 来强制 Qt 在调用时处理对象的投递事件。

合成事件

Qt应用程序可以生成自己的事件,无论是预定义类型还是自定义类型。这是通过创建 QEvent(或其子类)的实例并调用 QApplication::postEvent()QApplication::sendEvent() 来完成的。

这两个函数都接受一个 QObject * 和一个 QEvent * 作为参数。

如果调用 postEvent(),必须使用 new 创建事件对象,Qt 会在处理后自动删除它。

如果调用 sendEvent(),必须在栈上创建事件。

以下是投递事件的示例:

QApplication::postEvent(mainWin, new QKeyEvent(QEvent::KeyPress, Key_X, 'X', 0));

以下是发送事件的示例:

QKeyEvent event(QEvent::KeyPress, Key_X, 'X', 0);
QApplication::sendEvent(mainWin, &event);

其实,Qt 应用程序很少需要直接调用 postEvent()sendEvent(),因为大多数事件都是由 Qt 或窗口系统在必要时自动生成的。在大多数想发送事件的情况下,Qt 都包含了一个专门做这件事的高级函数(例如,update()repaint() )。

自定义事件类型

Qt 允许创建自己的事件类型。这种技术在多线程应用程序中特别有用,作为与GUI线程通信的一种方式。

请参阅《C++ GUI Programming with Qt 3》第17章(第359页)的示例。

自定义类型在单线程应用程序中也可能有用,作为一种对象间通信机制。你会使用事件而不是标准函数调用或信号和槽的主要原因是:

事件可以同步和异步使用(取决于是使用 sendEvent() 还是 postEvents()),而函数调用或调用槽总是同步的。

事件的另一个优势是它们可以被过滤的。下一节会有更多关于这方面的内容。

以下是展示如何发布自定义事件的代码片段:

const QEvent::Type MyEvent = (QEvent::Type)1234;
...
QApplication::postEvent(obj, new QCustomEvent(MyEvent));

事件必须是 QCustomEvent 类型(或其子类)。构造函数的参数是事件类型。1024以下的值由Qt保留用于预定义的事件类型;其他值可以由应用程序使用。

要处理自定义事件类型,重新实现 customEvent() 函数:

void MyLineEdit::customEvent(QCustomEvent *event)
{
    if (event->type() == MyEvent) {
        myEvent();
    } else {
        QLineEdit::customEvent(event);
    }
}

QCustomEvent 类有一个 void * 成员,你可以用于自己的目的。你也可以子类化 QCustomEvent 并添加其他成员,如果你想要更多的类型安全性——但是这样你也需要在 customEvent() 中将QCustomEvent 转换为你的特定类型。

注意:在Qt4/Qt5中,自定义事件是通过子类化QEvent来创建的。

事件处理和过滤

Qt中的事件可以在五个不同的级别上处理。

  • 重新实现特定的事件处理程序(event handler)QObjectQWidget 为不同类型的事件提供了许多特定的事件处理程序(例如,paintEvent() 用于绘制事件)。

  • 重新实现QObject::event()event() 函数是对象所有事件的入口点。QObjectQWidget 中的默认实现只是将事件转发到特定的事件处理程序。

  1. QObject 上安装事件过滤器。事件过滤器是一个对象,它在事件到达预定目标之前接收另一个对象的事件。

  2. qApp 上安装事件过滤器。特别地,qApp上的事件过滤器监视发送到应用程序中所有对象的所有事件。

  3. 重新实现 QApplication::notify() 。Qt 的事件循环和 sendEvent() 调用这个函数来分发事件。通过重新实现它,你可以在其他人之前看到事件。

一些事件类型可以被传播。这意味着如果目标不处理事件,Qt 会尝试为事件找到另一个接收者,并使用新的目标调用 QApplication::notify()

例如,键盘事件会被传播。如果具有焦点的小部件不处理某个键,Qt 会将相同的事件分派给父小部件,然后是父小部件的父小部件,依此类推,直到达到顶级小部件。

接受还是忽略事件?

可以传播的事件有 accept()ignore() 函数,你可以调用这些函数来告诉 Qt 你 “接受” 或 “忽略” 事件。如果事件处理程序在事件上调用 accept(),该事件就不会被进一步传播;如果事件处理程序调用 ignore(),Qt 会尝试找到另一个接收者。

如果你像大多数 Qt 开发者一样,你可能从未真正在程序中调用过 accept()ignore()。这是对的。Qt 的设计方式使你通常不需要调用它们。默认值是 “接受”,QWidget 中的默认事件处理程序实现调用 ignore()。如果你想接受事件,你只需要重新实现事件处理程序并避免调用 QWidget 实现。如果你想忽略事件,只需将其传递给 QWidget 实现。以下代码片段说明了这一点:

void MyFancyWidget::keyPressEvent(QKeyEvent *event)
{
    if (event->key() == Key_Escape) {
        doEscape();
    } else {
        QWidget::keyPressEvent(event);
    }
}

在这个例子中,如果用户按下Esc,调用 doEscape(),事件被 ACCEPT(默认),事件不会传播到父小部件。如果用户按下任何其他键,我们调用 QWidget的默认实现:

void QWidget::keyPressEvent(QKeyEvent *event)
{
    event->ignore();
}

由于 ignore() 调用,事件将被传播到父小部件。

到目前为止,我们假设基类是 QWidget 。然而,同样的习惯用法在任何级别都有效,只需将 QWidget 替换为基类。例如:

void MyFancyLineEdit::keyPressEvent(QKeyEvent *event)
{
    if (event->key() == Key_SysReq) {
        doSystemRequest();
    } else {
        QLineEdit::keyPressEvent(event);
    }
}

如果由于某种原因你在 event() 中处理事件,而不是在特定的处理程序(如 keyPressEvent() )中处理,则程序略有不同。event() 函数返回一个 bool 值,告诉调用者事件是否被接受(true表示"接受")。

event() 调用 accept()ignore() 是没有意义的。"接受"标志是特定事件处理程序和 event() 之间的通信机制,而 event()bool 返回值用于与 QApplication::notify() 通信。QWidget 中的默认 event() 实现将"接受"标志转换为 bool ,如下所示:

bool QWidget::event(QEvent *event)
{
    switch (e->type()) {
    case QEvent::KeyPress:
        keyPressEvent((QKeyEvent *)event);
        if (!((QKeyEvent *)event)->isAccepted())
            return false;
        break;
    case QEvent::KeyRelease:
        keyReleaseEvent((QKeyEvent *)event);
        if (!((QKeyEvent *)event)->isAccepted())
            return false;
        break;
        ...
    }
    return true;
}

到目前为止所说的不仅适用于键盘事件,也适用于鼠标、滚轮、平板电脑和上下文菜单事件。

关闭事件的工作方式不同。调用 QCloseEvent::ignore() 取消关闭操作,而 accept() 告诉 Qt 继续正常关闭操作。为避免任何混淆,最好在 closeEvent() 重新实现中显式调用 accept()ignore() ,如下所示:

void MainWindow::closeEvent(QCloseEvent *event)
{
    if (userReallyWantsToQuit()) {
        event->accept();
    } else {
        event->ignore();
    }
}