什么是自发事件?哪些事件类型可以被传递或压缩到一起?投递(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(); } } 首先,事件循环处理所有的投递事件(posted events),直到事件队列为空。然后,它处理自发事件(spontaneous events),直到处理完所有事件。最后,它处理所有在处理自发事件(spontaneous events)过程中所产生的投递事件(posted events)。 发射事件(sent events)不会被事件循环所处理。它们被直接传递给对象。 让我们看看这在绘制事件(paint events)的执行中是如何工作的。当一个窗口小部件(widget)第一次显示时,或者当它被隐藏后又变的可见时,窗口系统会产生一个(自发的)要求程序重绘该窗口小部件的绘制事件(paint event)。最后,事件循环获取该事件,并将它发送给需要重绘的窗口小部件。 不是所有的绘制事件(paint events)都是由窗口系统所产生。当你调用QWidget::update()来重绘一个窗口小部件时,该窗口小部件向自身投递一个绘制事件(paint event)。该绘制事件排队等待,并在最后由事件循环分派。 如果你没有耐心,并且不想等待事件循环去重绘一个窗口小部件,那么理论上,你可以直接调用paintEvent()来强制立即重绘。但是在实际中,这样做并不一定可行,因为paintEvent()是一个受保护的函数。这也将绕过任何现有的事件过滤器。基于这一原因,Qt提供了一个直接发射(sending)事件,而不是投递(posting)事件的机制。QWidget::repaint()使用这一机制来强制进行立即的重绘。 投递事件(posting events)相比于发射(sending)事件的一个优势在于投递(posting)的方式使Qt有机会对事件进行压缩。如果你在同一个窗口小部件(widget)上连续地调用10次update(),并且没有返回事件循环(event loop),那么由update()所产生的10次事件将自动被合并为一个单一的事件,这一过程伴随着在它们所有的QPaintEvents对象中指定的区域的合并。 最后,要注意的是你可以在任何时候调用QApplication::sendPostedEvents()以强制Qt处理一个对象的投递事件(posted events)。 手工事件(即由应用程序主动激发,而不是由外界激发的事件) Qt应用程序可以产生它们自己的预定义或自定义类型的事件。这是通过创建一个QEvent(或其子类)的对象实例,并调用QApplication::postEvent() 或 QApplication::sendEvent()来实现的。 这两个函数都有一个QObject *和一个QEvent *类型的参数。如果你调用postEvent(),那么你必须使用new关键字来创建一个事件对象(event object),并且当它被处理完以后,Qt将自动删除它。如果你调用sendEvent(),那么你必须在栈(stack)上创建事件对象。这里有一个投递(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(),因为大多数事件在需要时会由Qt或窗口系统自动产生。在大多数你想发送一个事件的情形中,Qt包含了一个高级的函数来代替它(例如update() 和repaint())。 自定义事件类型 Qt使你可以创建自己的事件类型。这一技术在多线程应用程序中特别有用,可以作为一种与GUI线程通信的手段;请参阅C++ GUI Programming with Qt 3 (p. 359)第17章的一个例子。 自定义类型在单线程应用程序中也是很有用的,可以作为一种对象间的通信机制。你愿意使用事件而不是标准函数调用或信号与槽的主要原因是事件既可以同步使用,也可以异步使用(取决于你是否调用sendEvent()或postEvents()),然而调用一个函数或一个槽就总是同步的。事件的另一个优势是他们可以被过滤。关于这一点更详细的论述在下面的一节。 这里有一个展示如何投递(post)一个自定义事件的代码片段: const QEvent::Type MyEvent = (QEvent::Type)1234; ... QApplication::postEvent(obj, new QCustomEvent(MyEvent)); 事件必须是QCustomEvent(或一个子类)的类型。构造函数的参数是事件的类型。1024以下的值被Qt所保留用于预定义的事件类型;其它可以被应用程序所使用。 (译者注:在Qt4中,自定义事件是通过子类化QEvent来创建的。事件特定的数据被按照适合你的应用程序的方式存储。自定义事件仍然被发送给每个对象的customEvent()处理器函数,但是它们被作为QEvent对象而不是已经过时的QCustomEvent对象处理) 为了处理自定义事件类型,需要重载customEvent()函数: void MyLineEdit::customEvent(QCustomEvent *event) { if (event->type() == MyEvent) { myEvent(); } else { QLineEdit::customEvent(event); } } QCustomEvent拥有一个void *类型的成员,你可以将其用于自己的目的。如果你想要更多的类型安全,那么你也可以子类化QCustomEvent,并添加其它成员。但是然后你也需要在customEvent()中将QCustomEvent对象指向你特定的类型。 事件处理和过滤 Qt中的事件可以进行5个不同层次的处理: • 重新实现一个指定的事件处理器。 QObject 和 QWidget为不同的事件类型提供了许多特殊的事件处理器(例如,用于处理绘制事件的paintEvent())。 • 重载QObject::event()。 event()函数是一个对象的所有事件的入口点。QObject 和 QWidget中的该函数的默认实现只是简单地将事件转发给指定的事件处理器。 • 在一个QObject对象中安装一个事件过滤器。事件过滤器是一种对象,它能在另一个对象的事件到达指定目标之前,接收这些事件。 • 在qApp中安装一个事件过滤器。特别地,一个qApp上的事件过滤器监控所有的发送给应用程序中全部对象的事件。 • 重载QApplication::notify()。Qt事件循环和sendEvent()调用该函数以分发事件。通过重载它,你可以在其它任何人之前查看事件。 一些事件类型可以被蔓延传播。这意味着如果一个目标对象不处理某个事件,那么Qt设法为事件找到另外一个接收者,并用新的目标对象调用QApplication::notify()。 例如,按键事件被传播;如果拥有焦点的窗口小部件(widget)不处理某一个按键事件,那么Qt会向其父窗口小部件发送一个相同的事件,然后再发送给父窗口小部件的父窗口等等,直到事件到达最顶层的窗口小部件。 接受或忽略? 能被传播的事件都拥有一个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(),并且事件被接受(默认)。该事件不会被继续传播给父窗口小部件。如果用户按下了其他任何一个按键,那么我们调用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()函数会返回一个布尔值来告诉调用者某个事件是否被接受或忽略(true表示“接受”)。在event()中调用一个事件上的accept()或ignore()是没有意义的。“accept”标记是特定的事件处理器和event()之间的一种通信机制,而event()返回的布尔值被用于同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(); } }