event system

事件的概念

简单来说, 就是应用程序感兴趣的应用内部或者外部的活动结果。 在Qt中, 使用QEvent 抽象这些活动。

事件驱动模型

事件驱动模型现在在计算机很多领域都有使用。 例如 BSD socket 中的 select 模型和信号驱动的 I/O 模型、 GUI 程序中的事件处理、操作系统中断等。以一个GUI程序中发生的鼠标事件为例, 事件驱动模型大体思路如下:

  • 1. 有一个事件(消息)队列;
  • 2. 鼠标按下时,往这个队列中增加一个点击事件(消息);
  • 3. 有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;
  • 4. 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;

如图:

UML

 

Message是一个个的消息,每个消息代表一个事件,每个Message都有独立的处理者target;

多个消息利用Message里的next属性首尾相接组成一个MessageQueue队列;

MessageQueue提供了插入Message接口enqueueMessage和读取Message接口next;

Looper是一个循环类,它利用MessageQueue的next接口,不断从MessageQueue获得消息;

如果Looper获得了一个消息Message,那么就调用Message的target来处理该消息,这个target其实就是一个Handler;

Handler的HandlerMessage函数就是处理Message的地方,这是一个接口函数,所以使用者根据业务需求重写这个接口;

Handler除了处理Message外,还提供了插入Message的接口sendMessage,从这也可以看出,利用这四个类设计最简单的事件驱动模型只需操作Hanlder一个类即可,因为它既提供了插入消息的接口,也提供了处理消息的接口。

Qt  Event

当一个事件发生时, Qt 会创建一个QEvent 的子类, 并且把它发送给一个 QObject (包括子类型)实例。 QEvent 的重要信息包括:

  • 一个事件类型, 用于区分不同的事件
  • 额外的参数, 如 resize event 需要的old rect 和 rect。

Qt 简单的把事件分为 自发事件和非自发事件:自发事件是来自窗口系统的事件, 由应用程序自己通过调用 sendMessage 或者 postMessage 发出的事件称为非自发事件。这个可以通过 QEvent 的 spontaneous 函数确定。

能被传播的事件都拥有一个accept()和一个ignore()函数,你可以调用它们来告诉Qt系统你“接受”或“忽略”该事件。如果一个事件处理器在某个事件上调用accept(),那么该事件就不会被继续传播;如果事件处理器是调用ignore(),那么Qt会设法寻找另一个接收者。在 event 函数或者 event filter中, 通过返回true 或者 false来表示是否继续分发该事件。

事件接收者收到 QEvent 以后, 通过event 进行处理。 Event 简单的判断 QEvent 的类型, 并把它转发给相应的 event handler,亦即调用相应的事件函数, 例如 paintEvent, keyPressEvent 等等, 这些函数一般都是protect virtual类型的。 因此, 如果你不需要改变 QObject 对 事件的反应动作, 而是只需要自定义某一个事件的新动作, 可以仅仅复写该事件的 event handler。

Event filter 是Qt 事件系统中比较有特色的一项。 通过 QObject::installEventFilter 可以为某一个 Object 创建一个filter。 这个filter 会先于该 object的 event 处理 事件, 一般使用 filter 拦截事件并把它转发给子控件。 同 event 函数一样, filter 返回 true 表示事件已经被处理, 返回false 将转发给该object 的event handler。

在 Qt 中, 有五种处理事件的方法:

  • 重写 event handle , 这是最简单的方法;
  •  QCoreApplication::notify(QObject * receiver, QEvent * event)  简单的调用 receiver->event(event)
  • 为 QCoreApplication 的实例注册 event filter;  它可以有多个 filter, 而且能够捕捉到 disabled widget 的鼠标事件, 但它只能注册位于主线程内的object。
  • 重写 QObject::event 函数
  • 在某一个 object 上注册 event filter, 这种方法多用于管理子控件, 比如, 管理 tab 键顺序等。 

事件驱动模型下的 Qt Event

Qt 的事件系统符合 事件驱动模型, 但是也有一些不同之处。

Qt事件可以基于产生和发送的方式来分为三种类型:

  • 自发事件(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 的事件传递过程已经很明朗了。

  • QCoreApplication(代表应用程序) 不断调用 exec检查事件队列。
  • QCoreApplication
  1. 首先检查 主线程的投递事件队列, 如果不为空, 则按照FIFO 的规则 遍历每一个事件, 获取事件的目标object, 并调用 notify 函数;
  2. 接着按照同样的规则处理自发事件队列;
  3. 最后按照同样的规则再次检查投递事件队列, 这是因为在处理自发事件的过程中, 有可能产生新的投递事件。
  • notify 函数会  
  1. 首先检查系统的filter, 如果没有被拦截, 则
  2. 检查目标 object 的 filter, 如果没有被拦截, 则
  3. 调用该 object 的event 函数, 把它分发给 event handler(如果有的话), 这里使用了多态, 也就是说, 继承自 QObject 的子类如果实现了自己的handler 函数, 则event 函数会使用该子类的 handler。 完毕。
  4. 如果 第一步被拦截,并重定向 object2, 则进入object2,开始第二步; 如果没有被重定向, 则处理完毕;
  5. 如果第二步被拦截, 并重定向 object2, 则进入 object2, 开始第二步; 如果没有被重定向, 则 处理完毕

 

自定义事件类型

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));

