閱讀227 返回首頁    go 阿裏雲 go 技術社區[雲棲]


在Qt中保持GUI響應[上]

Keeping the GUI Responsive

原文作者: Witold Wysota

譯者: Jason Lee @ https://blog.csdn.net/jasonblog

 

QtCentre 裏的人們經常提到一個反複出現的問題:長操作期間 GUI 界麵無響應。這個問題不難解決,並且有多種應對方案,因此我在這裏列出一些針對不同情況的可能的解決方案。

 

長操作

第一件事是列出這個問題域並且列出可以采取的解決方案。前麵涉及的這個問題可能以兩種形式出現。第一種情況是程序在執行計算密集型任務,也就是需要經過一係列操作才能得到最後的結果。比如快速傅裏葉變換。

另一種情況是程序必須在已經觸發的活動(比如從網絡上下載東西)結束後才能繼續算法的下一步。這種情況,就本身而言,通過使用 Qt 是比較容易避免的,因為大多數的異步操作在 Qt 中可以采用信號和槽的機製來實現,所以你可以將之連接到一個槽中來繼續算法的下一步。

在計算過程中(無論以何種方式使用信號和槽)所有的事件處理都會被暫停。因此, GUI 不會被刷新,用戶輸入不會被處理,網絡活動停止以及定時器不運作——程序看起來被凍結了並且,事實上,程序中不耗時的那部分也被凍結了。“長操作”是多長呢?任何使得程序無法即時響應用戶的事情都算長。一秒鍾是長,任何超過兩秒鍾的操作肯定太長了。

本文旨在保持程序功能,防止終端用戶被一個不響應的 GUI (以及網絡和定時器)所惹惱。為了做到這個目標首先需要看看可能的解決方案和問題的主因。

我們可以通過兩種方式來獲取計算型任務的結果——通過在主線程(單一線程方案)或者在單獨的線程(多線程解決方案)進行計算。後者較多地為人所知並且在 JAVA 中得到應用,但是有時候在使用單一線程會更好的情況下它也被濫用了。與主流觀點相反,線程通常使你的程序變慢而非變快,因此除非你確定你的程序可以通過使用多線程變得更優良,否則不要因為你會就隨處創建新線程。

該問題域可以看做是兩種情形組成的。我們不一定能夠將問題細化成更小的步驟、循環或者子問題(通常它不應該是一個整體)。如果問題可以細化,它們也不一定互相依賴。如果它們之間是獨立的,我們可以任意處理它們。否則,我們必須同步我們的任務。最壞的情況下,我們隻能一次處理一些並且隻有上一步結束了才能開始下一步。充分考慮這些因素,我們可以選擇不同的解決方案。

 

人工的事件處理

最基本的解決方案就是顯示地要求 Qt 在計算過程的某個點處理懸掛事件。為此,需要周期性地調用 QCoreApplication::processEvents() 。以下是示例:

for (int i = 3; i <= sqrt(x) && isPrime; i += 2) { label->setText(tr("Checking %1...").arg(i)); if (x % i == 0) isPrime = false; QCoreApplication::processEvents(); if (!pushButton->isChecked()) { label->setText(tr("Aborted")); return; } }

這種方法有明顯的缺點。比如,假設你想並發地執行兩個循環——調用其中一個會暫定另一個,直到該調用結束(所以不能在不同任務之間不能分配計算量)。它也造成了程序對事件處理的延遲。更重要的是代碼難以閱讀和分析,因此這種方案隻適合短而簡單的、在單一線程中處理的問題,比如閃屏或短期操作監控。

 

使用任務線程

另一種不同的解決方案就是在單獨的線程中執行長操作從而避免阻塞主事件循環。假如任務是由第三方庫通過阻塞方式執行,這是特別有效的。在這種情況下,它是不可能去打擾 GUI 處理懸掛事件的。

一種可以完全控製獨立線程的方式是使用 QThread 。可以繼承它並且重寫它的 run() 函數,或者調用 QThread:exec() 來啟動線程的事件循環,或者兩者共用:繼承然後必要的時候在 run() 函數裏調用 exec() 。可以通過信號和槽來連接主線程——隻要記住 QueuedConnection 會被使用到或者其它線程可能失去穩定性然後導致你的程序崩潰。

由於有很多多線程相關材料,所以這裏不列舉代碼示例了,而把注意力集中在其它地方。

 

在本地事件循環中等待

要介紹的下一種處理等待異步任務結束的方案已經完結。這裏,將要討論如何在一個網絡操作結束前阻塞程序流,並不阻塞事件處理。從本質上說,我們可以做的就是:

task.start(); while (!task.isFinished()) QCoreApplication::processEvents();

這叫做忙等待——頻繁判斷一個條件是否滿足。大多數情況下這是一個壞主意,它占用了全部 CPU 資源並且有著人工事件處理的所有缺點。

幸運的是, Qt 有一個類來幫助我們處理這項任務: QEventLoop 是程序和模態對話框在它們 exec() 調用裏使用的相同的類。每個該類的實例都連接到主事件分發機製,並且一旦它的 exec() 函數激活,它就開始處理事件直到使用 quit() 停止。

我們利用這種機製結合信號和槽使得異步操作轉化為同步操作——我們可以開啟一個本地事件循環然後通過特定對象的特定信號告知它結束退出:

QNetworkAccessManager manager; QEventLoop q; QTimer tT; tT.setSingleShot(true); connect(&tT, SIGNAL(timeout()), &q, SLOT(quit())); connect(&manager, SIGNAL(finished(QNetworkReply*)), &q, SLOT(quit())); QNetworkReply *reply = manager.get(QNetworkRequest( QUrl("https://www.qtcentre.org"))); tT.start(5000); // 5s timeout q.exec(); if(tT.isActive()){ // download complete tT.stop(); } else { // timeout }

我們使用一個網絡訪問管理類獲取遠程 URL 。由於它工作在異步方式,我們創建了一個本地事件循環來等待下載者的結束信號。此外,我們實例化一個定時器在五秒後來結束事件循環以免發生錯誤。通過連接合適的信號,提交請求和開始定時器,我們進入了新建的事件循環。對 exec() 的調用會在下載結束或者五秒結束後返回。我們通過判斷定時器是否還在運作來判斷是哪個原因引發結束。然後我們處理結果或者告知用戶下載失敗。

在這裏需要注意另外兩件事。一個通過 QxtSignalWaiter 類實現類似的解決方案是 libqxt 項目的一部分。另外一件事是,對於一些操作而言, Qt 提供了一係列的“等待”方法(比如 QIODevice::waitForBytesWritten() )或多或少地做些和上麵代碼片段一樣的事,但並沒有運行一個事件循環。然而,“等待”方案會凍結 GUI 因為它們沒有運行它們自己的時間循環。

 

 

最後更新:2017-04-02 05:21:05

  上一篇:go magento 1.4 -- 自定義變量(Custom Variables)使用初探
  下一篇:go CSDN廣州地區2010.05.30腐敗會策劃進行時,同時歡迎來自哈爾濱的yizia大版主