從某種意義上來說,做圖形也好,做GUI也好,做編譯器也好,大概都是一種情結(jié)。其實只要稍微想一想就知道,能把它們?nèi)哂袡C統(tǒng)一起來的,就只有游戲。我很久以前的確是為了想開發(fā)游戲才對編程產(chǎn)生興趣的,而學習游戲開發(fā)占據(jù)了我前六年的時間。雖然現(xiàn)在不做了,不過偶爾總是會覺得手癢。但是做游戲沒美工做不好怎么辦呢?就只好寫游戲代碼了。但是沒有資源寫出來的游戲又不好玩,于是就只好寫庫。那寫什么庫呢,自然就只有渲染器、界面引擎和腳本引擎了。我的博客的大部分文章也是圍繞著這三件事情建立起來,而且在中間不斷切換的。
撒,所以今天就輪到GUI了。我一直很想做出一個自繪的GUI出來,無奈一直設(shè)計不出一個好的架構(gòu)。后來嘗試用原生API,但是卻又很不喜歡MFC的設(shè)計,就嘗試自己照著.NET Framework和Delphi那套VCL的樣子
封裝了一個控件庫出來。無奈原生API細節(jié)無敵多,后來沒做把所有的功能都全部做完。因此后來一段時間凡是需要界面我都直接用C#做。然后CEGUI出了,WPF和Silverlight也出了,我發(fā)現(xiàn)如今要做一個漂亮的GUI非自繪已經(jīng)做不到了,所以我又做了一次嘗試。
去年在美帝的時候曾經(jīng)試圖再設(shè)計一次,得到了一些結(jié)果。后來我發(fā)現(xiàn)其實根本沒辦法為GDI、DirectX、OpenGL和其它繪圖設(shè)備抽象一個公用的接口,否則就會遭遇大量性能問題。因為在很多細節(jié)上,譬如說渲染文字,為了達到較高的性能,OpenGL和DirectX需要使用幾乎相反的策略來做。因此這次我又換了一個方法,而且在
Vczh Library++ 3.0的Candidate目錄下已經(jīng)checkin了一個試驗品。
我把一個自繪的GUI分成了下面若干個層次。
1、NativeWindow。NativeWindow表示的是一個頂層窗口的實現(xiàn)。譬如說我們想用Windows的窗口作為自繪窗口的頂層窗口(游戲里面的很多頂層窗口是繪制在游戲窗口里面的,所以頂層窗口并不一定是Windows的窗口)。
2、控件庫。控件庫包含了這個自繪GUI庫的所有預定義控件。控件本身包含對用戶輸入的相應邏輯,但是每一個控件的繪制以及鼠標點中測試不在此范圍內(nèi)。
3、控件皮膚接口。每一個最終控件都會擁有一個控件皮膚接口。每當控件的狀態(tài)發(fā)生了變化,控件會調(diào)用皮膚接口更新控件的當前狀態(tài)。每當控件需要知道某一個點是否位于一個控件里面的時候,他也會去調(diào)用該控件的皮膚獲得結(jié)果。因此控件皮膚接口包含了一切關(guān)于繪制(因此理所當然也就包含了點中測試)的邏輯。
為了達到最高的性能,一套皮膚的實現(xiàn)只能綁定在某種繪圖設(shè)備上,也就是說缺省狀態(tài)下一套為GDI設(shè)備設(shè)計出來的皮膚是不能直接使用在DirectX設(shè)備上面的。當然我這個框架的設(shè)計也是足夠開放的,如果你非得用同一套代碼來實現(xiàn)不同繪圖設(shè)備上的皮膚,那么你是可以自己動手豐衣足食,做到給GDI和DirectX設(shè)計一個公共接口并插入我的GUI框架的(只不過這種做法一般情況下都會慘死)。
那么如何添加繪圖設(shè)備呢?目前NativeWindow有一個基于Windows窗口的實現(xiàn),并且NativeWindow的接口要求該實現(xiàn)在創(chuàng)建、銷毀、接收到很多窗口事件的時候都調(diào)用某一個回調(diào)對象。我們可以通過注冊一個全局回調(diào)對象或者具體窗口的回調(diào)對象來獲得NativeWindow狀態(tài)的變更。基于Windows窗口的NativeWindow實現(xiàn)還提供了一個額外函數(shù),可以讓你獲得一個NativeWindow的HWND(但這個函數(shù)并不被控件庫依賴)。現(xiàn)在我還實現(xiàn)了一個基于HWND+HDC的繪圖設(shè)備,主要方法就是先注冊全局回調(diào)對象,每當知道一個NativeWindow被創(chuàng)建了,我就會注冊一個NativeWindow的回調(diào)對象,用來維護一個窗口里面的一塊32位DIBSections位圖緩沖區(qū)。窗口的大小如果變化了,我也會在適當?shù)臅r候重新創(chuàng)建一塊合適的緩沖區(qū)。不過為了避免每一次大小變化都會創(chuàng)建新的緩沖區(qū),我創(chuàng)建的緩沖區(qū)的大小總會比窗口大一點。然后這個GDI繪圖設(shè)備就暴露了一個函數(shù),可以獲得一個NativeWindow的HDC和WinGDIElementEnvironment。
WinGDIElementEnvironment是基于HWND+HDC的這一套實現(xiàn)上專有的、為了GDI皮膚設(shè)計出來的一個公共的資源庫(譬如用來保存各種面向業(yè)務邏輯的pen啊brush什么的,比如說disable的時候什么顏色,選中的時候什么顏色等等)。如果你想設(shè)計一個基于HWND+DirectX的皮膚,那么類似WinGDIElementEnvironment的這套東西要重新做一次——因為為了達到相同的性能。具體細節(jié)相差太大。當然HWND+HDC上面可以有多套皮膚,WinGDIElementEnvironment是公用的。WinGDIElementEnvironment要求繪制是通過一個具體的WinGDIElement對象達到的,而一套皮膚可以有自己的一套WinGDIElement的實現(xiàn)。WinGDIElement被設(shè)計成面向業(yè)務的、一套皮膚的基本元素組成部分,譬如說按鈕邊框啦、焦點長方形啦、文字啦,而不是帶有pen和brush的長方形啊,文字啊,各種亂七八糟的最低等級的繪圖元素。舉一個例子,按鈕邊框跟菜單邊框很像,都可以用Rectangle來組成。但是Element里面就直接是按鈕邊框和菜單邊框,而不是一個可以讓你自由修改顏色的Rectangle。因為不同的控件要共享配色方案,而配色方案是由業(yè)務邏輯+空間狀態(tài)的集合實現(xiàn)的,因此WinGDIElement還是一個比較高層次的概念。當一個WinGDIElement被渲染的時候,他會給你一個HDC,然后你根據(jù)被設(shè)置的狀態(tài)來調(diào)用GDI函數(shù)繪制到HDC指向的32位DIBSections位圖緩沖區(qū)上面。
那么,當我們使用HWND+HDC的實現(xiàn),創(chuàng)建了一個布滿了控件的窗口,那實際上是發(fā)生了什么事情呢?首先控件自己會組成一棵樹。其次,控件的皮膚也會組成一棵樹。現(xiàn)在就有控件樹跟皮膚樹兩顆樹了。控件樹負責所有用戶輸入變更狀態(tài)的邏輯部分,而皮膚樹負責繪圖和點中測試。而一個HWND+HDC實現(xiàn)的皮膚樹,會在皮膚組合成樹的時候,在底下又組合出了一顆WinGDIElement樹。因此大局上就是:
控件樹(負責相應輸入變更狀態(tài))--> 皮膚樹(負責儲存控件狀態(tài)的可視部分并決定什么時候需要刷新)-->WinGDIElement樹(負責繪圖整個窗口)
這個時候,如果我們僅僅需要簡單的重新繪制窗口的話,那么控件樹跟皮膚樹都不需要被訪問到,底層僅需要讓WinGDIElement樹重新繪制一遍即可。而WinGDIElement的粒度實際上也不小,因此不會每一個圖元都有一個WinGDIElement從而使得創(chuàng)建出了一大堆對象的。
最后一個設(shè)計就是在什么時候才重繪窗口的問題。假設(shè)說我們現(xiàn)在收到了一個WM_KEYDOWN消息,最終傳播到了控件樹里面去,然后修改了10個控件上面的文字。每當你修改文字的時候?qū)嶋H上都需要重繪,那如何將無數(shù)次不可控的重繪合并成一次呢?SendMessage(WM_PAINT)是立刻執(zhí)行的,所以Windows自帶的合并WM_PAINT的方法在這個時候是無效的。我所采取的解決方法就是:反正控件樹的所有消息來源都是從NativeWindow里面來的,那實際上控件樹發(fā)出一個重繪請求的時候,我就會把NativeWindow的HWND實現(xiàn)里面的一個bool變量設(shè)成true,然后當NativeWindow每一個傳播到控件樹的消息結(jié)束傳播之后,才讀一次那個變量,如果是true,那么就調(diào)用WinGDIElement進行重繪并把變量設(shè)計成false。壞處是每一個傳播到控件樹的消息在處理完之后都必須檢查是否需要重繪,好處是這個東西被封裝在了NativeWindow的HWND實現(xiàn)里面里面,無論是控件樹、皮膚樹還是WinGDIElement樹也好,都在也不需要關(guān)心繪圖時機的事情了。
因為GUI被分割成了很多層,而且每一層的都關(guān)心業(yè)務邏輯的不同部分,所以他們都是可以被替換的。譬如說我們可以做成:
HWND+HDC實現(xiàn):最普通的方法
HWND+DirectX:WPF和Silverlight地方法
單一HWND+多個虛擬窗口+DirectX:可以在游戲里面用
無論下面的繪圖設(shè)備和窗口實現(xiàn)如何發(fā)生變化,GUI控件的邏輯部分都跟這些實現(xiàn)嚴格分離,因此不會受到影響。而且大部分情況下,我們是不需要擁有一個跨繪圖設(shè)備的皮膚庫的,譬如說游戲和應用程序,外表總不能做成一樣的。對于那些需要同時在DirectX和OpenGL上面運行的程序(譬如說3dsmax),它已經(jīng)有DirectX和OpenGL的公共接口了,因此這些軟件可以利用它們的公共接口來實現(xiàn)GUI的繪圖設(shè)備部分,從而在上面構(gòu)造起來的皮膚自然是可以跨DirectX和OpenGL的。
這比起一年前作的GUI實現(xiàn)又進了一大步。上一次的GUI嘗試為不同的繪圖設(shè)備抽象一套公共接口,后來慘死。不知道這次實際上做出來的效果如何,拭目以待吧。
posted on 2011-04-29 19:50
陳梓瀚(vczh) 閱讀(5625)
評論(13) 編輯 收藏 引用 所屬分類:
2D