概述
Qt程序是事件驱动的, 程序的每个动作都是由内部某个事件所触发。事件系统在Qt总扮演了十分重要的角色,其事件的生成与事件的派发对GUI 程序起到了核心的作用。Qt事件处理流程图和时序图大致如下, 下面对于整个事件循环系统进行详细的讲解。
1. 事件生成
Qt中事件主要来源于两类,一类是平台插件,另一类是用户自己发送的事件。
平台插件事件
用鼠标事件举例,一个鼠标事件,首先肯定是从驱动中传递上来,uos采用Xorg作为图形服务器,Xorg会加载驱动程序库,并从中获取事件转发到client上,Qt中通过QXcbConnect 连接XServer,并且在prossXcbEvents中使用了while 循环,不断的处理xcb消息并转发出去,Qt客户端中最原始的鼠标事件便来自于这里。那么我们从这里出发看看鼠标事件是怎么从平台插件接收到消息并传递到上层的。
该函数比较简单,首先进入函数先判断连接有错误就退出程序,然后是一个while 循环中不断的从 m_eventQueue 中读取事件,compressEvent是压缩事件的意思,最后通过调用handleXcbEvent函数处理事件。在handleXcbEvent中会根据对应事件类型 调用qxcbwindow中具体的事件处理函数 QXcbWindow::handleMouseEvent 。在该函数中调用了静态函数QWindowSystemInterface::handleMouseEvent,代码如下:
void QXcbWindow::handleMouseEvent(xcb_timestamp_t time, const QPoint &local, const QPoint &global, Qt::KeyboardModifiers modifiers, QEvent::Type type, Qt::MouseEventSource source)
{
m_lastPointerPosition = local;
connection()->setTime(time);
Qt::MouseButton button = type == QEvent::MouseMove ? Qt::NoButton : connection()->button();
QWindowSystemInterface::handleMouseEvent(window(), time, local, global,
connection()->buttonState(), button,
type, modifiers, source);
}
QWindowSystemInterface 类可以看作一个中间层,用于隔离上层的Application和下层的平台插件。Qt是跨平台的,上层需要对下层的具体平台API屏蔽,采用提供QPlatform* 类采用虚接口的方式调用,下层应该尽量减少对上层的依赖。消息就通过QWindowSystemInterface这样一个中间层进行传递。
继续看windowSysteminterface的代码,在handleMouseEvent函数中构造了MouseEvent对象并且通过handleWindowSystemEvent 函数发送,值得注意的是这里虽然构造了鼠标消息,单这并不是真正的QMouseEvent消息,我们继续往下看。
QT_DEFINE_QPA_EVENT_HANDLER(bool, handleMouseEvent, QWindow *window, ulong timestamp,
const QPointF &local, const QPointF &global, Qt::MouseButtons state,
Qt::MouseButton button, QEvent::Type type, Qt::KeyboardModifiers mods,
Qt::MouseEventSource source)
{
...........
auto localPos = QHighDpi::fromNativeLocalPosition(local, window);
auto globalPos = QHighDpi::fromNativePixels(global, window);
QWindowSystemInterfacePrivate::MouseEvent *e =
new QWindowSystemInterfacePrivate::MouseEvent(window, timestamp, localPos, globalPos,
state, mods, button, type, source);
return QWindowSystemInterfacePrivate::handleWindowSystemEvent<Delivery>(e);
}
在 handleWindowSystemEvent 函数中,调用了模板类型的 handleWindowSystemEvent,通过synchronousWindowSystemEvents 判断是调用同步事件处理还是异步事件处理,异步处理方式就加入到事件队列中,同步处理就直接采用函数调用的方式。我们继续分析这两种方式的处理流程。
template<>
bool QWindowSystemInterfacePrivate::handleWindowSystemEvent<QWindowSystemInterface::DefaultDelivery>(QWindowSystemInterfacePrivate::WindowSystemEvent *ev)
{
if (synchronousWindowSystemEvents)
return handleWindowSystemEvent<QWindowSystemInterface::SynchronousDelivery>(ev);
else
return handleWindowSystemEvent<QWindowSystemInterface::AsynchronousDelivery>(ev);
}
同步调用比较简单,在同步调用函数首先判断是否为主线程调用,如果非主线程调用那么就走异步调用,如果是当前线程是主线程,那么调用QGuiApplicationPrivate::processWindowSystemEvent(ev 戒指传递到上层,代码如下。
template<>
bool QWindowSystemInterfacePrivate::handleWindowSystemEvent<QWindowSystemInterface::SynchronousDelivery>(WindowSystemEvent *ev)
{
bool accepted = true;
if (QThread::currentThread() == QGuiApplication::instance()->thread()) {
// Process the event immediately on the current thread and return the accepted state.
QGuiApplicationPrivate::processWindowSystemEvent(ev);
accepted = ev->eventAccepted;
delete ev;
} else {
// Post the event on the Qt main thread queue and flush the queue.
// This will wake up the Gui thread which will process the event.
// Return the accepted state for the last event on the queue,
// which is the event posted by this function.
handleWindowSystemEvent<QWindowSystemInterface::AsynchronousDelivery>(ev);
accepted = QWindowSystemInterface::flushWindowSystemEvents();
}
return accepted;
}
下面是关键函数调用堆栈(还是以mouseEvent为例).
在QGuiApplicationPrivate::processMouseEvent 函数中调用 sendSpontaneousEvent函数,进行发送事件这里可以和上面的用户事件堆栈代码进行对比。sendSpontaneousEvent的和sendEvent 的区别是 前者将事件标记为 Spontaneous,意思是事件起源于应用程序外部,一般来说由平台插件调用。我们自己发送事件就调用sendEvent 。sendSpontaneousEvent函数调用后调用notify 函数然后直接发送到对应类的event函数中。notifyInternal 函数的作用是通知全局回调函数。回调函数被定义为typedef bool (*qInternalCallback)(void **); 该函数定义可以很方便的与其他的语言进行交互,例如脚本、java等。当然也可以通过这个函数捕获所有的Qt事件。notify函数是一个虚函数,自定义的Application可以通过重新实现该函数,达到事件过滤的效果。 notify_helper 的任务同样也是事件过滤的作用,在notify_helper 函数中,会分别给安装在 Application 和receiver 对象中的事件过滤器发送事件。可以看到Qt 可以过滤事件的地方还是很多。
void QXcbConnection::processXcbEvents
void QXcbConnection::handleXcbEvent
void QXcbWindow::handleMouseEvent
static bool QWindowSystemInterface::handleMouseEvent
static bool QWindowSystemInterface:handleWindowSystemEvent(同步)
void QGuiApplicationPrivate::processWindowSystemEvent
void QGuiApplicationPrivate::processMouseEvent
bool QCoreApplication::sendSpontaneousEvent
bool QCoreApplication::notifyInternal2
bool QCoreApplication::notify
bool QCoreApplicationPrivate::notify_helper
receiver->event(event);
在异步调用中,把事件添加到Qt 的事件循环队列中,然后通过事件循环处理事件,然后发送到上层(也就是图上的QGuiApplication层),异步调用代码如下:
template<>
bool QWindowSystemInterfacePrivate::handleWindowSystemEvent<QWindowSystemInterface::AsynchronousDelivery>(WindowSystemEvent *ev)
{
windowSystemEventQueue.append(ev);
if (QAbstractEventDispatcher *dispatcher = QGuiApplicationPrivate::qt_qpa_core_dispatcher())
dispatcher->wakeUp();
return true;
}
总结一下,结合前面的流程图这里的平台事件整个流程就比较清晰了。有两个地方是值得注意的。第一个就是QXcbConnect::prossXcbEvents。这里是有一个while循环不断的读取xcb事件,我们知道一个事件循环一般就是一个while循环不断的取事件并进行分发处理,那么这里是就是整个Qt的事件循环吗? 第二个值得注意的是,handleWindowSystemEvent的异步调用,它将事件加入到事件循环中,这个循环是整个Qt的事件循环吗,那它又是在哪里派发事件? 我们先记住这两点,再来看看用户事件。
用户事件
用户事件比较简单,在单元测试中尤为常见,通过QCoreapplication的postEvent和sendEvent 发送事件。值得注意的是postEvent 是把事件添加到事件循环中,而sendEvent则是直接发送事件。
先看postEvent函数,部分代码如下,第一个参数是消息接收者,第二个参数为具体消息,第三个参数为消息优先级。postEvent 主要是把事件添加到 postEventList 这样一个容器中,然后调用事件分发器的wakeUp。再往下走就走不通了,那么,这个消息进入事件循环后,在哪里进行分发的呢? 稍后我们再讨论事件循环的时候再讲它。
void QCoreApplication::postEvent(QObject *receiver, QEvent *event, int priority)
{
..............
QScopedPointer<QEvent> eventDeleter(event);
Q_TRACE(QCoreApplication_postEvent_event_posted, receiver, event, event->type());
data->postEventList.addEvent(QPostEvent(receiver, event, priority));
eventDeleter.take();
event->posted = true;
++receiver->d_func()->postedEvents;
QAbstractEventDispatcher* dispatcher = data->eventDispatcher.loadAcquire();
if (dispatcher)
dispatcher->wakeUp();
}
sendEvent就没啥好说了,函数参数比postEvent少了一个优先级,我们直接看它的调用堆栈就一目了然.前面讲平台事件的时候已经介绍了这些函数的作用,就不多讲了
bool QCoreApplication:: sendEvent( QObject * receiver, QEvent * event)
--> bool QCoreApplication:: notifyInternal( QObject * receiver, QEvent * event)
-->--> bool QCoreApplication:: notify( QObject * receiver, QEvent * event)
-->-->--> bool QCoreApplicationPrivate::notify_helper(QObject *receiver, QEvent * event)
-->-->-->--> receiver-> event( event);
-->-->-->-->--> receiver->XXXXEvent(event);
到此位置,Qt事件的发生也就是图中红框这一部分基本都介绍完了。
2. 事件循环与分发
事件循环
我们都知道,创建一个Qt程序的时候,总会在main函数中调用QGuiapplication::exec 函数,只有当调用了QGuiapplication::exit 函数的时候,它才会退出,否这永远会”卡”在这个函数中。这个函数被称为事件循环函数。
在这一节我们先来看看事件循环长什么样子,然后结合前面事件的发生来分析事件分发的过程。先来看看exec函数堆栈。
main()
int QApplication::exec()
int QGuiApplication::exec()
int QCoreApplication::exec()
int QEventLoop::exec(ProcessEventsFlags flags)
bool QEventLoop::processEvents(ProcessEventsFlags flags)
bool QXcbGlibEventDispatcher::processEvents
bool QEventDispatcherGlib::processEvents
从main函数开始,依次调用了QApplication、GUI、CORE的exec函数。这三者区别在Qt 官方文档中有详细的描述。我们主要关注两个点 QEventLoop和 QXcbGlibEventDispatcher。
QEventLoop是 事件循环的关键,从堆栈中可以发现在QEventLoop的processEvents中调用了QXcbGlibEventDispatcher::processEvents。而QXcbGlibEventDispatcher继承于QAbstractEventDispatcher类,QAbstractEventDispatcher是事件分发的抽象类,根据不同的平台,有不同的事件分发派生类,例如在windows下使用的就是标准的 Windows 消息机制,在linux下一般是采用glib进行事件分发,关键代码如下:
bool QEventDispatcherGlib::processEvents(QEventLoop::ProcessEventsFlags flags)
{
..................
bool result = g_main_context_iteration(d->mainContext, canWait);
while (!result && canWait)
result = g_main_context_iteration(d->mainContext, canWait);
..................
return result;
}
这个函数应该很熟悉的,在我们调试一些简单的Qt程序的时候,按下暂停断点一般都会停在这里,该函数也是一个while循环一直被循环执行,直至退出。而该函数内部则使用了Glib的GMainContext对象,并调用g_main_context_iteration()函数,该函数将能够遍历一次GMainContext提供的主循环,并有参数确定如果没有事件准备好是否需要的等待。那么GMainContext对象包含什么内容,又是如何创建的呢?
首先在QEventDispatcherGlibPrivate构造函数中,新建GMainContext对象。 然后使用函数g_source_new()创建GSource事件源对象,这里需要传入一组回调函数指针GSourceFuncs,一般包括prepare/check/dispatch这3种回调函数,用于获知事件状态。主循环调用 prepare/check 接口, 询问事件是否准备好如果 prepare 与 check接口的返回值均为 TRUE, 那么此时主事件循环会调用 dispatch 接口分发消息。并使用g_source_attach()将GSource对象绑定到GMainContext对象。 最后如前面所讲,g_main_context_iteration()函数会对GMainContext对象进行检查,会调用前面定义的回调函数, 这里涉及到glib中事件循环的机制,不多展开描述。
在Qt中,QXcbGlibEventDispatcher 创建了XcbEventSource 事件源对象用于处理xcb事件,另外该类继承QEventDispatcherGlib,在父对象的构造函数中 分别创建了post event,socket notification,normal timer,和idle timer这几种事件源对象,总共也就是 5类事件源对象,并将其绑定到GMainContext对象中。
事件分发
也就是这里,在Qt中利用了Glib的事件分发机制,将事件与g_source 绑定,使用一个while循环的机制不断的分发出去。那么Glib的事件是怎么和Qt中对应起来的,又是怎么从事件队列中分发的,以前面用户事件QCoreApplication::postEvent 为例。
回顾一下之前的postEvent 函数的代码,当调用 data->postEventList.addEvent(QPostEvent(receiver, event, priority)); 之后,将事件和对应的接收者打包放在postEventList 容器中,最后调用了一个事件调度器的wakeUp函数 dispatcher->wakeUp();
void QEventDispatcherGlib::wakeUp()
{
Q_D(QEventDispatcherGlib);
d->postEventSource->serialNumber.ref();
g_main_context_wakeup(d->mainContext);
}
在该函数中讲 postEventSource 的 序列号serialNumber 增加 ref表示原子性+1 和 serialNumber++ 类似,并调用了 g_main_context_wakeup函数唤醒poll。这时候g_main_context_iteration 轮询各事件源,检查有没有需要调用的事件,主循环调用 prepare/check 接口, 询问事件是否准备好如果 prepare 与 check接口的返回值均为 TRUE, 那么此时主事件循环会调用 dispatch 接口分发消息。我们查看在prepare 与 check的代码,postEventSourceFuncs 的postEventSourcePrepare函数关键实现如下:
static gboolean postEventSourcePrepare(GSource *s, gint *timeout)
{
QThreadData *data = QThreadData::current();
if (!data)
return false;
gint dummy;
if (!timeout)
timeout = &dummy;
const bool canWait = data->canWaitLocked();
*timeout = canWait ? -1 : 0;
GPostEventSource *source = reinterpret_cast<GPostEventSource *>(s);
source->d->wakeUpCalled = source->serialNumber.loadRelaxed() != source->lastSerialNumber;
return !canWait
}
如果当前序列号与最后一次调用的序列号不同的话,就把 wakeUpCalled 设置为 true 并返回。canWait 表示如果没有挂起的事件,则等待。check 函数同样也调用的 prepare 函数,那么如果我们有postEvent函数加入进来,prepare 和 check 返回为true。接下来Glib库就对调用 dispatch 函数分发事件。
static gboolean postEventSourceDispatch(GSource *s, GSourceFunc, gpointer)
{
GPostEventSource *source = reinterpret_cast<GPostEventSource *>(s);
source->lastSerialNumber = source->serialNumber.loadRelaxed();
QCoreApplication::sendPostedEvents();
source->d->runTimersOnceWithNormalPriority();
return true; // i dunno, george...
}
在dispatch 函数中调用了 QCoreApplication::sendPostedEvents 发送事件,并且把最后一次序列号 lastSerialNumber 设置为当前的序列号。
在 QCoreApplication::sendPostedEvents 函数中,也是一个while 循环,将我们开始放在 postEventList 容器中的事件,然后调用 QCoreApplication::sendEvent(r, e)全部发送出去。sendEvent就前面已经给详细讲解过了,该函数是直接的函数调用,和事件循环无关了。关键代码如下:
void QCoreApplicationPrivate::sendPostedEvents(QObject *receiver, int event_type,
QThreadData *data)
{
.............. //省略部分代码
while (i < data->postEventList.size()) {
// avoid live-lock
if (i >= data->postEventList.insertionOffset)
break;
const QPostEvent &pe = data->postEventList.at(i);
++i;
...............//省略部分代码
pe.event->posted = false;
QEvent *e = pe.event;
QObject * r = pe.receiver;
..............//省略部分代码
// after all that work, it's time to deliver the event.
QCoreApplication::sendEvent(r, e);
// careful when adding anything below this point - the
// sendEvent() call might invalidate any invariants this
// function depends on.
}
cleanup.exceptionCaught = false;
}
所以在linux下整个事件循环的流程图如下,通过上面5中接口,向事件循环中添加事件,事件队列由自己维护,gllib事件分发机制通过回调函数查询是否有需要处理的事件,并调用下面的5种函数接口,对事件队列中的事件pop然后进行处理。
回到开始将平台插件的地方,平台插件的事件异步调用的时候在QWindowSystemInterface 的时候被加入到事件循环中,我们看回顾开始的代码,可以看到该事件循环就是获取的QCoreApplication 中的事件循环。那它的事件分发在哪里呢? 前面讲到的5类事件源对象其中的 XcbEventSource 就是处理这里的事件。在Prepare/check查询函数中永远返回了true。也就是只要有xcb事件就会进行处理。
static gboolean xcbSourcePrepare(GSource *source, gint *timeout)
{
Q_UNUSED(timeout)
auto xcbEventSource = reinterpret_cast<XcbEventSource *>(source);
return xcbEventSource->dispatcher_p->wakeUpCalled;
}
在 dispatch事件分发函数中调用了QWindowSystemInterface::sendWindowSystemEvents函数。然后在该函数调用了 QGuiApplicationPrivate::processWindowSystemEvent 将事件传递到上层处理
static gboolean xcbSourceDispatch(GSource *source, GSourceFunc, gpointer)
{
auto xcbEventSource = reinterpret_cast<XcbEventSource *>(source);
QEventLoop::ProcessEventsFlags flags = xcbEventSource->dispatcher->flags();
xcbEventSource->connection->processXcbEvents(flags);
// The following line should not be necessary after QTBUG-70095
QWindowSystemInterface::sendWindowSystemEvents(flags);
return true;
}
值得注意的是在dispatch 函数中还调用了 processXcbEvents 函数,这个函数是xcb消息起源的函数。前面已经提到了,该函数中也是有个while 循环从 一个队列中取数据,并转化为 xcb 消息。那么该事件队列的数据来自于哪里? Qt主线程负责界面绘制、刷新、事件等任务,而平台消息基于socket,实时性要求较高,所以在平台插件的内部有专门的一个线程QXcbEventQueue负责处理xcb的消息。QXcbEventQueue 继承于QThread,在QxcbConnect中创建,run函数中调用 xcb_wait_for_event 函数等待 xcb消息,并将其加入到 QXcbEventQueue中供 processXcbEvents 处理。
3. QA
到此结束,事件循环就讲解完了,有几个问题
Q: 需要等待100 ms,又不想阻塞主线程,下面代码的原理是什么?
QEventLoop loop;
QTimer::singleShot(100, &loop, SLOT(quit()));
loop.exec();
A:当执行到exec 的代码的时候,程序不再往下继续执行,因为进入了 事件分发器dispatch 函数的while 循环中,如果这时候事件队列中有事件,那么就继续处理事件,如果这时候没有事件,就暂停在这里 100ms 后继续执行后面代码。
Q:QDialog的exec 也调用了exec。其原理是什么?为什么会阻塞其他widget的消息文章来源:https://www.toymoban.com/news/detail-610924.html
A:QDialog的exec 原理和第一个问题一样。同样是创建了QEventloop对象,这时候xcb事件队列中仍然会收到QWidget 的消息,并且进入事件循环,QWidget的消息转发到上层的时候,在QGuiapplication中被 blockedByModalWindow变量拦截。不再进行传递、文章来源地址https://www.toymoban.com/news/detail-610924.html
到了这里,关于Qt事件机制的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!