天天看點

QT-事件循環機制

作者:QT進階進階

QT事件循環了解

一. 常見問題

問題1:Qt中常見的事件有哪些?

答:滑鼠事件(QMouseEvent)、鍵盤事件(QKeyEvent)、繪制事件(QPaintEvent)、視窗尺寸改變(QResizeEvent)、滾動事件(QScrollEvent)、控件顯示(QShowEvent)、控件隐藏(QHideEvent)、定時器事件(QTimerEvent)等。

問題2:Qt是事件驅動的,這句話該怎麼了解呢?

Qt将系統産生的信号(軟體中斷)轉換成Qt事件,并且将事件封裝成類,所有的事件類都是由QEvent派生的,事件的産生和處理就是Qt程式的主軸,且伴随着整個程式的運作周期。是以我們說,Qt是事件驅動的。

問題3:Qt事件是由誰産生的?

答:Qt的官方手冊說,事件有兩個來源:程式外部和程式内部,多數情況下來自作業系統并且通過spontaneous()函數傳回true來獲知事件來自于程式外部,當spontaneous()傳回false時說明事件來自于程式内部,就像例程1建立一個事件并把它分發出去。

問題4:Qt事件是由誰接收的?

答:QObject!它是所有Qt類的基類!是Qt對象模型的核心!QObject類的三大核心功能其中之一就是:事件處理。QObject通過event()函數調用擷取事件。所有的需要處理事件的類都必須繼承自Qobject,可以通過重定義event()函數實作自定義事件處理或者将事件交給父類。

問題5:事件處理的流程是什麼樣的?

答:事件有别于信号的重要一點:事件是一個類對象具有特定的類型,事件多數情況下是被分發到一個隊列中(事件隊列),當隊列中有事件時就不停的将隊列中的事件發送給QObject對象,當隊列為空時就阻塞地等待事件,這個過程就是事件循環!

問題6:Qt 事件和信号的關系?

Qt的事件是windows的底層消息封裝而成的。這個消息和MFC裡的消息是同一概念,都是指鍵盤、滑鼠等的按壓、松開等消息。例如按下鍵盤後,windows系統會發出一個 WM_KEYDOWN的消息,Qt捕獲這個消息後,将其轉換成 Qt::Key_Down 事件。Qt的事件是較為底層的概念。先有事件,然後才有信号。即:消息 -> 事件 -> 信号。

總結:windows發出消息,Qt捕獲消息後轉換成事件,再由事件處理後發出信号。

【領更多QT學習資料,點選下方連結免費領取↓↓,先碼住不迷路~】

點選→Qt開發進階技術棧學習路線和資料

二.事件循環

那事件循環是什麼?   

在說明事件循環之前,先認識兩個術語:

可重入的(Reentrant):如果多個線程可以在同一時刻調用一個類的所有函數,并且保證每一次函數調用都引用一個唯一的資料,就稱這個類是可重入的大多數C++類都是可重入的。類似的,一個函數被稱為可重入的,如果該函數允許多個線程在同一時刻調用,而每一次的調用都隻能使用其獨有的資料。全局變量就不是函數獨有的資料,而是共享的。換句話說,這意味着類或者函數的使用者必須使用某種額外的機制(比如鎖)來控制對對象的執行個體或共享資料的序列化通路。  

線程安全(Thread-safe):如果多個線程可以在同一時刻調用一個類的所有函數,即使每一次函數調用都引用一個共享的資料,就說這個類是線程安全的。如果多個線程可以在同一時刻通路函數的共享資料,就稱這個函數是線程安全的。

