Another Look at Events
by Jasmin Blanchette
什么是spontaneous event? 那种事件类型可以被传递和压缩?posting和sending一个事件有什么区别?什么时候应该在事件上调用accept()和ignore()?如果 你不知道这些答案,请阅读:
- 事件的起源
- 合成事件
- 自 定义事件类型
- 事件处理和过滤
-
接 受还是忽略?
事件的起源
根据事件如何创建和如何分派,事件可以分为三种类型:
原生事件(Spontaneous event),由Window system产生。它们被放到一个系统队列中,被事件循环依次处理。
posted event,由Qt或者应用程序 产生。它们被Qt排队,由事件循环处理。
sent event,由Qt或者应用程序产生,但它们被直接传送给目标对象。
当在man()函数 的最后调用 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();
}
}
首先,事件循环处理任何posted event直到队列为空。然后,它处理原生事件直到没有该类事件。最后它处理任何在处理原生事件的过程中产生的posted event。
Sent 事件不被事件循环处理。它们直接被传送给对象。
我们来看看在paint events的实践中的工作原理。当一个widget第一次被显示时,或者当它被隐藏后变得可见时,window system会产生一个spontaneous paint event去要求程序重绘该widget。事件循环最终会选出该事件并将其分派给需要重绘的widget。
不是所有的绘制事件都是由window system产生的。当你调用QWidget::update()去重绘widget时,the widget posts a paint event to itself。该paint event被排队并最终被事件循环分派。
如果你没有耐心,不愿等待事件循环来重绘一个widget,你可以在理论上直接调用paintEvent()去强制一个即时的重绘。但是在实践中这并不总 是可行的,因为paintEvent()是一个protected function。这种做法也将绕过任何现存的事件过滤器。基于这个原因,Qt提供了一个机制,用于直接sending events而不是posting them。QWidget::repaint()使用这个机制来强制一个即时的重绘。
相对于sending events,posting events的好处在于posting给Qt一个压缩它们的机会。如果你在同一个widget上连续调用update()10次并没有返回到事件循环,这 10次update()产生的事件将自动地被合并到一个单独的事件中,该事件合并了在所有的QPaintEvents中指定的区域。(the ten events generated by update() will automatically be merged into a single event with the union of the regions specified in all their QPaintEvents.)压缩的事件类型包括paint events,move events,resize events,layout events,layout hint events,and language change events.
最后,注意你可以在任何时候调用QApplication::sendPostedEvents()来强制Qt处理一个对象在调用期间的posted events。
手工事件(Synthetic Events)
Qt应用程序可以产生它们自己的事件,或者是预定义类型或者是自定义类型。这是通过创建一个QEvent(或其子类)的实例并调用 QApplication::postEvent()或者QApplication::sendEvent()来实现的。
这两个函数都以一个QObject*和一个QEvent*作为参数。如果你调用postEvent(),你必须使用new创建事件对象,该事件对象被处理 后Qt会自动删除它。如果你调用sendEvent(),你必须在栈中创建事件。这里是posting一个事件的例子 :
QApplication::postEvent(mainWin, new QKeyEvent(QEvent::KeyPress, Key_X, 'X', 0));
这里是sending一个事件的例子:
QKeyEvent event(QEvent::KeyPress, Key_X, 'X', 0);
QApplication::sendEvent(mainWin, &event);
Qt应用程序很少需要直接调用postEvent()或者sendEvent(),因为当需要的时候,大多数events会由Qt或者由window system自动地产生。在你想发送一个事件的大多数情形下,Qt包含了一个高层的函数来为你达到此目的(例如,update()和 repaint())。
自定义事件类型
Qt可以让你创建你自己的事件类型。作为GUI线程间通信的方式,这个技术在多线程程序中尤其有用。作为一个对象间通信机制,自定义类型在单线程程序中也 是有用的。使用事件而不使用标准的函数调用或者信号与槽的主要原因在于,事件既可以同步使用,又可以异步使用(依赖于你调用的是sendEvent()还 是postEvent()),然而调用一个函数或者调用一个槽则总是同步的。使用事件的另一个好处是它们可以被过滤。
下面的代码片段展示了如何post一个自定义事件:
const QEvent::Type MyEvent = (QEvent::Type)1234;
...
QApplication::postEvent(obj, new QCustomEvent(MyEvent));
该事件必须是类型QCustomEvent(或者其子类)。QCustomEvent构造函数的参数是该事件的事件类型。小于1024的值被Qt保留为预 定义事件类型了,其他的值可以被应用程序使用。(译者注:在Qt4 中,自定义事件是通过子类化QEvent来创建 的。事件特定的数据被按照适合你的应用程序的方式存储。自定义事件仍然被发送给每个对象的customEvent()处理器函数,但是它们被作为 QEvent对象而不是过时的QCustomEvent对象处理)
为了处理自定义事件类型,需要重新实现customEvent()函数:
void MyLineEdit::customEvent(QEvent *event)
{
if (event->type() == MyEvent) {
myEvent();
} else {
QLineEdit::customEvent(event);
}
}
QCustomEvent类有一个void*成员可以用来达到你的目的。如果你想更加类型安全的话,你也可以子类化QCustomEvent,并加入其他 成员——但是那样你还需要在customEvent()中,转换QCustomEvent为你所指定的类型。
事件处理和过滤
Qt中的事件可以在五种不同的层次进行处理。
使用或者重新实现特定的事件处理器
QObject和QWidget为不同类型的事件提供了许多特定的事件处理器(例如,paintEvent()用于paint event)。重新实现这些事件处理器可以完成许多用户 自定义的操作。
重新实现QObject::event()
event()函数是一个对象的所有事件的入口点,QObject和QWidget中的默认实现只是简单地传递事件到指定的事件处理器。因此重新实现该函 数可以进行自定义事件处理。这种方式还可以用于处理那些没有特定事件处理器的不常见类型的事件。在重新实现event()函数时,必须对没有明确处理的情 况调用其基类的event()函数。
在一个QObject上安装事件过滤器
一个事件过滤器对象会在另一个对象的事件到达预期的目标之前,接收这些事件。某个QObject一旦被目标对象的 installEventFilter()注册过,则发送给目标对象的所有事件都会首先发给该QObject的eventFilter()函数。若有多个 目标对象在该QObject上都安装了过滤器,则最后注册的过滤器最先被激活。
在QApplication对象上安装一个事件过滤器
一旦为app(QApplication对象)注册了一个事件过滤器,那么应用程序中每个对象的每个事件在被送往其他事件过滤器之前,会被送给app的 eventFilter()函数。这种方法对于调试非常有用。它也可以用来处理发送给disable widgets的鼠标事件,它们通常会被QApplication丢弃。
子类化QApplication并重新实现notify()
Qt的事件循环和sendEvent()函数都调用QApplication::notify()来分派一个事件。重新实现这个函数是在任何事件过滤器看 到所有事件之前获得它们的唯一方式。事件过滤器通常更有用,因为可以同时存在多个事件过滤器,而只有一个notify()函数。
某些事件类型可以被传递;如果具有焦点的widget没有处理某个键,Qt会分派同样的事件给其父类,然后是父类的父类,一直到顶级widget。
接受还是忽略?
可以传递的事件具有一个accept()和一个ignore()函数,你可以调用它们来告诉Qt你是接受或者忽略该事件。如果一个事件处理器在一个事件上 调用accept(),该事件就不会进一步传递;如果事件处理器调用ignore(),Qt将试着查找另一个接收者。
像大多数Qt开发者 一样,你可能从来不会为在你的程序中调用 accept()和ignore()所困扰。事实上的确如此。Qt也是按照你从来不需调用它们的方式设计 的。默认的值是accept,在QWidget中实 现的默认事件处理器会调用ignore()。如果你想接受该事件,你只需重新实现事件处理器并避免调用QWidget的实现。如果你想忽略一个事件,只需 简单地将其传递到QWidget的实现中。下面的代码片段描述了这一点:
void MyFancyWidget::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Key_Escape) {
doEscape();
} else {
QWidget::keyPressEvent(event);
}
}
在这个例子中,如果用户按下Esc,我们将调用doEscape(),事件将会被接受(默认值)。事件不会传递到其父widget。如果用户按了其他键, 我们将调用QWidget的默认实现:
void QWidget::keyPressEvent(QKeyEvent *event)
{
event->ignore();
}
由于调用了ignore(),事件将会传递到父类widget。
到目前为止,我们假定的基类是QWidget。然而,通过使用基类代替QWidget,在不同层次上同样的方言一样有效。例如:
void MyFancyLineEdit::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Key_SysReq) {
doSystemRequest();
} else {
QLineEdit::keyPressEvent(event);
}
}
如果因为某些原因,你是在event()里面处理事件而不在诸如KeyPressEvent()的指定的处理器中处理,这个过程则有些不同。事件函数返回 一个bool值告诉调用者事件是否被接受(true表示接受)。在event()中,在一个事件上调用accept()或者ignore()是无意义的。 在指定的事件处理器和event()之间,“accept”标志是一个通信机制,尽管event()的bool返回值被用来和 QApplication::notify()通信。QWidget的默认的event()实现转换“accept”标志为一个布尔值:
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();
}
}