1 開篇廢話
我喜歡用C++寫 GUI 框架,因為那種成就感是實實在在地能看到的。從畢業(yè)到現(xiàn)在寫了好多個了,都是實驗性質(zhì)的。什么拳腳飛刀毒暗器,激光核能反物質(zhì),不論是旁門左道的陰暗伎倆,還是名門正派的高明手段,只要是 C++ 里有的技術(shù)都試過了。這當中接觸過很多底層或是高級的技術(shù),像編譯時類型檢測,運行時代碼修改等等,按實現(xiàn)的不同 GUI 涉及的東西是沒有邊際的。從最開始模仿 MFC,ATL 那樣的實現(xiàn)學到很多東西,然后開始看一些開源的著名的 GUI 框架,像 MFC,WTL,SmartWin++,win32gui,jlib2 ,VCF 獲得很多啟發(fā),到現(xiàn)在似乎有一種已看盡天下 GUI 的感覺。在學習別人的框架和自己的實現(xiàn)過程中,真真實實地感覺自己成長了不少,也有很多感悟。
寫到這,我作為輪子制造愛好者,在這里向那些喊著"不要重復制造輪子的"批評家們承認錯誤。在有那么多好的輪子的情況下,我不值得浪費地球資源,浪費時間精力來自己手工重復打造。但是不值得歸不值得,在值得和喜歡之間我還是選擇后者。并且人生在世,什么才是值得?我覺得不是拯救人類,為世界和平做貢獻,也不是努力奮斗,為地球人民謀福利,而是簡單地做自己喜歡的事。
寫過的那些代碼很多都消失在硬盤的海洋里了,但那些挑燈苦想來的感悟還在。在它們也消失之前,我想利用空閑時間把這些覺得有點用處的經(jīng)驗寫出來,正好這個博客也已經(jīng)快一年沒更新了。另外也算是對那些發(fā)我郵件的朋友的回應(yīng)。
我的想法是用一系列日志,按照實現(xiàn)一個 GUI 框架的具體思維遞進過程來闡述實現(xiàn)一個 GUI 框架的具體思維遞進過程。這樣說好像有點遞歸,簡單地解釋就是這一系列日志不是想用《記憶碎片》那樣錯亂的敘述方式來說明一個多有意思的故事,而是盡量簡單自然地記錄一下寫 GUI 框架過程中我的思考。這個遞進過程也就是實現(xiàn)一個 GUI 框架的過程,一系列日志之后,我們將會看到一個長得漂亮眼,極富彈性,能干又節(jié)約的 GUI 框架。
雖然寫的內(nèi)容都是在 Windows 的 GUI 系統(tǒng)之上,但其原理是觸類旁通的,其它基于消息的 GUI 系統(tǒng)也都大同小異。所用的代碼也都是闡述原理的,自知絕對達不到商業(yè)巨作的水準,所以請不要一上來就批判,要知道我只是想分享而已。之所以先這樣說一下,是很害怕那種一上來就"怎么不跨平臺啊?","怎么都還看得到HWND啊?","怎么不能用成員函數(shù)處理消息啊?"的同志。不喜歡站在高處指著別人的天靈蓋說話的人。要知道車輪也是一步步造出來的,不要一開始就想載著MM在高速路上飆豪車像少年啦飛馳。
我認為寫技術(shù)博客有三種境界,一種是一直在那繪聲繪色地描述自己的魚有多可口多美味,讓讀者只能垂涎興嘆,一種是授人以魚的人,悶頭就擺出來各種生猛海鮮,讓讀者難以消化,還有一種境界是授人以漁,怎么釣魚怎么煮魚都細細地教給讀者。讀博客的人有兩個境界,一種是只吃魚的,一上來就只要代碼,一種是學打魚的,想知其然更想知其所以然。讀博客時我努力做學打魚的類型,自己寫博客時我會努力做到授人以漁的境界。
另外要說明的是,同樣作為塵世中的一個渺小個體,我大多數(shù)時候也是在為生存而奔波勞累著的。除此之外剩余的大多時候,更是要玩游戲,K歌,看電影,陪MM,吃喝玩樂。再剩余用來寫這個的時候不是很多,有可能這一系列日志一夜寫就,也有可能增刪五年披閱十載,孩子都叫爸了還沒完成。所以請大家不要對這個博客抱很大的期待,就當我是路邊街頭的表演,你打醬油經(jīng)過時偶爾瞟過來一眼就好了。
要說的廢話終于說完了,下面開始正題。
2 基本概念
基于消息的 GUI 框架的封裝,一切都圍繞消息展開。復雜的框架設(shè)計,明確了需求之后,第一步首先是劃分模塊。所以,要闡述一個設(shè)計過程,第一步也應(yīng)該是先說清最基本的概念和模塊劃分,而不是一上來就用廣義相對論把讀者全部放倒。GUI 框架是干什么的當然是地球人都知道的,但 GUI 框架沒有什么已經(jīng)劃分的標準概念,我是按照設(shè)計的需要來劃分的。如果把 GUI 框架看作一個單位,那么這個單位里最重要的角色有這幾個:
- 消息發(fā)送者(message sender)
- 消息監(jiān)聽者(message listener)
- 消息檢查者(message checker)
- 消息處理者(message handler)
- 消息分解者(message cracker)
- 消息映射者(message mapper)
下面分別說明。
2.1 消息發(fā)送者和消息(message sender,message)
消息發(fā)送者其實只是在這里友情客串一下,它不在框架設(shè)計之內(nèi),由操作系統(tǒng)扮演這個勞苦功高的角色,它的工作是將消息發(fā)送到消息監(jiān)聽者。在這里面隱含了一下最重要的角色,消息。其實剩余的所有角色說到底也只是死跑龍?zhí)椎模嬲I(lǐng)銜的是消息本身,比如窗口大小改變了的消息,按鈕被點擊了的消息等等,所有人都高舉旗幟緊密團結(jié)在它周圍進行工作。但消息本身只是一個很簡單的數(shù)據(jù)結(jié)構(gòu),因為再復雜的 GUI 系統(tǒng),它的消息也不過是幾個參數(shù),所以框架的實現(xiàn)重點在其它的角色。在此之前簡單地封裝一下消息,一個最簡單的封裝可能是這樣:
1: // 消息封裝類
2: class Message
3: {
4: public:
5: Message( UINT id_=0,WPARAM wparam_=0,LPARAM lparam_=0 )
6: :id( id_ )
7: ,wparam ( wparam_ )
8: ,lparam ( lparam_ )
9: ,result ( 0 )
10: {}
11:
12: UINT id;
13: WPARAM wparam;
14: LPARAM lparam;
15: LRESULT result;
16: };
就這樣的我們的公司已經(jīng)有了核心角色了。從概念上講,我們的這個基于消息的 GUI 框架已經(jīng)完成了 99% 。然后我們可以以它為中心,按功能劃分進行詳細討論,一步步完成那剩余的 1% 的極富創(chuàng)意和挑戰(zhàn)的工作。在此之前,先得簡單解釋一下這幾個角色都各是什么概念。消息傳送者如上所述,將不在討論范圍內(nèi)。
2.2 消息監(jiān)聽者(message listener)
消息監(jiān)聽者完成的工作是從操作系統(tǒng)接收到消息,消息是從這里真正到達了框架之內(nèi)。最簡單的消息監(jiān)聽者是一個提供給操作系統(tǒng)的回調(diào)函數(shù),比如在 Windows 平臺上這個函數(shù)的樣子是這樣:
1: //我是最質(zhì)樸的消息接收者
2: LRESULT CALLBACK windowProc( HWND window,UINT id,WPARAM wparam,LPARAM lparam );
一個好 GUI 框架當然不能赤祼祼地使用這個東西,我們要在此之上進行面向?qū)ο蟮姆庋b。消息監(jiān)聽者能想到的最自然的封裝模式是觀察者模式(Observer),這樣的模式下的監(jiān)聽者實現(xiàn)看起來像這個樣子:
1: //我是一個漂亮的觀察者模式的消息監(jiān)聽者
2: class MessageListener
3: {
4: public:
5: virtual LRESULT onMessage( Message* message ) = 0;
6: };
7:
8: //監(jiān)聽者這樣工作
9: MessageListener* listener;
10: window->addListener( listener );
11:
jlib2 和 VCF 的實現(xiàn)就是這種模式。但現(xiàn)實當中大多數(shù)框架沒有使用這種模式,比如 SmartWin++ 和 win32gui ,甚至沒有使用任何模式比如 MFC 和 WTL 。我想它們所以不采用觀察者模式,有些是因為框架整體實現(xiàn)的牽制,有的則可能是因為沒能解決某些技術(shù)問題。我們的 GUI 框架將實現(xiàn)觀察者模式的消息監(jiān)聽者,所以這些問題我們后面也會遇到,到時候再詳述。
2.3 消息檢查者(message checker)
消息檢查者完成的工作很簡單。當收到消息的時候,框架調(diào)用消息檢查者檢查這個消息是否符合某種條件,如果符合,則框架再調(diào)用消息處理者來處理這個消息,所以有點類似一個轉(zhuǎn)換者,輸入(消息),輸出一個(是/否)的值。最簡單的檢查者可能就是一個消息值的比較,比如:
1:
2: /最簡單的消息檢查者
3: essage.id == /*消息值*/
4:
5: /比如
6: essage.id == WM_CREATE
展開MFC 和 ATL 的消息映射宏,可以看到它們的消息檢查就是用堆積起來的消息值比較語句完成。這就是消息檢查者最原始最自然最簡單的實現(xiàn)方式,但這種方式缺陷太多。我們的框架將實現(xiàn)一個自動化,具有擴展性的消息檢查者,后文詳細討論。
2.4 消息處理者(message handler)
消息處理者是我們最終的目的。GUI 框架所做的一切努力都只是前期的準備,直到消息處理者運行起來那一刻,整個公司才算是真正地運轉(zhuǎn)起來了。消息處理者的具體實現(xiàn)可能是自由函數(shù),成員函數(shù)或者其它可調(diào)用體,甚至可以是外部腳本,處理完畢可能需要給操作系統(tǒng)返回一個結(jié)果。最簡單的消息處理者可以就是條語句,比如:
1: //消息處理
2: alert( "窗口創(chuàng)建成功了!" );
3:
4: //返回結(jié)果
5: message.result = TRUE;
上面代碼中"顯示消息框"的動作就是一個消息處理,以上兩行代碼可視為消息處理者。最常見的消息處理者是函數(shù),比如:
1: //消息處理
2: _handleCreated( message );
代碼中的函數(shù) _handleCreated 就是一個典型的消息處理者。消息處理者的實現(xiàn)難處在于,既要支持多樣性的調(diào)用接口,又要支持統(tǒng)一的處理方式。我們的框架將實現(xiàn)一個支持自由函數(shù),成員函數(shù),函數(shù)對象,或者其它可調(diào)用體的消息處理者,并且這些可調(diào)用體可以具有不同參數(shù)列表。后文將進行消息處理者的詳細討論。
在這里有必要再說明一下。一個判斷語句的大括號之前(判斷部分)是消息檢查的動作,大括號之內(nèi)(執(zhí)行部分)是實際的消息處理。因此一個判斷語句雖簡單,卻包含消息檢查者和消息處理者,以及另外一個神秘的部分(見后文),一共三個部分。代碼像這樣:
1: if ( //消息檢查者 )
2: {
3: //消息處理者
4: }
比如下面的代碼:
1: // message.id == WM_CREATE 是消息檢查者
2: // _handleCreated( message )是消息處理者
3:
4: if ( message.id == WM_CREATE )
5: {
6: _handleCreated( message );
7: }
8:
2.5 消息分解者(message cracker)
消息分解者是為消息處理者服務(wù)的。不同的消息處理者需要的信息肯定不一樣,比如一個繪制消息(WM_PAINT)的消息處理者可能需要的是一個圖形設(shè)備的上下文句柄(HDC),而一個按鈕點擊消息(BN_CLICK)的消息處理者則可能需要的是按鈕的ID,它們都不想看到一個赤祼祼的消息杵在那里。從消息中分解出消息攜帶的具體信息,這就是消息分解者的工作。最簡單的消息分解者可能是一個強制轉(zhuǎn)換,比如:
1: // WM_CREATE 消息參數(shù)分解
2: CREATESTRUCT* createStruct = (CREATESTRUCT*)message.lparam;
3:
4: // WM_SIZE 消息參數(shù)分解
5: long width = LOWORD( message.lparam );
6: long height = HIWORD( message.lparam );
上面的的代碼雖然簡單但 100% 完成了消息分解的任務(wù),所以它也是合格的消息分解者。我的框架將實現(xiàn)一個自動化,可擴展的消息分解者。后文將以此為目標進行詳細討論。
2.6 消息映射者(message mapper)
消息映射者是最直接與框架外部打交道的部分,顧名思義,它的工作就是負責將消息檢查者與消息處理者映射起來。最簡單的映射者可以是一條判斷語句,這個判斷語句,如代碼所示:
1: // if 語句的框架就是一個消息映射者
2:
3: // 消息映射者
4: if ( /*消息檢查者*/ )
5: {
6: /*消息處理者*/
7: }
1: // if 語句將消息檢查者 message.id==WM_CREATE 和消息處理者 _handleCreated(message) 聯(lián)系起來了
2: if ( message.id == WM_CREATE )
3: {
4: _handleCreated( message );
5: }
上面的代碼 的if 語句中,判斷的部分是消息檢查者,執(zhí)行的部分是消息處理者。if 語句把這兩個部分組成了一個映射,這是最簡單的消息映射者。到這里可以發(fā)現(xiàn),這個簡單的 if 語句有多不簡單。它低調(diào)謙遜但獨自地完成了很多工作,就像公司的小張既要寫程序,又要掃地倒茶,還義務(wù)地給女同事講笑話。MFC 和 WTL 的消息映射宏展開就是這樣的 if 語句。像 jlib2 那樣的框架,雖然處理者都虛函數(shù),但在底層也是用 if 語句判斷消息然后來進行調(diào)用的。當然還有華麗一點的消息映射者,像這樣:
1: // 華麗一點的消息映射者
2: window.onCreated( &_handledCreated );
這個 onCreated 也是一個消息映射者,在它的內(nèi)部把 WM_CREAE 消息和 _handleCreated 函數(shù)映射到一起,這種方式最有彈性,但實現(xiàn)起來也比宏和虛函數(shù)都要困難得多。SmarWin++ 就是使用的這種方式,它的消息映射者版本看起來一樣的陽光帥氣,但內(nèi)部實現(xiàn)有些細節(jié)稍嫌猥瑣。我們的 GUI 框架將實現(xiàn)一個看起來更美,用起來很爽的消息映射者像這個樣子:
1: // 將消息處理者列表清空,設(shè)置為某個處理者
2: // 可以這樣
3: window.onCreated = &_handleCreated;
4: // 或者這樣
5: window.onCreated.add( &_handleCreated );
6:
7: // 在消息處理者列表中添加一個處理者
8: // 可以這樣
9: window.onCreated += &_handleCreated;
10: // 或者這樣
11: window.onCreated.add( &_handleCreated );
12:
13: // 清空消息處理者列表
14: // 可以這樣
15: window.onCreated --;
16: // 或者這樣
17: window.onCreated.clear();
值得說一下,這種神奇的映射者是接近零成本的,它沒有數(shù)據(jù)成員沒有虛函數(shù)什么都沒有,就是一個簡單的空對象。就像傳說中的工作能力超強,但卻不拿工資,不泡公司MM,甚至午間盒飯也不要的理想職員。在后文當中會具體詳述這個消息映射者的實現(xiàn)。
3 結(jié)尾廢話
到目前為止我們的框架已經(jīng)完成了 99% 。下篇準備開始寫最簡單的消息檢查者,但說實話我也不知道下一篇什么時候開始。看看上一篇日志,竟然是一年前寫的,這一年內(nèi)發(fā)生的事情很多,但自己渾渾噩噩地的好像一眨眼就到了現(xiàn)在,看著 CPPBLOG 上的好多其它兄弟出的很多很有水準的東西,心里真是慚愧。昨天看了《2012》,現(xiàn)在心里還殘留有那種全世界在一間瞬間灰飛煙滅的震撼,2012年也不遠了,我也趕緊在地球毀滅之前加把油把這些日志寫完了吧。不管怎么樣,今天哥先走了,請不要迷戀哥。