進一步說,對于一個類,如果不同的執行個體可以被不同線程同時使用而不受影響,就說這個類是可重入的;如果這個類的所有成員函數都可以被不同線程同時調用而不受影響,即使這些調用針對同一個對象,那麼我們就說這個類是線程安全的。由此可以看出,線程安全的語義要強于可重入。接下來,我們從事件開始讨論。之前我們說過,Qt是事件驅動的。在 Qt 中,事件由一個普通對象表示(QEvent或其子類)。這是事件與信号的一個很大差別:事件總是由某一種類型的對象表示,針對某一個特殊的對象,而信号則沒有這種目标對象。所有QObject的子類都可以通過覆寫QObject::event()函數來控制事件的對象。

事件可以由程式生成,也可以在程式外部生成。例如:

QKeyEvent和QMouseEvent對象表示鍵盤或滑鼠的互動,通常由系統的視窗管理器産生;
QTimerEvent事件在定時器逾時時發送給一個QObject,定時器事件通常由作業系統發出;
QChildEvent在增加或删除子對象時發送給一個QObject,這是由 Qt 應用程式自己發出的。           

需要注意的是,與信号不同,事件并不是一産生就被分發。

事件産生之後被加入到一個隊列中(先進先出),該隊列即被稱為事件隊列。

事件分發器周遊事件隊列,如果發現事件隊列中有事件,那麼就把這個事件發送給它的目标對象。這個循環被稱作事件循環。

QCoreApplication::exec()開啟了這種循環,一直到QCoreApplication::exit()被調用才終止,是以說事件循環是伴随着Qt程式的整個運作周期!

三.深度了解事件循環

事件循環一般用exec()函數開啟。QApplicaion::exec()、QMessageBox::exec()都是事件循環。其中前者又被稱為主事件循環。事件循環首先是一個無限“循環”,程式在exec()裡面無限循環,能讓跟在exec()後面的代碼得不到運作機會,直至程式從exec()跳出。從exec()跳出時,事件循環即被終止。QEventLoop::quit()能夠終止事件循環。其次,之是以被稱為“事件”循環,是因為它能接收事件,并處理之。當事件太多而不能馬上處理完的時候,待處理事件被放在一個“隊列”裡,稱為“事件循環隊列”。當事件循環處理完一個事件後,就從“事件循環隊列”中取出下一個事件處理之。當事件循環隊列為空的時候,它和一個啥事也不做的永真循環有點類似,但是和永真循環不同的是,事件循環不會大量占用CPU資源。事件循環的本質就是以隊列的方式再次配置設定線程時間片。****

事件循環的僞代碼描述大緻如下所示:

while (is_active)
{
    while (!event_queue_is_empty) {
        dispatch_next_event();
    }
    wait_for_more_events();
}           

調用QCoreApplication::exec()函數意味着進入了主循環。我們把事件循環了解為一個無限循環,直到QCoreApplication::exit()或者QCoreApplication::quit()被調用,事件循環才真正退出。

僞代碼裡面的while會周遊整個事件隊列,發送從隊列中找到的事件;wait_for_more_events()函數則會阻塞事件循環,直到又有新的事件産生。我們仔細考慮這段代碼,在wait_for_more_events()函數所得到的新的事件都應該是由程式外部産生的。(因為所有内部事件都應該在事件隊列中處理完畢了。)

在類 UNIX 系統中,視窗管理器(比如X11)會通過套接字(UnixDomain或TCP/IP)向應用程式發出視窗活動的通知,因為用戶端就是通過這種機制
與X伺服器互動的。如果我們決定要實作基于内部的socketpair函數的跨線程事件的派發,那麼視窗的管理活動需要喚醒的是:
套接字 SOCKET、定時器 TIMER
這也正是select系統調用所做的:它監視視窗活動的一組描述符,如果在一定時間内沒有活動,它會發出逾時消息(這種逾時是可配置的)。Qt所要做
的,就是把select()的傳回值轉換成一個合适的QEvent子類的對象,然後将其放入事件隊列。
好了,現在你已經知道事件循環的内部機制了。           

是以,我們說事件循環在wait_for_more_events()函數進入休眠,并且可以被下面幾種情況喚醒:

