概述
Qt程序是事件驅(qū)動的, 程序的每個動作都是由內(nèi)部某個事件所觸發(fā)。事件系統(tǒng)在Qt總扮演了十分重要的角色,其事件的生成與事件的派發(fā)對GUI 程序起到了核心的作用。Qt事件處理流程圖和時序圖大致如下, 下面對于整個事件循環(huán)系統(tǒng)進行詳細的講解。


1. 事件生成
Qt中事件主要來源于兩類,一類是平臺插件,另一類是用戶自己發(fā)送的事件。
平臺插件事件
用鼠標事件舉例,一個鼠標事件,首先肯定是從驅(qū)動中傳遞上來,uos采用Xorg作為圖形服務(wù)器,Xorg會加載驅(qū)動程序庫,并從中獲取事件轉(zhuǎn)發(fā)到client上,Qt中通過QXcbConnect 連接XServer,并且在prossXcbEvents中使用了while 循環(huán),不斷的處理xcb消息并轉(zhuǎn)發(fā)出去,Qt客戶端中最原始的鼠標事件便來自于這里。那么我們從這里出發(fā)看看鼠標事件是怎么從平臺插件接收到消息并傳遞到上層的。

該函數(shù)比較簡單,首先進入函數(shù)先判斷連接有錯誤就退出程序,然后是一個while 循環(huán)中不斷的從 m_eventQueue 中讀取事件,compressEvent是壓縮事件的意思,最后通過調(diào)用handleXcbEvent函數(shù)處理事件。在handleXcbEvent中會根據(jù)對應(yīng)事件類型 調(diào)用qxcbwindow中具體的事件處理函數(shù) QXcbWindow::handleMouseEvent 。在該函數(shù)中調(diào)用了靜態(tài)函數(shù)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* 類采用虛接口的方式調(diào)用,下層應(yīng)該盡量減少對上層的依賴。消息就通過QWindowSystemInterface這樣一個中間層進行傳遞。

