2019年3月26日

從Qt設計看視窗系統設計

GUI,算是賈伯斯時代最屌的發明之一。
目前所有的GUI設計,從Windows、UNIX、Linux...等,幾乎都遵循了類似的設計,
這個設計,就我的理解,應該是賈伯斯在Mac上發明的,直到現在,它幾乎沒有太大的變化。


Qt的GUI設計:


在Qt的設計中,Qt會在背景維護一個Event Queue,Qt Event包括了所有的UI動作,
像是滑鼠移動、滑鼠點擊、視窗畫面更新、鍵盤輸入...等,
Qt最主要的工作,就是不斷的檢查Event Queue,有任何Event,就立刻執行它。
我們所有的UI操作,在Qt中就是將操作的「動作」,建立成Qt Event,送入Event Queue中,
Qt就會在檢查Event Queue時,按操作去執行。
以上圖為例,我們的動作很簡單,「移動到視窗的輸入欄,滑鼠點一下,輸入abcd」。
轉換成Qt內部行為,就會是產生下面9個Qt Event:
  1. 滑鼠移動
  2. 畫面更新
  3. 滑鼠移動
  4. 畫面更新
  5. 滑鼠左鍵點擊
  6. 鍵盤輸入a
  7. 鍵盤輸入b
  8. 鍵盤輸入c
  9. 鍵盤輸入d


Qt就會在背景檢查,陸續將這些動作完成。

Qt的GUI設計與程式碼的關聯:
當我們透過Qt開發工具(Qt Creator)建立出一個最基本的視窗程式時,它的主程式如下:
#include "widget.h"
#include


int main(int argc, char *argv[])
{
  QApplication a(argc, argv);
  Widget w;
  w.show();


  return a.exec();
}


一開始看很陌生,長得很奇怪,沒關係,這裡解釋後就一目了然了。


我們這裡要描述的主角叫做QApplication,也就是:
QApplication a(argc, argv);
a.exec();


相信大家都有個疑問,a.exec()後,Qt在幹嘛?
這樣的語法不通阿,程式無法運作吧?
我們的程式要寫在哪裡?
這就是我要帶領大家了解的,我們回到前面「Qt的GUI設計」,裡面曾經提到:
Qt會在背景維護一個Event Queue;Qt最主要的工作,就是不斷的檢查Event Queue,
有任何Event,就立刻執行它。


實際上,在Qt中是「誰」不斷的檢查Event Queue並執行Queue裡面的Event?
它就是QApplication。


套入後就很明白了,Qt主程式的最主要工作,就是產生一個QApplication,
然後把QApplication執行起來,它內部就是不斷的檢查Event Queue,
當有任何動作,QApplication就會按動作執行它。
因此,Qt講物件導向,所有UI元件,都是物件,
所有行為,實際上在內部都轉化成Event送入Event Queue,交給QApplication執行,
我們的程式碼在哪?
我們的程式實際上在Qt的其他物件中,要動作,就透過Qt提供的method,
它就會幫我們產生出Event給QApplication執行。

Qt與視窗系統設計:
前面我們描述了QApplication、Qt的Event在和整個視窗的行為,這裡帶出2個問題:
  1. 這是Qt獨有的設計嗎?
  2. 這個設計背後的實作是什麼?


這是Qt獨有的設計嗎?
這並不是Qt獨有的設計,這裡看Java的UI - SWT,SWT的最基本程式碼如下:
參考:Example
Display display = new Display();
Shell shell = new Shell(display);
shell.open();

// run the event loop as long as the window is open
while (!shell.isDisposed()) {
   // read the next OS event queue and transfer it to a SWT event
 if (!display.readAndDispatch())
  {
 // if there are currently no other OS event to process
 // sleep until the next OS event is available
   display.sleep();
  }
}

// disposes all associated windows and their components
display.dispose();
注意到Shell,看到while(!shell.isDisposed()),看到它的註解。


在Java的SWT中,它也是同樣的設計,只是Qt中的QApplication換成了Shell,原理是相同的。


在Windows中,我們可以直接參考下面這篇文章:


這篇文章是800萬年前的老文章,重點是它描述了Windows UI的內部機制 - Message Loop,
看完後你會發現,跟上述描述的根本如出一轍,
原來,全世界的GUI設計,根本都一模一樣,這設計哪裡來的?
我的了解是,這個設計應該就是來自於賈伯斯的Mac,
至於是不是賈伯斯本人設計或獨創的,我不清楚,但這設計很屌,
而且延續至今,沒意外的話,應該會繼續存在下去,直到GUI或電腦消失為止。



