在处理QT循环事件的时候遇到了问题,查了半天资料都没弄明白问题出在哪,后来找大牛同事问了一下,同事就给我写了QCoreApplication::processEvent()这个函数,好啦,终于搞定了,这里小记一下,以免以后遇到。
于是乎这里认真仔细的看了一下Qt的事件和事件循环。(引用了碎炎的博客)
事件和事件循环
作为一个事件驱动的工具包,事件和事件传递扮演者Qt架构中的中心角色。在本文中我们不会给出一个对这个话题的全面的概述,我们将着眼于一些线程相关的概念。
事件能被程序的内部和外部产生,举个例子:
QKeyEvent和QMouseEvent对象代表了一个键盘和鼠标的事件,它们从窗口由用户的操作而产生。
QTimerEvent对象是当某个时间被激发时投入,它们都由操作系统产生。
QChildEvent对象是当一个子窗口被添加或者移除时候被送入QObject的,它们的源头是Qt程序自己。
事件的重点是它们被产生的时候不会被传递;它们会先进入事件队列,某刻会被传递。传送者自己循环访问事件队列并把事件传递给目标QObject对象,因此称作事件循环。概念上说,时间循环就像这个:
1. while (is_active)
2. {
3. while (!event_queue_is_empty)
4. dispatch_next_event();
5.
6. wait_for_more_events();
7. }
我们通过运行QCoreApplication::exec()来进入消息循环,这个循环直到exit()或者quit()被调用时才会被堵塞,然后退出。
这个”wait_for_more_events()”函数处于堵塞状态,直到有新的事件被产生了。假如我们考虑它,所有在此刻可能产生的事件是外部源头的。因此,这个消息循环能被以下情况唤醒:
窗口管理活动(鼠标按键操作等);
套接字事件;
定时器事件;
其它线程中投递的事件、
在Unix-like系统中,窗口管理器通过套接字来通知应用程序,即使客户端使用它们来与x server通讯。如果我们决定用内部的套接字对去实现跨线程的事件投递,所有剩下的唤醒条件如下:
套接字;
定时器;
这个就是select(2)系统调用所做的;它监视着一系列的一系列的活动者的描述符,如果它们在一定的时间内没有特定的活动,它们就超时了。
一个运行着的事件循环需要什么?
这个不是完整的列表,但是如果你有整体画面,你将能够去猜测什么类需要一个运行着的事件循环。
Widgets的绘画和互动:QWidget::paintEvent()将在传递QPaintEvent对象时候被调用,这个对象将会在调用QWidget::update()或者窗口管理器的时候产生:响应的事件将需要一个时间循环去分发。
Timers:长话短说,它们在当select(2)或者超时的时候产生,因此它们需要让Qt在返回时间循环的时候作这些调用。
Networking“所有的底层Qt网络类 (QTcpSocket,QUdpSocket,QTcpServer等)都是异步设计的。当你调用ready(),它们只是返回已经可用的数据,当你调 用write(),它们只是将这个操作放入队列,适时会写入。只有当你返回消息循环的时候真实的读取,写入才会执行。注意它们确实提供了同步的方法,但是 它们的用法是不被提倡的,因为它们会堵塞事件循环。高级类,比如QNetworkAccessManager,简单的不提供同步API,需要一个事件循 环。
堵塞事件循环
在我们讨论为什么你应该从不堵塞消息循环之前,我们试着分析堵塞的含义。假象你有一个按钮,它将会在它被点击的时候发出clicked信号;在我们的对象中连接着一个槽函数,当你点击了那个按钮后,栈追踪将会像这样:
1. main(int, char **)
2. QApplication::exec()
3. […]
4. QWidget::event(QEvent *)
5. Button::mousePressEvent(QMouseEvent *)
6. Button::clicked()
7. […]
8. Worker::doWork()
在main函数中我们启动了时间循环,平常的调用了 exex(),窗口管理器给我们发送了一个鼠标点击事件,它被Qt内核取走,转换成QMouseEvent并被送往我们widget的event()方 法,该方法被QApplication::notify()发送。因为按钮没有重写event(),基类方法将被调用,QWidget::event() 检测到了这个事件确实是一个鼠标点击事件,然后调用特定的事件处理函数,那就是Button::mousePressEvent(),我们重写这个方法去 发送clicked()信号,这将会调用被连接的槽函数。
当该对象处理量很大,那么消息循环在作什么?我们应该猜测它:什么都不做!它分发鼠标按下事件,然后就堵塞着等待着事件处理函数返回。这个就是堵塞了时间循环,它意味着没有消息被分发了,知道我们从槽函数返回了,然后继续处理挂起的消息。
在消息循环被卡住的情况下,widgets将不能更新它们 自身,不可能有更多的互动,timers将不会被激发,网络通讯将缓慢下来,或者停止。进一步的说,许多窗口管理器将检测到你的应用程序不在处理事件了, 然后告诉用户你的程序没有响应。这就是为什么快速的对事件响应并且即时返回到事件循环是多么的重要!
强制事件分发
所以,假如我们有一个很长的任务去运行但是又不希望堵塞这 个消息循环,该怎么做呢?一个可能的回答是将这个任务移到另一个线程中,在下一个张洁我们将看到这是如何做的。我们也能手动强制事件循环去运行,这个方法 是通过在堵塞的任务函数中调用QCoreApplication::processEvent()来实现 的,QCoreApplication::processEvent()将处理所有在消息队列中的消息并返回给调用者。
另一个可选的选项是我们能够强制重入事件循环的对象,就是QEventLoop类。通过调用QEventLoop::exec()我们将重入事件循环,然后我们能将槽函数QVentLoop::quit()连接到信号上去使它退出。举个例子:
1. QNetworkAccessManager qnam;
2. QNetworkReply *reply = qnam.get(QNetworkRequest(QUrl(...)));
3. QEventLoop loop;
4. QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
5. loop.exec();
6. /* reply has finished, use it */
QNetworkReply不提供堵塞的API,它要求一个在运行的事件循环。我们进入了一个本地的事件循环,然后当回复完成时候,这个本地的循环退出了。
要特别小心的是在其它路径下重入事件循环:它可能导致不希望的递归!让我们回到前面看看按钮的例子。假如我们在槽函数中调用了QCoreApplication::processEvent(),当用户点击了这个按钮,这个槽函数将被再次调用:
1. main(int, char **)
2. QApplication::exec()
3. […]
4. QWidget::event(QEvent *)
5. Button::mousePressEvent(QMouseEvent *)
6. Button::clicked()
7. […]
8. Worker::doWork() // first, inner invocation
9. QCoreApplication::processEvents() // we manually dispatch events and…
10. […]
11. QWidget::event(QEvent * ) // another mouse click is sent to the Button…
12. Button::mousePressEvent(QMouseEvent *)
13. Button::clicked() // which emits clicked() again…
14. […]
15. Worker::doWork() // DANG! we’ve recursed into our slot.
一个快速并简便的变通方法是把QEventLoop::ExcludeUserInputEvent传递给QCoreApplication::processEvents(),这会告诉消息循环不要再次分发任何用户的输入事件。
幸运的是,这个相同的事情不会在检测事件中发生。事实上,它们被Qt通过特殊的方法处理了,只有当运行的时间循环有了一个比deleteLater被调用后更小的”nesting”值才会被处理:
将不会使object成为一个悬空指针。相同的东西被应用 到了本地的事件循环中。唯一的一个显著区别我已经发现了,它在假如当没有事件循环在运行的时候deleteLater被调用了的条件下,然后第一个消息循 环进入了后会取走这个事件,然后删除这个object。这是相当合理的,因为Qt不知道任何外部的循环将最终影响这个检测,因此马上删除了这个 object。