(译)再看Qt事件(Another Look at Events)
再进一步学习 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)。
QObject
和QWidget
为不同类型的事件提供了许多特定的事件处理程序(例如,paintEvent()
用于绘制事件)。 -
重新实现
QObject::event()
。event()
函数是对象所有事件的入口点。QObject
和QWidget
中的默认实现只是将事件转发到特定的事件处理程序。
-
在
QObject
上安装事件过滤器。事件过滤器是一个对象,它在事件到达预定目标之前接收另一个对象的事件。 -
在
qApp
上安装事件过滤器。特别地,qApp上的事件过滤器监视发送到应用程序中所有对象的所有事件。 -
重新实现
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();
}
}