視窗管理器的動作(鍵盤、滑鼠按鍵按下、與視窗互動等); 套接字動作(網絡傳來可讀的資料,或者是套接字非阻塞寫等); 定時器; 由其它線程發出的事件。

至于為什麼需要事件循環,我們可以簡單列出一個清單:

元件的繪制與互動:QWidget::paintEvent()會在發出QPaintEvent事件時被調用。該事件可以通過内部QWidget::update()調用或者視窗管理器 (例如顯示一個隐藏的視窗)發出。所有互動事件(鍵盤、滑鼠)也是類似的:這些事件都要求有一個事件循環才能發出。 定時器:長話短說,它們會在select或其他類似的調用逾時時被發出,是以你需要允許 Qt 通過傳回事件循環來實作這些調用。 網絡:所有低級網絡類(QTcpSocket、QUdpSocket以及QTcpServer等)都是異步的。當你調用read()函數時,它們僅僅傳回已可用的資料; 當你調用write()函數時,它們僅僅将寫入列入計劃清單稍後執行。隻有傳回事件循環的時候,真正的讀寫才會執行。注意,這些類也有同步函數 (以waitFor開頭的函數),但是它們并不推薦使用,就是因為它們會阻塞事件循環。進階的類,例如QNetworkAccessManager則根本不提供同步 API,是以必須要求事件循環。

有了事件循環,你就會想怎樣阻塞它。阻塞它的理由可能有很多,例如我就想讓QNetworkAccessManager同步執行。在解釋為什麼永遠不要阻塞事件循環之前,我們要了解究竟什麼是“阻塞”。假設我們有一個按鈕Button,這個按鈕在點選時會發出一個信号。這個信号會與一個Worker對象連接配接,這個Worker對象會執行很耗時的操作。當點選了按鈕之後,我們觀察從上到下的函數調用堆棧:

main(int, char **)
QApplication::exec()
[…]
QWidget::event(QEvent *)
Button::mousePressEvent(QMouseEvent *)
Button::clicked()
[…]
Worker::doWork()           

我們在main()函數開始事件循環,也就是常見的QApplication::exec()函數。視窗管理器偵測到滑鼠點選後,Qt 會發現并将其轉換成QMouseEvent事件,發送給元件的event()函數。這一過程是通過 QApplication::notify() 函數實作的。

注意:我們的按鈕并沒有覆寫event()函數,是以其父類的實作将被執行,也就是QWidget::event()函數。這個函數發現這個事件是一個滑鼠點選事件 ,于是調用了對應的事件處理函數,就是Button::mousePressEvent()函數。我們重寫了這個函數,發出Button::clicked()信号,而正是這個信 号會調用Worker::doWork()槽函數。

在worker努力工作的時候,事件循環在幹什麼?或許你已經猜到了答案:什麼都沒做!事件循環發出了滑鼠按下的事件,然後等着事件處理函數傳回。此時,它一直是阻塞的,直到Worker::doWork()函數結束。

注意,我們使用了“阻塞”一詞,也就是說,所謂阻塞事件循環,意思是沒有事件被派發處理。

在事件就此卡住時,元件也不會更新自身(因為QPaintEvent對象還在隊列中),也不會有其它什麼互動發生(還是同樣的原因),定時器也不會逾時并且網絡互動會越來越慢直到停止。也就是說,前面我們大費周折分析的各種依賴事件循環的活動都會停止。

這時候,需要視窗管理器會檢測到你的應用程式不再處理任何事件,于是告訴使用者你的程式失去響應。這就是為什麼我們需要快速地處理事件,并且盡可能快地傳回事件循環。

現在,重點來了:我們不可能避免業務邏輯中的耗時操作,那麼怎樣做才能既可以執行那些耗時的操作,又不會阻塞事件循環呢?一般會有三種解決方案:

【領更多QT學習資料,點選下方連結免費領取↓↓,先碼住不迷路~】