如前一节所讲, 一个 CustomEvent 需要一个 event type , 以及相应的一些参数。 并且需要使用 postEvent 或者 sendEvent 把它发送给接收者。 同时, 接收者也要能够识别这个事件并处理。  

 

多线程中的事件

线程中的事件循环

异步的postEvent 可以在任何时候向任何Object 发送事件, 而sendEvent 必须向同一个线程里的object发送事件。

Event filters 所控制的objects 与 监听者必须在同一个线程里。

线程内的object 在某些方面来说不是线程安全的, 比如说, 线程中的事件处理系统。 当线程切换时, 事件处理系统很可能发生混乱。

但是, emit 过程是线程安全的。 可以通过connect 的附加参数决定 signal /slot的连接类型。 有如下几种类型:

  • Auto Connection : 如果信号发送者和接收者位于同一个线程, 则它的行为同 Direct Connection, 否则同 Queued Connection
  • Direct Connection : 当信号发出时, 槽函数被复制并且在发送者所在线程里面执行, 而非接收者线程。
  • Queued Connection: 当控制权回到接收者线程的事件循环中时, slot函数被复制, 并在接收者线程中执行。
  • Blocking Queued Connection: 当前线程会阻塞直到 slot 返回。 其它同Queued Connection。 使用这个选项可能会发生死锁。
  • Unique Connection :   这个选项可以与以上4个进行或操作, 它表示, 如果已经存在一个指定的connection(sender receiver  signal slot相同), 则再次调用 connect 生成相同连接时, 会失败。

超时事件

超时事件的优先级是最低的。 它会等待其它事件处理完毕才会发生。 因此, 程序的粒度决定了Timer 的精度。 QBasicTimer是Timer的一个简化版, 它不是 object 的子类。

Delete

创建QObject 的线程对它有拥有权,对于QThread 也是如此(并非想象中的属于调用run的线程),  如果在其它线程中delete QObject, 必须保证这个QObject 没有事件发生(这个很难保证)。  因此, 如果必须这么做, 可以使用deleteLater, 这会导致一个 DeferredDelete  被post, 并且在 该Object执行完所有事件后调用。

QObject 与线程

QObject 以及它的许多非GUI 子类都是线程安全的, 但是在一个线程中创建、在另一个线程中使用QObject(及子类)则是不安全的。

QObject 的子类的实例必须与其父对象在同一个线程中创建, 因此 QObject 不能以 QThread 为父类, 因为 QThread 毫无疑问是其它线程创建的。

事件驱动对象仅在一个线程中有效:具体来说, 是时间事件和网络事件。

删除QThread 之前, 必须删除它拥有的所有 QObject。 这个一般是比较简单的, 仅仅需要你在栈里创建 QObject即可。

一个QObject 实例生存于创建它的线程之中,  与它有关的事件由这个拥有它的线程分派管理,使用 QObject::thread()  可以获得这个线程。使用 QObject::moveToThread() 可以改变它所在的线程, 但是这个函数不是线程安全的,并且只能在当前线程调用, 把该对象“push”到其它线程。

QApplication 以后的代码中创建的QObject, 它所在的thread 是 qApp。 在 Application 之前创建的, QObject::thread() 返回 0。

 

 

Windows 事件简介

Windows中大概有400多种消息,这些消息可以进一步细分为3类:

窗口消息(Windows Message)

与窗口的内部运作有关,如创建窗口,绘制窗口,销毁窗口等。可以是一般的窗口,也可以是Dialog,控件等。

如:WM_CREATE, WM_PAINT, WM_MOUSEMOVE, WM_CTLCOLOR, WM_HSCROLL...

命令消息(Command Message)

与处理用户请求有关, 如单击菜单项或工具栏或控件时, 就会产生命令消息。

WM_COMMAND, LOWORD(wParam)表示菜单项,工具栏按钮或控件的ID。如果是控件, HIWORD(wParam)表示控件消息类型

控件通知(Notify Message)

控件通知消息, 这是最灵活的消息格式, 其Message, wParam, lParam分别为:WM_NOTIFY, 控件ID,指向NMHDR的指针。NMHDR包含控件通知的内容, 可以任意扩展。

 

Comment:

  参考 http://www.qtcn.org/bbs/simple/?t31383.html  Another Look at Events(再谈Events)

参考 http://blog.csdn.net/gykimo/article/details/9182287  经典软件设计模型 - 事件驱动模型

posted @ 2015-08-24 23:19  aslistener  阅读(251)  评论(0编辑  收藏  举报