在Qt中,事件(event)被封裝為QEvent類/子類物件,用來表示應用內部或外部發生的各種事情。事件可以被任何QObject子類的物件接收並處理。
根據事件的建立方式和排程方式,Qt中事件可分為三類,分別是:
Qt中的事件迴圈(event loop)是通過一個佇列來回圈處理事件的機制。當佇列中有事件時,事件迴圈會處理事件;如果佇列中沒有事件,則事件迴圈會阻塞等待。
在Qt程式中,每個執行緒都可以有自己的事件迴圈(每個執行緒只能擁有一個活動的事件迴圈,並對應一個事件佇列),事件迴圈在呼叫.exec()時進行啟動。主執行緒/GUI執行緒對應的事件迴圈,被稱為主事件迴圈(main event loop)。一般來說,在main()函數的末尾會呼叫QApplication::exec()函數,從而啟動並進入Qt的主事件迴圈(更準確地說,Qt的主事件迴圈在主執行緒中呼叫QCoreApplication::exec()時啟動),直到exit()函數被呼叫。
從概念上來說,事件迴圈可以理解為一些while迴圈:
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事件,直到佇列為空。然後,處理Spontaneous事件(首先轉換為QEvents物件,然後傳送給QObjects範例)。最後,處理在Spontaneous事件處理過程中生成的Posted事件。事件迴圈不會對Sent事件進行處理,Sent事件會直接傳遞給目標物件。
事件迴圈首先會將事件傳遞給物件樹頂部的QObject範例,然後從根節點開始逐級向下傳遞事件,直到找到能夠處理該事件的接收者物件。這個過程被稱為事件傳遞(Event Propagation)。事件的處理通過呼叫QObject範例的event()函數來完成的。
在使用事件迴圈機制時,exec()用於開啟事件迴圈,exit()或quit()用於退出事件迴圈,值得注意的是,的quit()或exit()函數並不會立即退出事件迴圈,而是等待控制權返回事件迴圈後,才會真正退出事件迴圈,並返回exec()呼叫處,這裡有點費解,之後會通過一個例子說明。
更多關於Qt中事件系統的細節,除了參考Qt原始碼和官方檔案The Event System、QEvent Class,還推薦其他兩篇文章Another Look at Events、Qt原始碼閱讀-事件迴圈
這裡出現一個問題:次執行緒中建立的事件迴圈是否會處理Spontaneous事件?以下是GPT的回答,正確性未驗證僅供參考,歡迎各位大佬指點。
GPT:在其他執行緒中建立的事件迴圈通常不會處理自發事件,除非明確地要求。如果需要在其他執行緒中處理自發事件,您需要自行建立和管理事件迴圈,並顯式地設定相應的機制來觸發事件的處理。也就是說,如果直接呼叫QThread::exec()函數開啟事件迴圈,是不會處理Spontaneous事件的。
訊號槽(signal-slot)和事件處理是兩種不同的機制,都可以用於實現程式中不同物件之間的同步或非同步通訊。它們可以在Qt應用程式中一起使用,但它們在實現方式和應用範圍上有一些區別。
訊號槽機制:
事件和事件處理:
Qt中訊號槽的使用非常簡單,使用connect將兩個物件(必須是QObject的子類)的訊號和槽連線起來即可。值得注意的是,connect函數的第5個引數為列舉型別Qt::ConnectionType,用於指定連線型別。以下是列舉型別Qt::ConnectionType的值:
Qt::AutoConnection(預設值):Qt會根據訊號和槽的所線上程來自動選擇連線型別。如果訊號和槽在同一執行緒,將採用 Qt::DirectConnection,否則採用 Qt::QueuedConnection。
Qt::DirectConnection:訊號被髮射時,槽會直接在訊號發射的執行緒上呼叫,不涉及事件佇列。這通常在同一執行緒內的連線中使用,且是同步的。
Qt::QueuedConnection:訊號被髮射時,槽會被放入接收者物件的事件佇列中,等待事件迴圈處理。這用於在不同執行緒之間建立連線,因此是非同步的。這裡說明,訊號槽機制的佇列連線實現依賴事件迴圈機制。
Qt::BlockingQueuedConnection:類似於 Qt::QueuedConnection,但不返回到訊號發射者,阻塞直到槽函數完成執行。
Qt::UniqueConnection:如果已經存在一個具有相同引數的連線,將不會建立新連線,而是返回一個無效的連線。
通常,直接使用預設值Qt::AutoConnection,即可滿足大多數情況的需求,因為它會根據上下文自動選擇合適的連線型別。特殊情況下,也可以手動指定連線型別,比如,指定同一個執行緒中的兩個物件間為佇列連線,或指定不同兩個執行緒中的兩個物件為直接連線。
下面用一個例子,詳細解釋以上所有特性。
#include <QDebug>
#include <QCoreApplication>
#include <QTimer>
#include <QThread>
class Foo : public QObject {
Q_OBJECT
public:
Foo(QObject *parent = nullptr) : QObject(parent) {}
private:
void doStuff() {
qDebug() << QThread::currentThreadId() << ": Emit signal one";
emit signal1();
qDebug() << QThread::currentThreadId() << ": Emit signal finished";
emit finished();
qDebug() << QThread::currentThreadId() << ": Emit signal two";
emit signal2();
}
signals:
void signal1();
void finished();
void signal2();
public slots:
void slot1() {
qDebug() << QThread::currentThreadId() << ": Execute slot one";
}
void slot2() {
qDebug() << QThread::currentThreadId() << ": Execute slot two";
}
void start() {
doStuff();
qDebug() << QThread::currentThreadId() << ": Bye!";
}
};
#include "main.moc"
int main(int argc, char **argv) {
qDebug() << "main thread id:" << QThread::currentThreadId();
QCoreApplication app(argc, argv);
Foo foo;
Foo foo2;
QThread *foo2thread = new QThread(&app);
foo2.moveToThread(foo2thread);
foo2thread->start();
QObject::connect(&foo, &Foo::signal1, &foo, &Foo::slot1);
QObject::connect(&foo, &Foo::signal1, &foo2, &Foo::slot1);
QObject::connect(&foo, &Foo::finished, &app, &QCoreApplication::quit);
QObject::connect(&foo, &Foo::finished, foo2thread, &QThread::quit);
QObject::connect(&foo, &Foo::signal2, &foo, &Foo::slot2); // Qt::DirectConnection
QObject::connect(&foo, &Foo::signal2, &foo2, &Foo::slot2); // Qt::QueuedConnection
QTimer::singleShot(0, &foo, &Foo::start);
return app.exec();
}
以下是執行結果:
main thread id: 0x165c
0x165c : Emit signal one
0x165c : Execute slot one
0x165c : Emit signal finished
0x5578 : Execute slot one
0x165c : Emit signal two
0x165c : Execute slot two
0x165c : Bye!
在這段程式碼中,
第1步:建立了兩個Foo的範例foo和foo2,並將foo2移動到另一個執行緒foo2thread中。
第2步:將foo的兩個訊號分別連線到foo2兩個槽函數。此外,還將foo的finished()訊號,連線到app和foo2thread的quit函數上,以便在發出finished訊號時,通知主事件迴圈和foo2thread執行緒的事件迴圈退出。
第3步:將單次定時器連線到foo的start() 函數,準備進入主事件迴圈。
第4步:啟動並進入主事件迴圈。
當exec()函數被呼叫時,事件迴圈開始。發生的第一個事件是計時器在0毫秒後發出超時訊號。訊號timeout()連線到foo物件的start()槽函數。在輪詢任何其他事件之前,start()槽函數將被執行完成。這導致了該doStuff()方法發出signal1(). 連線到該訊號的槽slot1()將立即被執行。一旦控制返回到doStuff(),它就會發出第二個訊號finished()。該訊號連線到應用程式app和foo2thread執行緒的quit函數上,這是否意味著應用程式將立即退出?
答案是否定的。如前所述,QCoreApplication::quit()槽實際上呼叫QCoreApplication::exit(0),而分析後者的原始碼可以發現,其只是將事件迴圈的退出標誌設為true。在控制權返回到主事件迴圈之前,實際的退出不會發生。
因此,在發出訊號finished()之後,程式會繼續執行doStuff(),發出訊號signal2(),這裡注意,由於foo的signal2和slot2之間是直接連線,因此在發射signal2的同時,foo的slot2便阻塞執行了,而signal2和foo2的slot2之間是佇列連線,執行緒foo2thread的控制權已經回到了事件迴圈處,並已經退出事件迴圈。因此,foo2的slot2不會執行。
隨後,返回start()。在start()退出之前,列印「Bye!」。
最後,回到主事件迴圈。
由於這時主事件迴圈退出標誌設定為true,便會返回主函數中exec的呼叫處,隨之程式結束。
如果手動指定QObject::connect(&foo, &Foo::signal2, &foo, &Foo::slot2)的連線型別為Qt::QueuedConnection,最後得到的結果會有所不同,感興趣的讀者可以自己試一試。