繼續(xù)看windowSysteminterface的代碼,在handleMouseEvent函數(shù)中構(gòu)造了MouseEvent對象并且通過handleWindowSystemEvent 函數(shù)發(fā)送,值得注意的是這里雖然構(gòu)造了鼠標消息,單這并不是真正的QMouseEvent消息,我們繼續(xù)往下看。
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 函數(shù)中,調(diào)用了模板類型的 handleWindowSystemEvent,通過synchronousWindowSystemEvents 判斷是調(diào)用同步事件處理還是異步事件處理,異步處理方式就加入到事件隊列中,同步處理就直接采用函數(shù)調(diào)用的方式。我們繼續(xù)分析這兩種方式的處理流程。
template<>
bool QWindowSystemInterfacePrivate::handleWindowSystemEvent<QWindowSystemInterface::DefaultDelivery>(QWindowSystemInterfacePrivate::WindowSystemEvent *ev)
{
if (synchronousWindowSystemEvents)
return handleWindowSystemEvent<QWindowSystemInterface::SynchronousDelivery>(ev);
else
return handleWindowSystemEvent<QWindowSystemInterface::AsynchronousDelivery>(ev);
}
同步調(diào)用比較簡單,在同步調(diào)用函數(shù)首先判斷是否為主線程調(diào)用,如果非主線程調(diào)用那么就走異步調(diào)用,如果是當前線程是主線程,那么調(diào)用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;
}
下面是關(guān)鍵函數(shù)調(diào)用堆棧(還是以mouseEvent為例).
在QGuiApplicationPrivate::processMouseEvent 函數(shù)中調(diào)用 sendSpontaneousEvent函數(shù),進行發(fā)送事件這里可以和上面的用戶事件堆棧代碼進行對比。sendSpontaneousEvent的和sendEvent 的區(qū)別是 前者將事件標記為 Spontaneous,意思是事件起源于應(yīng)用程序外部,一般來說由平臺插件調(diào)用。我們自己發(fā)送事件就調(diào)用sendEvent 。sendSpontaneousEvent函數(shù)調(diào)用后調(diào)用notify 函數(shù)然后直接發(fā)送到對應(yīng)類的event函數(shù)中。notifyInternal 函數(shù)的作用是通知全局回調(diào)函數(shù)?;卣{(diào)函數(shù)被定義為typedef bool (*qInternalCallback)(void **); 該函數(shù)定義可以很方便的與其他的語言進行交互,例如腳本、java等。當然也可以通過這個函數(shù)捕獲所有的Qt事件。notify函數(shù)是一個虛函數(shù),自定義的Application可以通過重新實現(xiàn)該函數(shù),達到事件過濾的效果。 notify_helper 的任務(wù)同樣也是事件過濾的作用,在notify_helper 函數(shù)中,會分別給安裝在 Application 和receiver 對象中的事件過濾器發(fā)送事件??梢钥吹絈t 可以過濾事件的地方還是很多。
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);
在異步調(diào)用中,把事件添加到Qt 的事件循環(huán)隊列中,然后通過事件循環(huán)處理事件,然后發(fā)送到上層(也就是圖上的QGuiApplication層),異步調(diào)用代碼如下:
template<>
bool QWindowSystemInterfacePrivate::handleWindowSystemEvent<QWindowSystemInterface::AsynchronousDelivery>(WindowSystemEvent *ev)
{
windowSystemEventQueue.append(ev);
if (QAbstractEventDispatcher *dispatcher = QGuiApplicationPrivate::qt_qpa_core_dispatcher())
dispatcher->wakeUp();
return true;
}
總結(jié)一下,結(jié)合前面的流程圖這里的平臺事件整個流程就比較清晰了。有兩個地方是值得注意的。第一個就是QXcbConnect::prossXcbEvents。這里是有一個while循環(huán)不斷的讀取xcb事件,我們知道一個事件循環(huán)一般就是一個while循環(huán)不斷的取事件并進行分發(fā)處理,那么這里是就是整個Qt的事件循環(huán)嗎? 第二個值得注意的是,handleWindowSystemEvent的異步調(diào)用,它將事件加入到事件循環(huán)中,這個循環(huán)是整個Qt的事件循環(huán)嗎,那它又是在哪里派發(fā)事件? 我們先記住這兩點,再來看看用戶事件。
用戶事件
用戶事件比較簡單,在單元測試中尤為常見,通過QCoreapplication的postEvent和sendEvent 發(fā)送事件。值得注意的是postEvent 是把事件添加到事件循環(huán)中,而sendEvent則是直接發(fā)送事件。
先看postEvent函數(shù),部分代碼如下,第一個參數(shù)是消息接收者,第二個參數(shù)為具體消息,第三個參數(shù)為消息優(yōu)先級。postEvent 主要是把事件添加到 postEventList 這樣一個容器中,然后調(diào)用事件分發(fā)器的wakeUp。再往下走就走不通了,那么,這個消息進入事件循環(huán)后,在哪里進行分發(fā)的呢? 稍后我們再討論事件循環(huán)的時候再講它。
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就沒啥好說了,函數(shù)參數(shù)比postEvent少了一個優(yōu)先級,我們直接看它的調(diào)用堆棧就一目了然.前面講平臺事件的時候已經(jīng)介紹了這些函數(shù)的作用,就不多講了
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事件的發(fā)生也就是圖中紅框這一部分基本都介紹完了。