這個設計背後的實作是什麼?
回想看看,以前的程式有所謂的「物件導向」嗎?
再想想,GUI這樣的設計,所有視窗都是獨立的元件,所有視窗動作,都會產生Event,
這...不就是物件導向嗎?
它讓我們將所有視窗都以「物件」的方式思考和設計,所有的「視窗動作」都是「物件行為」,
這應該就很清楚明白了。
但它背後有什麼實作?
我們從寫程式的「物件」發想,再想想前面描述的Event Queue設計,我們就會得出一個有趣的結論:
電腦本身不存在「物件」的概念,它還是一個「程序」的操作邏輯,
賈伯斯將GUI以「物件」設計,讓所有UI程式設計師都使用「物件導向」來寫視窗程式,
可是電腦不認識物件怎麼辦?
所以回頭看看Event Queue的設計,它的本質實際上是將立體變成平面,
把所有物件導向的動作,統統轉化成電腦認識的程序操作。
看看滑鼠、鍵盤、視窗的所有動作,它們會來自於各個位置、各種沒有關聯的行為,
但最終都會進入統一的入口 - Event Queue,並由統一的執行者QApplication執行。


這種將立體轉換成平面,或者平面轉化成立體的設計,在UI中很常見,
例如多個頁面怎麼串在一起輪詢?多個輸入欄怎麼轉成一個陣列的字串?
一組陣列字串怎麼放置到樹狀結構中...等。

GUI設計的限制 - 多執行緒處理:
前面講到了GUI的設計都是透過統一的Event Queue和單一的執行者QApplication執行,
這造成了一個很大的問題,多執行緒在UI下如何處理?
我猜想,這問題產生的原因,可能和設計當時多執行緒不發達有關聯,
但不論如何,這在現在就是個問題。
看看下圖情境:


假設我們一個UI程式中,有3個Frame,每個Frame都是獨立的執行緒複製檔案,
檔案複製過程中會各自更新畫面,請問,它的畫面更新怎麼處理?
在目前的GUI設計中,無法處理...........


因為目前的GUI設計,所有的畫面更新、動作處理,
都必須透過單一的Event Queue和單一的QApplication來動作,
多執行緒的UI畫面更新,違背了由QApplication統一處理UI的設計,所以無法動作,
這樣的設計,往往會造成UI畫面不動,或者動作異常的問題、印出錯誤訊息...等。


變通的辦法是,各個Frame內「複製檔案的動作是獨立執行緒」,
複製檔案過程中,會各自送出Event到Event Queue,
QApplication就能在收到這些Event後,更新每個Frame上的複製進度。
它是多執行緒嗎?
不完全是,它只有核心動作是多執行緒,UI還是維持單一執行緒的設計。

GUI設計的限制 - sleep:
sleep是很常見的設計,電腦跑太快,某些時候還是需要讓它等一下,
但同樣的,這在GUI設計中也會造成困擾。
sleep,會讓CPU真的停下來,多半是執行nop的無限迴圈,直到時間到為止。
還記得QApplication吧,它負責所有Event的執行和所有畫面的更新,
我們的所有程式,實際上都是包裹在Event當中,由QApplication執行的。
現在,我們寫個Qt視窗程式,讓電腦在按下按鈕後sleep 30秒,會發生什麼事情?
會發現,視窗卡住了.....


原因很簡單,也有點白爛,因為QApplication執行按下按鈕的method,裡面要sleep 30秒,
QApplication就等30秒,過程中不做事情,就是等,因此畫面不更新、滑鼠不動作。


變通的辦法是,在Qt的UI中,不能真的用sleep,會造成UI卡死。要透過Qt的計時器,
實作一個類似sleep的功能,過程中使用者不能輸入,
但QApplication實際上跑去執行其他Event,
等到計時器時間到之後,才回來繼續剛剛的程式。


結尾:

Qt的設計不算差,但很多方面受限在GUI設計上,
不了解,寫Qt程式就會撞牆,這邊提出的2個限制,
就是我曾經撞過牆的地方,只有這2個嗎?
當然不是,但這2個印象最深刻,因為都很糟糕,很難處理。

沒有留言: