事件系統
文章為本人理解,如有理解不到位之處,煩請各位指正。
@
Qt的事件迴圈,應該是所有Qter都避不開的一個點,所以,這篇部落格,我們們來了解原始碼中一些關於Qt中事件迴圈的部分。
先丟擲幾個疑問,根據原始碼,下面一一進行解析。
什麼是事件迴圈?
對於Qt事件迴圈個人理解是,事件迴圈是一個佇列去迴圈處理事件。當佇列中有事件時,則去處理事件,如果沒有事件時,則會阻塞等待。
事件是如何產生的?
事件的產生可以分為兩種:
- 程式外部產生
- 程式內部產生
程式外部所產生的事件主要是指系統產生的事件,比如說滑鼠按下(MouseButtonPress)、按鍵按下(KeyPress)等,Qt捕捉系統的事件,然後將系統事件封裝成自己的QEvent
類,再將事件傳送出去。
程式內部產生的事件主要指我們在程式碼裡,手動建立一個事件,然後將事件透過sendEvent
/postEvent
,來傳送到事件迴圈中。而sendEvent
和postEvent
區別又在於一個是阻塞的(sendEvent
)一個是非阻塞的(postEvent
)。
我們結合原始碼分析,看一下sendEvent
和postEvent
分別幹了什麼導致一個是阻塞的一個是非阻塞的。
sendEvent
完整原始碼如下:
bool QCoreApplication::sendEvent(QObject *receiver, QEvent *event)
{
// sendEvent是阻塞呼叫
Q_TRACE(QCoreApplication_sendEvent, receiver, event, event->type());
if (event)
event->spont = false;
return notifyInternal2(receiver, event);
}
可以看到,sendEvent
是呼叫了notifyInternal2
這個函式
bool QCoreApplication::notifyInternal2(QObject *receiver, QEvent *event)
{
...
// Qt enforces the rule that events can only be sent to objects in
// the current thread, so receiver->d_func()->threadData is
// equivalent to QThreadData::current(), just without the function
// call overhead.
// 事件只能在同一個執行緒被send
QObjectPrivate *d = receiver->d_func();
QThreadData *threadData = d->threadData;
QScopedScopeLevelCounter scopeLevelCounter(threadData);
if (!selfRequired)
return doNotify(receiver, event);
return self->notify(receiver, event);
}
進一步跟蹤到其doNotify
函式
static bool doNotify(QObject *receiver, QEvent *event)
{
if (receiver == nullptr) { // serious error
qWarning("QCoreApplication::notify: Unexpected null receiver");
return true;
}
#ifndef QT_NO_DEBUG
// 檢查接受執行緒與當前是否同執行緒
QCoreApplicationPrivate::checkReceiverThread(receiver);
#endif
// QWidget類必須用QApplication
return receiver->isWidgetType() ? false : QCoreApplicationPrivate::notify_helper(receiver, event);
}
再到QCoreApplicationPrivate::notify_helper
bool QCoreApplicationPrivate::notify_helper(QObject *receiver, QEvent * event)
{
// Note: when adjusting the tracepoints in here
// consider adjusting QApplicationPrivate::notify_helper too.
Q_TRACE(QCoreApplication_notify_entry, receiver, event, event->type());
bool consumed = false;
bool filtered = false;
Q_TRACE_EXIT(QCoreApplication_notify_exit, consumed, filtered);
// send to all application event filters (only does anything in the main thread)
if (QCoreApplication::self
&& receiver->d_func()->threadData.loadRelaxed()->thread.loadAcquire() == mainThread()
&& QCoreApplication::self->d_func()->sendThroughApplicationEventFilters(receiver, event)) {
filtered = true;
return filtered;
}
// send to all receiver event filters
if (sendThroughObjectEventFilters(receiver, event)) {
filtered = true;
return filtered;
}
// deliver the event
// 直接呼叫物件的event函式,所以是阻塞的
consumed = receiver->event(event);
return consumed;
}
然後我們可以看到主要有幾個流程:
-
判斷QCoreApplication有沒有安裝事件過濾器,有就把訊號傳送到事件過濾器裡,由事件過濾器對事件進行處理。
// send to all application event filters (only does anything in the main thread) if (QCoreApplication::self && receiver->d_func()->threadData.loadRelaxed()->thread.loadAcquire() == mainThread() && QCoreApplication::self->d_func()->sendThroughApplicationEventFilters(receiver, event)) { filtered = true; return filtered; }
-
判斷事件接受物件,有沒有安裝事件過濾器,有就將訊號傳送到事件過濾器。
// send to all receiver event filters if (sendThroughObjectEventFilters(receiver, event)) { filtered = true; return filtered; }
具體遍歷事件接受物件所安裝的事件過濾器的程式碼如下:
bool QCoreApplicationPrivate::sendThroughObjectEventFilters(QObject *receiver, QEvent *event) { if (receiver != QCoreApplication::instance() && receiver->d_func()->extraData) { for (int i = 0; i < receiver->d_func()->extraData->eventFilters.size(); ++i) { QObject *obj = receiver->d_func()->extraData->eventFilters.at(i); if (!obj) continue; if (obj->d_func()->threadData != receiver->d_func()->threadData) { qWarning("QCoreApplication: Object event filter cannot be in a different thread."); continue; } if (obj->eventFilter(receiver, event)) return true; } } return false; }
我們可以看到,只要事件被一個事件過濾器所成功處理,那麼後續的事件過濾器就不會被響應。同時,參看Qt幫助手冊中有提及到:
If multiple event filters are installed on a single object, the filter that was installed last is activated first.
後插入的事件過濾器會被優先響應。 具體安裝事件過濾器,我們在後面進行分析。
-
直接呼叫事件接受物件的
event
函式進行處理。因為是直接呼叫的物件的event
,所以說,sendEvent
函式會阻塞等待。// deliver the event // 直接呼叫物件的event函式,所以是阻塞的 consumed = receiver->event(event); return consumed
postEvent
完整程式碼如下:
void QCoreApplication::postEvent(QObject *receiver, QEvent *event, int priority)
{
Q_TRACE_SCOPE(QCoreApplication_postEvent, receiver, event, event->type());
// 事件的接收者不能為空
if (receiver == nullptr) {
qWarning("QCoreApplication::postEvent: Unexpected null receiver");
delete event;
return;
}
// 對事件接受物件所線上程的事件處理列表上鎖
auto locker = QCoreApplicationPrivate::lockThreadPostEventList(receiver);
if (!locker.threadData) {
// posting during destruction? just delete the event to prevent a leak
delete event;
return;
}
QThreadData *data = locker.threadData;
// if this is one of the compressible events, do compression
// 將重複的事件,進行壓縮
if (receiver->d_func()->postedEvents
&& self && self->compressEvent(event, receiver, &data->postEventList)) {
Q_TRACE(QCoreApplication_postEvent_event_compressed, receiver, event);
return;
}
if (event->type() == QEvent::DeferredDelete)
receiver->d_ptr->deleteLaterCalled = true;
if (event->type() == QEvent::DeferredDelete && data == QThreadData::current()) {
// remember the current running eventloop for DeferredDelete
// events posted in the receiver's thread.
// Events sent by non-Qt event handlers (such as glib) may not
// have the scopeLevel set correctly. The scope level makes sure that
// code like this:
// foo->deleteLater();
// qApp->processEvents(); // without passing QEvent::DeferredDelete
// will not cause "foo" to be deleted before returning to the event loop.
// If the scope level is 0 while loopLevel != 0, we are called from a
// non-conformant code path, and our best guess is that the scope level
// should be 1. (Loop level 0 is special: it means that no event loops
// are running.)
int loopLevel = data->loopLevel;
int scopeLevel = data->scopeLevel;
if (scopeLevel == 0 && loopLevel != 0)
scopeLevel = 1;
static_cast<QDeferredDeleteEvent *>(event)->level = loopLevel + scopeLevel;
}
// delete the event on exceptions to protect against memory leaks till the event is
// properly owned in the postEventList
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;
data->canWait = false;
locker.unlock();
QAbstractEventDispatcher* dispatcher = data->eventDispatcher.loadAcquire();
if (dispatcher)
dispatcher->wakeUp();
}
-
判斷事件接收物件是否為空
// 事件的接收者不能為空 if (receiver == nullptr) { qWarning("QCoreApplication::postEvent: Unexpected null receiver"); delete event; return; }
-
將事件接收物件所線上程的post事件列表上鎖,如果已經被鎖了,就把事件刪除掉,並返回,防止洩露。
// 對事件接受物件所線上程的事件處理列表上鎖 auto locker = QCoreApplicationPrivate::lockThreadPostEventList(receiver); if (!locker.threadData) { // posting during destruction? just delete the event to prevent a leak delete event; return; }
-
將一些可以壓縮的事件進行壓縮,及多個事件壓縮成只推送最後的一個事件。Qt介面的
update
就是這個操作,為了防止多次重新整理導致卡頓,短時間內多次的呼叫update
可能只會重新整理一次// if this is one of the compressible events, do compression // 將重複的事件,進行壓縮 if (receiver->d_func()->postedEvents && self && self->compressEvent(event, receiver, &data->postEventList)) { Q_TRACE(QCoreApplication_postEvent_event_compressed, receiver, event); return; }
-
將事件插入接收物件所線上程的post事件列表中,並喚醒執行緒的事件排程器,來進行事件的處理。所以
postEvent
是非阻塞的,因為其只是把事件插入了執行緒的事件列表,喚醒事件排程器之後便返回。// delete the event on exceptions to protect against memory leaks till the event is // properly owned in the postEventList 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; data->canWait = false; locker.unlock(); QAbstractEventDispatcher* dispatcher = data->eventDispatcher.loadAcquire(); if (dispatcher) dispatcher->wakeUp();
事件是如何處理的?
在Qt中,事件的接收者都是QObject
,而QObject
中事件處理是呼叫event
函式。如果當時物件不處理某個事件,就會將其轉發到父類的event
進行處理。
而事件的處理,主要分為三個部分:
- 先是由事件迴圈遍歷事件
- 然後判斷事件接受物件有沒有安裝事件過濾器(
installEventFilter
),有安裝的話,就把事件丟給事件過濾器(eventFilter
)進行處理。 - 如果沒有安裝事件過濾器或者事件過濾器對該事件不進行處理的話,那麼,事件將會進一步轉發到
event
函式里進行處理。
所以,在這一章節,我們同樣一步一步的分析這三個點。
事件迴圈是怎麼遍歷的?
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
上面是一個經典的QtGUI程式的main函式,呼叫a.exec()
int QCoreApplication::exec()
{
...
threadData->quitNow = false;
QEventLoop eventLoop;
self->d_func()->in_exec = true;
self->d_func()->aboutToQuitEmitted = false;
int returnCode = eventLoop.exec();
...
}
而看QApplication::exec
的原始碼,實際上就是開啟了一個事件迴圈(QEventLoop
)。同樣,我們去看QEventLoop::exec
的原始碼,進一步看處理事件的步驟是什麼。
int QEventLoop::exec(ProcessEventsFlags flags)
{
...
while (!d->exit.loadAcquire())
processEvents(flags | WaitForMoreEvents | EventLoopExec);
ref.exceptionCaught = false;
return d->returnCode.loadRelaxed();
}
上面可以看到,QEvenLoop::exec
裡,是一個while
迴圈,迴圈的去呼叫processEvent
,而且設定了WaitForMoreEvents
就是說,如果沒有事件,就阻塞等待。
void QCoreApplication::processEvents(QEventLoop::ProcessEventsFlags flags, int ms)
{
// ### Qt 6: consider splitting this method into a public and a private
// one, so that a user-invoked processEvents can be detected
// and handled properly.
QThreadData *data = QThreadData::current();
if (!data->hasEventDispatcher())
return;
QElapsedTimer start;
start.start();
while (data->eventDispatcher.loadRelaxed()->processEvents(flags & ~QEventLoop::WaitForMoreEvents)) {
if (start.elapsed() > ms)
break;
}
}
閱讀processEvent
,其呼叫了執行緒的事件排程器QAbstrctEventDispatcher
,而這個類是一個抽象基類,根據不同的平臺,有不同的實現,我們以windows下(QEventDispatcherWin32
)的為例,接著分析事件處理的流程。
bool QEventDispatcherWin32::processEvents(QEventLoop::ProcessEventsFlags flags)
{
Q_D(QEventDispatcherWin32);
...
// To prevent livelocks, send posted events once per iteration.
// QCoreApplication::sendPostedEvents() takes care about recursions.
sendPostedEvents();
...
}
void QEventDispatcherWin32::sendPostedEvents()
{
Q_D(QEventDispatcherWin32);
if (d->sendPostedEventsTimerId != 0)
KillTimer(d->internalHwnd, d->sendPostedEventsTimerId);
d->sendPostedEventsTimerId = 0;
// Allow posting WM_QT_SENDPOSTEDEVENTS message.
d->wakeUps.storeRelaxed(0);
QCoreApplicationPrivate::sendPostedEvents(0, 0, d->threadData.loadRelaxed());
}
可以看到,事件排程器最終還是呼叫了QCoreApplication
的sendPostEvents
void QCoreApplicationPrivate::sendPostedEvents(QObject *receiver, int event_type,
QThreadData *data)
{
if (event_type == -1) {
// we were called by an obsolete event dispatcher.
event_type = 0;
}
if (receiver && receiver->d_func()->threadData != data) {
qWarning("QCoreApplication::sendPostedEvents: Cannot send "
"posted events for objects in another thread");
return;
}
...
// Exception-safe cleaning up without the need for a try/catch block
struct CleanUp {
QObject *receiver;
int event_type;
QThreadData *data;
bool exceptionCaught;
inline CleanUp(QObject *receiver, int event_type, QThreadData *data) :
receiver(receiver), event_type(event_type), data(data), exceptionCaught(true)
{}
inline ~CleanUp()
{
if (exceptionCaught) {
// since we were interrupted, we need another pass to make sure we clean everything up
data->canWait = false;
}
--data->postEventList.recursion;
if (!data->postEventList.recursion && !data->canWait && data->hasEventDispatcher())
data->eventDispatcher.loadRelaxed()->wakeUp();
// clear the global list, i.e. remove everything that was
// delivered.
if (!event_type && !receiver && data->postEventList.startOffset >= 0) {
const QPostEventList::iterator it = data->postEventList.begin();
data->postEventList.erase(it, it + data->postEventList.startOffset);
data->postEventList.insertionOffset -= data->postEventList.startOffset;
Q_ASSERT(data->postEventList.insertionOffset >= 0);
data->postEventList.startOffset = 0;
}
}
};
CleanUp cleanup(receiver, event_type, data);
while (i < data->postEventList.size()) {
...
// first, we diddle the event so that we can deliver
// it, and that no one will try to touch it later.
pe.event->posted = false;
QEvent *e = pe.event;
QObject * r = pe.receiver;
--r->d_func()->postedEvents;
Q_ASSERT(r->d_func()->postedEvents >= 0);
// next, update the data structure so that we're ready
// for the next event.
const_cast<QPostEvent &>(pe).event = nullptr;
locker.unlock();
const auto relocker = qScopeGuard([&locker] { locker.lock(); });
QScopedPointer<QEvent> event_deleter(e); // will delete the event (with the mutex unlocked)
// 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;
}
我們一個一個的分塊分析:
-
判斷是否在一個執行緒
if (receiver && receiver->d_func()->threadData != data) { qWarning("QCoreApplication::sendPostedEvents: Cannot send " "posted events for objects in another thread"); return; }
-
一個有意思的異常安全的處理,不需要try/catch塊
// Exception-safe cleaning up without the need for a try/catch block struct CleanUp { QObject *receiver; int event_type; QThreadData *data; bool exceptionCaught; inline CleanUp(QObject *receiver, int event_type, QThreadData *data) : receiver(receiver), event_type(event_type), data(data), exceptionCaught(true) {} inline ~CleanUp() { if (exceptionCaught) { // since we were interrupted, we need another pass to make sure we clean everything up data->canWait = false; } --data->postEventList.recursion; if (!data->postEventList.recursion && !data->canWait && data->hasEventDispatcher()) data->eventDispatcher.loadRelaxed()->wakeUp(); // clear the global list, i.e. remove everything that was // delivered. if (!event_type && !receiver && data->postEventList.startOffset >= 0) { const QPostEventList::iterator it = data->postEventList.begin(); data->postEventList.erase(it, it + data->postEventList.startOffset); data->postEventList.insertionOffset -= data->postEventList.startOffset; Q_ASSERT(data->postEventList.insertionOffset >= 0); data->postEventList.startOffset = 0; } } }; CleanUp cleanup(receiver, event_type, data);
定義了一個結構體CleanUp
,結構體的解構函式(~CleanUp
)儲存了函式退出時需要執行的清理操作。然後在棧上建立了一個結構體物件,遍歷事件列表時,異常退出,那麼就會呼叫自動呼叫~CleanUp
的解構函式。
-
將事件傳送出去(
sendEvent
)while (i < data->postEventList.size()) { ... // first, we diddle the event so that we can deliver // it, and that no one will try to touch it later. pe.event->posted = false; QEvent *e = pe.event; QObject * r = pe.receiver; --r->d_func()->postedEvents; Q_ASSERT(r->d_func()->postedEvents >= 0); // next, update the data structure so that we're ready // for the next event. const_cast<QPostEvent &>(pe).event = nullptr; locker.unlock(); const auto relocker = qScopeGuard([&locker] { locker.lock(); }); QScopedPointer<QEvent> event_deleter(e); // will delete the event (with the mutex unlocked) // 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. }
可以看到,核心還是呼叫sendEvent
將事件傳送出去,而前面我們對sendEvent
的原始碼分析我們可以看到,事件先是經過事件過濾器,再經過物件的event函式,來進行事件的處理。所以就引出我們的下一個話題:事件過濾器
事件過濾器
在實際應用中,我們經常要將某一個視窗部件的某個事件如滑鼠滑輪滾動攔截,然後執行我們自己想要的操作。這個時候,我們就可以用到事件過濾器(EventFilter
**) **
首先,我們需要自己編寫一個eventFilter
函式,
bool Class::eventFilter(QObject* watcher, QEvent* event)
{
//以過濾滑鼠滾輪事件為例
if (object == m_watcherObject && event->type() == QEvent::Wheel) {
// do something
return true;
}
QWidget::eventFilter(watcher, event);
}
然後,我們需要為要攔截的某個視窗部件,安裝事件過濾器
void Class::initUI()
{
QWidget* m_watcherObject = new QWidget(this);
// 為物件安裝一個事件過濾器
m_watcherObject->installEventFilterr(this);
}
initUI();
那麼一個物件安裝的多個事件過濾器,會以什麼樣的順序觸發呢?我們在前面的講過,後安裝的事件過濾器會先觸發,這一點,我們可以在原始碼裡得到佐證:
void QObject::installEventFilter(QObject *obj)
{
Q_D(QObject);
if (!obj)
return;
if (d->threadData != obj->d_func()->threadData) {
qWarning("QObject::installEventFilter(): Cannot filter events for objects in a different thread.");
return;
}
if (!d->extraData)
d->extraData = new QObjectPrivate::ExtraData;
// clean up unused items in the list
d->extraData->eventFilters.removeAll((QObject*)nullptr);
d->extraData->eventFilters.removeAll(obj);
d->extraData->eventFilters.prepend(obj);
}
可以清楚的看到,事件過濾器,是以prepend
的形式被新增進事件過濾器列表的。
那麼,當有滑鼠滾輪事件觸發的時候,我們可以看到sendEvent
會優先走到事件過濾器裡,如果eventFilter
返回一個true,那麼事件就不會被繼續派發,否則,將會將事件傳送到其他的事件過濾器裡進行處理,如果其他的事件過濾器均對該事件不進行處理,那麼事件將會繼續往下派發,走到事件的處理函式event
event
接下來,就到了事件處理的最後一站,event
函式,這個函式比較簡單,我們可以自己重寫這個函式,對事件進行自定義的處理。
bool Class::event(QEvent *e)
{
switch (e->type()) {
case QEvent::Whell:
// do something
return true;
default:
if (e->type() >= QEvent::User) {
customEvent(e);
break;
}
return false;
}
return true;
}
夾帶私貨時間
- 之前有說到
processEvent
,新增一個小經驗。當我們有時候不得不在主執行緒迴圈執行很耗時的操作的時候,這個時候,介面就會重新整理不過來,就會導致介面卡頓,影響使用。但是,我們可以在這個迴圈裡,手動呼叫qApp->processEvent()
,這樣就可以手動呼叫處理掉所有的事件,就可以解決卡頓的問題。