2. 事件循環(huán)與分發(fā)
事件循環(huán)
我們都知道,創(chuàng)建一個Qt程序的時候,總會在main函數(shù)中調(diào)用QGuiapplication::exec 函數(shù),只有當調(diào)用了QGuiapplication::exit 函數(shù)的時候,它才會退出,否這永遠會”卡”在這個函數(shù)中。這個函數(shù)被稱為事件循環(huán)函數(shù)。
在這一節(jié)我們先來看看事件循環(huán)長什么樣子,然后結(jié)合前面事件的發(fā)生來分析事件分發(fā)的過程。先來看看exec函數(shù)堆棧。
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函數(shù)開始,依次調(diào)用了QApplication、GUI、CORE的exec函數(shù)。這三者區(qū)別在Qt 官方文檔中有詳細的描述。我們主要關(guān)注兩個點 QEventLoop和 QXcbGlibEventDispatcher。
QEventLoop是 事件循環(huán)的關(guān)鍵,從堆棧中可以發(fā)現(xiàn)在QEventLoop的processEvents中調(diào)用了QXcbGlibEventDispatcher::processEvents。而QXcbGlibEventDispatcher繼承于QAbstractEventDispatcher類,QAbstractEventDispatcher是事件分發(fā)的抽象類,根據(jù)不同的平臺,有不同的事件分發(fā)派生類,例如在windows下使用的就是標準的 Windows 消息機制,在linux下一般是采用glib進行事件分發(fā),關(guān)鍵代碼如下:
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;
}
這個函數(shù)應(yīng)該很熟悉的,在我們調(diào)試一些簡單的Qt程序的時候,按下暫停斷點一般都會停在這里,該函數(shù)也是一個while循環(huán)一直被循環(huán)執(zhí)行,直至退出。而該函數(shù)內(nèi)部則使用了Glib的GMainContext對象,并調(diào)用g_main_context_iteration()函數(shù),該函數(shù)將能夠遍歷一次GMainContext提供的主循環(huán),并有參數(shù)確定如果沒有事件準備好是否需要的等待。那么GMainContext對象包含什么內(nèi)容,又是如何創(chuàng)建的呢?
首先在QEventDispatcherGlibPrivate構(gòu)造函數(shù)中,新建GMainContext對象。 然后使用函數(shù)g_source_new()創(chuàng)建GSource事件源對象,這里需要傳入一組回調(diào)函數(shù)指針GSourceFuncs,一般包括prepare/check/dispatch這3種回調(diào)函數(shù),用于獲知事件狀態(tài)。主循環(huán)調(diào)用 prepare/check 接口, 詢問事件是否準備好如果 prepare 與 check接口的返回值均為 TRUE, 那么此時主事件循環(huán)會調(diào)用 dispatch 接口分發(fā)消息。并使用g_source_attach()將GSource對象綁定到GMainContext對象。 最后如前面所講,g_main_context_iteration()函數(shù)會對GMainContext對象進行檢查,會調(diào)用前面定義的回調(diào)函數(shù), 這里涉及到glib中事件循環(huán)的機制,不多展開描述。
在Qt中,QXcbGlibEventDispatcher 創(chuàng)建了XcbEventSource 事件源對象用于處理xcb事件,另外該類繼承QEventDispatcherGlib,在父對象的構(gòu)造函數(shù)中 分別創(chuàng)建了post event,socket notification,normal timer,和idle timer這幾種事件源對象,總共也就是 5類事件源對象,并將其綁定到GMainContext對象中。

事件分發(fā)
也就是這里,在Qt中利用了Glib的事件分發(fā)機制,將事件與g_source 綁定,使用一個while循環(huán)的機制不斷的分發(fā)出去。那么Glib的事件是怎么和Qt中對應(yīng)起來的,又是怎么從事件隊列中分發(fā)的,以前面用戶事件QCoreApplication::postEvent 為例。
回顧一下之前的postEvent 函數(shù)的代碼,當調(diào)用 data->postEventList.addEvent(QPostEvent(receiver, event, priority)); 之后,將事件和對應(yīng)的接收者打包放在postEventList 容器中,最后調(diào)用了一個事件調(diào)度器的wakeUp函數(shù) dispatcher->wakeUp();
void QEventDispatcherGlib::wakeUp()
{
Q_D(QEventDispatcherGlib);
d->postEventSource->serialNumber.ref();
g_main_context_wakeup(d->mainContext);
}
在該函數(shù)中講 postEventSource 的 序列號serialNumber 增加 ref表示原子性+1 和 serialNumber++ 類似,并調(diào)用了 g_main_context_wakeup函數(shù)喚醒poll。這時候g_main_context_iteration 輪詢各事件源,檢查有沒有需要調(diào)用的事件,主循環(huán)調(diào)用 prepare/check 接口, 詢問事件是否準備好如果 prepare 與 check接口的返回值均為 TRUE, 那么此時主事件循環(huán)會調(diào)用 dispatch 接口分發(fā)消息。我們查看在prepare 與 check的代碼,postEventSourceFuncs 的postEventSourcePrepare函數(shù)關(guān)鍵實現(xiàn)如下:
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
}
如果當前序列號與最后一次調(diào)用的序列號不同的話,就把 wakeUpCalled 設(shè)置為 true 并返回。canWait 表示如果沒有掛起的事件,則等待。check 函數(shù)同樣也調(diào)用的 prepare 函數(shù),那么如果我們有postEvent函數(shù)加入進來,prepare 和 check 返回為true。接下來Glib庫就對調(diào)用 dispatch 函數(shù)分發(fā)事件。
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 函數(shù)中調(diào)用了 QCoreApplication::sendPostedEvents 發(fā)送事件,并且把最后一次序列號 lastSerialNumber 設(shè)置為當前的序列號。
在 QCoreApplication::sendPostedEvents 函數(shù)中,也是一個while 循環(huán),將我們開始放在 postEventList 容器中的事件,然后調(diào)用 QCoreApplication::sendEvent(r, e)全部發(fā)送出去。sendEvent就前面已經(jīng)給詳細講解過了,該函數(shù)是直接的函數(shù)調(diào)用,和事件循環(huán)無關(guān)了。關(guān)鍵代碼如下:
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下整個事件循環(huán)的流程圖如下,通過上面5中接口,向事件循環(huán)中添加事件,事件隊列由自己維護,gllib事件分發(fā)機制通過回調(diào)函數(shù)查詢是否有需要處理的事件,并調(diào)用下面的5種函數(shù)接口,對事件隊列中的事件pop然后進行處理。