點選→Qt開發進階技術棧學習路線和資料

第一,我們将任務移到另外的線程中進行實作

第二,我們手動強制運作事件循環。想要強制運作事件循環,我們需要在耗時的任務中一遍遍地調用QCoreApplication::processEvents()函數。QCoreApplication::processEvents()函數會發出事件隊列中的所有事件,并且立即傳回到調用者。仔細想一下,我們在這裡所做的,就是模拟了一個事件循環。

另外一種解決方案:使用QEventLoop類重新進入新的事件循環。通過調用QEventLoop::exec()函數,我們重新進入新的事件循環,給QEventLoop::quit()槽函數發送信号則退出這個事件循環。舉一個網絡通信用到的例子來說:

QEventLoop eventLoop;
connect(netWorker, &NetWorker::finished,&eventLoop, &QEventLoop::quit);
QNetworkReply *reply = netWorker->get(url);
replyMap.insert(reply, FetchWeatherInfo);
eventLoop.exec();           

因為QNetworkReply沒有提供阻塞式API,并且要求有一個事件循環。我們通過一個局部的QEventLoop來達到這一目的:當網絡響應完成時,這個局部的事件循環也會退出。

四.QEventLoop 有關函數

1.一般我們的事件循環都是由exec()來開啟的,例如下面的例子:

QCoreApplicaton::exec()
 QApplication::exec()
 QDialog::exec()
 QThread::exec()
 QDrag::exec()
 QMenu::exec()           

這些都開啟了事件循環,事件循環首先是一個無限“循環”,程式在exec()裡面無限循環,能讓跟在exec()後面的代碼得不到運作機會,直至程式從exec()跳出。從exec()跳出時,事件循環即被終止。QEventLoop::quit()能夠終止事件循環。

事件循環實際上類似于一個事件隊列,對列入的事件依次的進行處理,當時間做完而時間循環沒有結束的時候,其實際上比較類似于一個不占用CPU事件的for(;;)循環。(其本質實際上是以隊列的方式來重新配置設定時間片。)

2.事件循環是可以嵌套的,當在子事件循環中的時候,父事件循環中的事件實際上處于中斷狀态,當子循環跳出exec之後才可以執行父循環中的事件。當然,這不代表在執行子循環的時候,類似父循環中的界面響應會被中斷,因為往往子循環中也會有父循環的大部分事件,執行QMessageBox::exec(),QEventLoop::exec()的時候,雖然這些exec()打斷了main()中的QApplication::exec(),但是由于GUI界面的響應已經被包含到子循環中了,是以GUI界面依然能夠得到響應。

通過“其它的入口”進入事件循環要特别小心:因為它會導緻遞歸調用

3.如果某個子事件循環仍然有效,但其父循環被強制跳出,此時父循環不會立即執行跳出,而是等待子事件循環跳出後,父循環才會跳出。 舉幾個例子吧,比如說如果想要将主線程等待100ms,總不能使用sleep吧,那樣會導緻GUI界面停止響應的,但是用事件循環就可以避免這一點:

QEventLoop loop;
 QTimer::singleShot(100, &loop, SLOT(quit())); // 在一個給定時間間隔msec(毫秒)之後調用一個槽
 loop.exec();           

還有,比如說對于一個槽函數,觸發之後會彈出一個dialog,但是像下面這樣寫的話,視窗會一閃而過的:

void ****::mySLot{
     QDialog dlg;
     dlg.show();
 }           

當然這裡可以使用将dlg改成一個靜态成員,通過增長期生存期的方法來解決這個問題,但是這裡同樣可以使用eventLoop來解決這個問題:

void ****::mySLot{
     QDialog dlg;
     dlg.show();
     QEventLoop loop;
     connect(&dlg, SIGNAL(finished(int)), &loop, SLOT(quit()));
     loop.exec(QEventLoop::ExcludeUserInputEvents);
 }           

繼續閱讀