在處理QT循環事件的時候遇到了問題,查了半天資料都沒弄明白問題出在哪,后來找大牛同事問了一下,同事就給我寫了QCoreApplication::processEvent()這個函數,好啦,終於搞定了,這里小記一下,以免以后遇到。
於是乎這里認真仔細的看了一下Qt的事件和事件循環。(引用了碎炎的博客)
事件和事件循環
作為一個事件驅動的工具包,事件和事件傳遞扮演者Qt架構中的中心角色。在本文中我們不會給出一個對這個話題的全面的概述,我們將着眼於一些線程相關的概念。
事件能被程序的內部和外部產生,舉個例子:
QKeyEvent和QMouseEvent對象代表了一個鍵盤和鼠標的事件,它們從窗口由用戶的操作而產生。
QTimerEvent對象是當某個時間被激發時投入,它們都由操作系統產生。
QChildEvent對象是當一個子窗口被添加或者移除時候被送入QObject的,它們的源頭是Qt程序自己。
事件的重點是它們被產生的時候不會被傳遞;它們會先進入事件隊列,某刻會被傳遞。傳送者自己循環訪問事件隊列並把事件傳遞給目標QObject對象,因此稱作事件循環。概念上說,時間循環就像這個:
1. while (is_active)
2. {
3. while (!event_queue_is_empty)
4. dispatch_next_event();
5.
6. wait_for_more_events();
7. }
我們通過運行QCoreApplication::exec()來進入消息循環,這個循環直到exit()或者quit()被調用時才會被堵塞,然后退出。
這個”wait_for_more_events()”函數處於堵塞狀態,直到有新的事件被產生了。假如我們考慮它,所有在此刻可能產生的事件是外部源頭的。因此,這個消息循環能被以下情況喚醒:
窗口管理活動(鼠標按鍵操作等);
套接字事件;
定時器事件;
其它線程中投遞的事件、
在Unix-like系統中,窗口管理器通過套接字來通知應用程序,即使客戶端使用它們來與x server通訊。如果我們決定用內部的套接字對去實現跨線程的事件投遞,所有剩下的喚醒條件如下:
套接字;
定時器;
這個就是select(2)系統調用所做的;它監視着一系列的一系列的活動者的描述符,如果它們在一定的時間內沒有特定的活動,它們就超時了。
一個運行着的事件循環需要什么?
這個不是完整的列表,但是如果你有整體畫面,你將能夠去猜測什么類需要一個運行着的事件循環。
Widgets的繪畫和互動:QWidget::paintEvent()將在傳遞QPaintEvent對象時候被調用,這個對象將會在調用QWidget::update()或者窗口管理器的時候產生:響應的事件將需要一個時間循環去分發。
Timers:長話短說,它們在當select(2)或者超時的時候產生,因此它們需要讓Qt在返回時間循環的時候作這些調用。
Networking“所有的底層Qt網絡類 (QTcpSocket,QUdpSocket,QTcpServer等)都是異步設計的。當你調用ready(),它們只是返回已經可用的數據,當你調 用write(),它們只是將這個操作放入隊列,適時會寫入。只有當你返回消息循環的時候真實的讀取,寫入才會執行。注意它們確實提供了同步的方法,但是 它們的用法是不被提倡的,因為它們會堵塞事件循環。高級類,比如QNetworkAccessManager,簡單的不提供同步API,需要一個事件循 環。
堵塞事件循環
在我們討論為什么你應該從不堵塞消息循環之前,我們試着分析堵塞的含義。假象你有一個按鈕,它將會在它被點擊的時候發出clicked信號;在我們的對象中連接着一個槽函數,當你點擊了那個按鈕后,棧追蹤將會像這樣:
1. main(int, char **)
2. QApplication::exec()
3. […]
4. QWidget::event(QEvent *)
5. Button::mousePressEvent(QMouseEvent *)
6. Button::clicked()
7. […]
8. Worker::doWork()
在main函數中我們啟動了時間循環,平常的調用了 exex(),窗口管理器給我們發送了一個鼠標點擊事件,它被Qt內核取走,轉換成QMouseEvent並被送往我們widget的event()方 法,該方法被QApplication::notify()發送。因為按鈕沒有重寫event(),基類方法將被調用,QWidget::event() 檢測到了這個事件確實是一個鼠標點擊事件,然后調用特定的事件處理函數,那就是Button::mousePressEvent(),我們重寫這個方法去 發送clicked()信號,這將會調用被連接的槽函數。
當該對象處理量很大,那么消息循環在作什么?我們應該猜測它:什么都不做!它分發鼠標按下事件,然后就堵塞着等待着事件處理函數返回。這個就是堵塞了時間循環,它意味着沒有消息被分發了,知道我們從槽函數返回了,然后繼續處理掛起的消息。
在消息循環被卡住的情況下,widgets將不能更新它們 自身,不可能有更多的互動,timers將不會被激發,網絡通訊將緩慢下來,或者停止。進一步的說,許多窗口管理器將檢測到你的應用程序不在處理事件了, 然后告訴用戶你的程序沒有響應。這就是為什么快速的對事件響應並且即時返回到事件循環是多么的重要!
強制事件分發
所以,假如我們有一個很長的任務去運行但是又不希望堵塞這 個消息循環,該怎么做呢?一個可能的回答是將這個任務移到另一個線程中,在下一個張潔我們將看到這是如何做的。我們也能手動強制事件循環去運行,這個方法 是通過在堵塞的任務函數中調用QCoreApplication::processEvent()來實現 的,QCoreApplication::processEvent()將處理所有在消息隊列中的消息並返回給調用者。
另一個可選的選項是我們能夠強制重入事件循環的對象,就是QEventLoop類。通過調用QEventLoop::exec()我們將重入事件循環,然后我們能將槽函數QVentLoop::quit()連接到信號上去使它退出。舉個例子:
1. QNetworkAccessManager qnam;
2. QNetworkReply *reply = qnam.get(QNetworkRequest(QUrl(...)));
3. QEventLoop loop;
4. QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
5. loop.exec();
6. /* reply has finished, use it */
QNetworkReply不提供堵塞的API,它要求一個在運行的事件循環。我們進入了一個本地的事件循環,然后當回復完成時候,這個本地的循環退出了。
要特別小心的是在其它路徑下重入事件循環:它可能導致不希望的遞歸!讓我們回到前面看看按鈕的例子。假如我們在槽函數中調用了QCoreApplication::processEvent(),當用戶點擊了這個按鈕,這個槽函數將被再次調用:
1. main(int, char **)
2. QApplication::exec()
3. […]
4. QWidget::event(QEvent *)
5. Button::mousePressEvent(QMouseEvent *)
6. Button::clicked()
7. […]
8. Worker::doWork() // first, inner invocation
9. QCoreApplication::processEvents() // we manually dispatch events and…
10. […]
11. QWidget::event(QEvent * ) // another mouse click is sent to the Button…
12. Button::mousePressEvent(QMouseEvent *)
13. Button::clicked() // which emits clicked() again…
14. […]
15. Worker::doWork() // DANG! we’ve recursed into our slot.
一個快速並簡便的變通方法是把QEventLoop::ExcludeUserInputEvent傳遞給QCoreApplication::processEvents(),這會告訴消息循環不要再次分發任何用戶的輸入事件。
幸運的是,這個相同的事情不會在檢測事件中發生。事實上,它們被Qt通過特殊的方法處理了,只有當運行的時間循環有了一個比deleteLater被調用后更小的”nesting”值才會被處理:
將不會使object成為一個懸空指針。相同的東西被應用 到了本地的事件循環中。唯一的一個顯著區別我已經發現了,它在假如當沒有事件循環在運行的時候deleteLater被調用了的條件下,然后第一個消息循 環進入了后會取走這個事件,然后刪除這個object。這是相當合理的,因為Qt不知道任何外部的循環將最終影響這個檢測,因此馬上刪除了這個 object。