回到開始將平臺插件的地方,平臺插件的事件異步調(diào)用的時候在QWindowSystemInterface 的時候被加入到事件循環(huán)中,我們看回顧開始的代碼,可以看到該事件循環(huán)就是獲取的QCoreApplication 中的事件循環(huán)。那它的事件分發(fā)在哪里呢? 前面講到的5類事件源對象其中的 XcbEventSource 就是處理這里的事件。在Prepare/check查詢函數(shù)中永遠返回了true。也就是只要有xcb事件就會進行處理。
static gboolean xcbSourcePrepare(GSource *source, gint *timeout)
{
Q_UNUSED(timeout)
auto xcbEventSource = reinterpret_cast<XcbEventSource *>(source);
return xcbEventSource->dispatcher_p->wakeUpCalled;
}
在 dispatch事件分發(fā)函數(shù)中調(diào)用了QWindowSystemInterface::sendWindowSystemEvents函數(shù)。然后在該函數(shù)調(diào)用了 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 函數(shù)中還調(diào)用了 processXcbEvents 函數(shù),這個函數(shù)是xcb消息起源的函數(shù)。前面已經(jīng)提到了,該函數(shù)中也是有個while 循環(huán)從 一個隊列中取數(shù)據(jù),并轉(zhuǎn)化為 xcb 消息。那么該事件隊列的數(shù)據(jù)來自于哪里? Qt主線程負責界面繪制、刷新、事件等任務(wù),而平臺消息基于socket,實時性要求較高,所以在平臺插件的內(nèi)部有專門的一個線程QXcbEventQueue負責處理xcb的消息。QXcbEventQueue 繼承于QThread,在QxcbConnect中創(chuàng)建,run函數(shù)中調(diào)用 xcb_wait_for_event 函數(shù)等待 xcb消息,并將其加入到 QXcbEventQueue中供 processXcbEvents 處理。
3. QA
到此結(jié)束,事件循環(huán)就講解完了,有幾個問題
Q: 需要等待100 ms,又不想阻塞主線程,下面代碼的原理是什么?
QEventLoop loop;
QTimer::singleShot(100, &loop, SLOT(quit()));
loop.exec();
A:當執(zhí)行到exec 的代碼的時候,程序不再往下繼續(xù)執(zhí)行,因為進入了 事件分發(fā)器dispatch 函數(shù)的while 循環(huán)中,如果這時候事件隊列中有事件,那么就繼續(xù)處理事件,如果這時候沒有事件,就暫停在這里 100ms 后繼續(xù)執(zhí)行后面代碼。
Q:QDialog的exec 也調(diào)用了exec。其原理是什么?為什么會阻塞其他widget的消息文章來源:http://www.zghlxwxcb.cn/news/detail-610924.html
A:QDialog的exec 原理和第一個問題一樣。同樣是創(chuàng)建了QEventloop對象,這時候xcb事件隊列中仍然會收到QWidget 的消息,并且進入事件循環(huán),QWidget的消息轉(zhuǎn)發(fā)到上層的時候,在QGuiapplication中被 blockedByModalWindow變量攔截。不再進行傳遞、文章來源地址http://www.zghlxwxcb.cn/news/detail-610924.html
到了這里,關(guān)于Qt事件機制的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!