轉(zhuǎn)帖請注明出處 http://www.shnenglu.com/cexer/archive/2008/08/06/58169.html
我看過一些開源 GUI 框架的源代碼,包括聲句顯赫的 WTL,win32gui 和 SmartWin,還有一些不知名但很優(yōu)秀的,包括 jlib2( java AWT 在 C++ 上的移植 ),F(xiàn)LTK (比較小跨平臺),甚至還曾鼓起勇氣去看過 QT 那 n 萬行的代碼(當然沒看明白)。
其中我個人覺得最好的并不是 win32gui 或者 SmartWin,WTL 這些“名”庫,而是那個名不見經(jīng)傳的 AWT c++ 移植版本的 jlib2--AWT 的設(shè)計相當?shù)貎?yōu)雅。SmartWin 則多少有點名不附實,里面的尖括號(模板)多到讓人難以忍受的程度,實際上很多是不需要的,我在自己的框架當中只用了少量的模板就完成了 SmartWin 要完成的一部分功能。win32gui 是標準庫風格的代碼風格,看起來舒服,可是那接口也太古怪。
我想 SmartWin 和 win32gui,WTL 這些庫那么出名,更多地是因為它們將一些非常有創(chuàng)意的手法運用到了 GUI 框架當中,想必以后的 GUI 框架都會多少受其影響。
看這些庫都是為了“師夷長技“,因為我自己非常喜歡寫 GUI 框架,沒完沒了反反復(fù)復(fù)地寫。
CPPBLOG 上關(guān)于 GUI 框架的東西不是很多,自己寫框架的少之又少。GUI 框架是如此大而復(fù)雜的一個車輪,除了少數(shù)像我這樣的偏執(zhí)狂,誰會愿意累得七竅流血去制作一個可能一開動就會車毀人亡的車輪呢?
寫大一些的框架,是一種鍛煉同時也是挑戰(zhàn)。GUI 框架更是如此,要寫一個好的 GUI 框架,就必須深入到很多的領(lǐng)域 ,比如線程,內(nèi)存管理,內(nèi)核對象,圖形處理等等,這些都是在程序設(shè)計當中不容易駕馭的東西,如果還要完成諸如 序列化,腳本接口 這些東西,GUI 框架涉及的領(lǐng)域可以說非常地深廣了。在與這些東西打交道的過程當中,我覺得自己慢慢地成長了不少。
不過寫了數(shù)個版本,從來沒有自己覺得滿意的,想起以前的一個很牛的朋友說過:如果一個人看到自己很久以前寫的代碼仍然覺得滿意,那么這個人要么很牛,要么很蹉,不過可以想像,大多有這樣感覺的人都屬于很蹉的那一部分,可見自己覺得不滿意并不是壞事。一直寫,就能一直進步,總有一天會以牛人的身分回頭看看自己的代碼,而仍能覺得很滿意。
我寫日志的一大問題就是,每次都是廢話說完一大堆,仍不知道怎么自然地進入正題,這一次讓我要以強硬的態(tài)度開門見山地進入。
進入正題:
介紹一下自己在寫的一個 GUI 框架。
這個框架是以前幾個版本的進化版,它的消息處理機制比較有意思。下面是這個框架的消息映射部分的一個測試代碼,從這個代碼里可以看出來這個框架在消息處理上的一些創(chuàng)新。
1: // cexer
2: #include "../../cexer/include/GUI/GUI.h"
3: #include "../../cexer/include/GUI/window.h"
4: #include "../../cexer/include/GUI/message.h"
5:
6: using namespace cexer;
7: using namespace cexer::gui;
8:
9: // cexer LIB
10: #include "../cexerLIB.h"
11:
12: // c++ std
13: #include <iostream>
14: using namespace std;
15:
16:
17: class TestWindow:public Window
18: {
19: CEXER_GUI_CLASS( TestWindow,Window );
20:
21: public:
22:
23: TestWindow( ParentWidget* parent=NULL,LPCTSTR name=_T("") )
24: :Window( parent,name )
25: {
26:
27: }
28:
29: LRESULT onMessage( WidgetMessage<WM_DESTROY>& message )
30: {
31: ::PostQuitMessage(0);
32: wcout<<L"window destroyed"<<endl;
33: return message.handled(this);
34: }
35:
36: LRESULT onMessage( WidgetMessage<WM_CREATE>& message )
37: {
38: wcout<<L"window created"<<endl;
39: return message.handled(this);
40: }
41:
42: LRESULT onMessage( WidgetMessage<WM_SIZE>& message )
43: {
44: long clientWidth = message.cx;
45: long clientHeight = message.cy;
46:
47: wcout<<L"window sized"<<endl;
48: wcout<<L"client width is: "<<clientWidth<<endl;
49: wcout<<L"client height is: "<<clientHeight<<endl;
50:
51: return message.handled(this);
52: }
53:
54: LRESULT onCommand( SystemCommand<SC_CLOSE>& command )
55: {
56: wcout<<L"system command SC_CLOSE"<<endl;
57: return command.handled(this,0,false);
58: }
59:
60: LRESULT onCommand( SystemCommand<SC_MAXIMIZE>& command )
61: {
62: wcout<<L"system command SC_MAXMIZE"<<endl;
63: return command.handled(this);
64: }
65: };
66:
67:
68: int _tmain( int argc,TCHAR** argv )
69: {
70: CEXER_GUI_THREAD();
71:
72: wcout.imbue( std::locale("") );
73:
74: TestWindow* window = new TestWindow();
75: if ( !window->create() )
76: {
77: wcout<<L"window creating failed"<<endl;
78: delete window;
79: }
80:
81: TestWindow child( window,_T("child") );
82: child.create();
83:
84:
85: return runGUIthread();
86: }
消息自動映射
不像 MFC,ATL 那樣的 BEGIN_MESSAGE_MAP ,MESSAGE_HANDLER 之類的一大堆宏來湊成一個巨大函數(shù),也沒有像 QT 那樣的 connect(xxxx,xxxx) 的顯示連接信號和槽的代碼,你在上面的測試代碼里找不到任何的映射痕跡,但是它的確工作得非常好。在這個 GUI 框架當中,你只需要定義消息處理函數(shù),框架就能自動地幫你映射。例如上面定義的函數(shù):
1: LRESULT onMessage( WidgetMessage<WM_DESTROY>& message )
2: {
3: ::PostQuitMessage(0);
4: wcout<<L"window destroyed"<<endl;
5: return message.handled(this);
6: }
框架在遇到 WM_DESTROY 消息的時候,會自動地調(diào)用這個函數(shù)。
框架當中的 WidgetMessageBase 和 WidgetCommandBase ,SystemCommandBase 等模板以基類的形式,給派生類提供消息自動映射的能力。WidgetMessage,WidgetCommand,SystemCommand 等模板則提供了所有消息的一般實現(xiàn)。利用這些模板,只需要很簡單的步驟即能完成消息映射和處理。
比如說要實現(xiàn)對消息 WM_CREATE 的處理,只需定義一個函數(shù):
1: LRESULT onMessage( WidgetMessage<WM_CREATE>& message )
2: {
3: // do something
4: // ...
5:
6: return message.handled(this);
7: }
要實現(xiàn)對 ID 為 IDOK 的按鈕的點擊事件的處理,只需定義一個函數(shù):
1: LRESULT onCommand( WidgetCommand<IDOK,BN_CLICKED>& okClicked )
2: {
3: // do something here
4: // ...
5:
6: return clicked.handled(this);
7: }
要實現(xiàn)系統(tǒng)菜單的關(guān)閉命令的處理,只需要定義一個函數(shù):
1: LRESULT onCommand( SystemCommand<SC_CLOSE>& command )
2: {
3: // do something
4: // ...
5:
6: return command.handled(this);
7: }
對于命令消息( WM_COMMAND),可以進一步地利用派生進行命令消息的分類處理,例如可以定義一個針對所有按鈕點擊事件的消息模板:
1: template<UINT t_id>
2: struct ButtonClicked:public WidgetCommandBase<ButtonClicked<t_id,BN_CLICKED>,t_id,BN_CLICKED>
3: {
4: Button* button;
5:
6: template<typename TWidget>
7: ButtonClicked( TWidget* widget,UINT mess,WPARAM wpar,LPARAM lpar )
8: :_BaseCommand( mess,wpar,lpar )
9: ,button( widget->child(reinterpret_cast<HWND>(lpar)) )
10: {
11:
12: }
13: };
用這個模板同樣可以完成上面那個 ID 為 IDOK 的按鈕的點擊事件的處理,并且進一步地從消息當中分解出了按鈕對象的指針:
1: LRESULT onCommand( ButtonClicked<IDOK>& okClicked )
2: {
3: Button* button = okClicked.button;
4:
5: // do something
6: // ...
7:
8: return okClicked.handled(this);
9: }
這些消息函數(shù)的名字并不是一定得命名為 onMessage 和 onCommand。它們可以是任何合法的 C++ 函數(shù)名。例如還是那個按鈕點擊事件的處理函數(shù)可以寫成:
1: LRESULT buttonClicked( ButtonClicked<IDOK>& okClicked )
2: {
3: Button* button = okClicked.button;
4:
5: // do something
6: // ...
7:
8: return okClicked.handledBy(this,&Self::buttonClicked);
9: }
消息的手動映射:
在有的環(huán)境當中(例如以 XML 為模板運行過程當中的某個時刻動態(tài)創(chuàng)建界面),控件是動態(tài)創(chuàng)建的,顯然這時候動態(tài)生成的控件 ID 不是一個編譯期常數(shù),因為這樣的 ID 不能用于模板參數(shù),所以無法使用消息的自動映射(自動映射需要控件的 ID 作為模板參數(shù)),這種情況下框架必須要提供手動映射的接口。
這個框架同時對控件的命令消息(WM_COMMAND)和通知消息(WM_NOTIFY )提供了手動映射的接口。例如對按鈕提供了 onClicked,onDoubleClicked,對編輯框提供了 onChange,onUpdate 等這些用于消息映射的函數(shù)對象,客戶通過調(diào)用這些函數(shù)對象來實現(xiàn)手動映射(像onClicked 的這種函數(shù)對象其實本身并不處理消息,只是執(zhí)行一次消息映射的動作)
例如要處理名為 button1 和 button2 的按鈕的點擊事件:
1: int buttonClicked( Button* button )
2: {
3: // do something
4: // ...
5:
6: return 0;
7: }
8:
9: LRESULT onMessage( WidgetMessage<WM_CREATE>& message )
10: {
11: buttonChild("button1")->onClicked( this,&Self::buttonClicked );
12: buttonChild("button2")->onClicked( this,&Self::buttonClicked );
13:
14: return message.handled(this);
15: }
手動映射對消息處理函數(shù)的類型要求比較寬松,例如上面的函數(shù) buttonClicked 的參數(shù)不局限為 Button* ,也可以是 Widget* 或 MessageListener*,或其派生路徑上任何一種類型的指針。返回值則可以為任何類型。
消息參數(shù)分解
這個框架除了消息自動映射,還能夠很輕松地針對不同消息分解出消息攜帶的信息。例如上面的函數(shù)當中:
1: LRESULT onMessage( WidgetMessage<WM_SIZE>& message )
2: {
3: long clientWidth = message.cx;
4: long clientHeight = message.cy;
5:
6: wcout<<L"window sized"<<endl;
7: wcout<<L"client width is: "<<clientWidth<<endl;
8: wcout<<L"client height is: "<<clientHeight<<endl;
9:
10: return message.handled(this);
11: }
針對 WM_SIZE 消息進行了消息的分解,因此在消息處理函數(shù)當中,不用再為了獲得 WM_SIZE 攜帶的信息(客戶區(qū)的寬度和高度)而一遍一遍地寫 LOWORD 和 HIOWRD ,而是直接就能從 WM_SIZE 消息參數(shù)的丙個成員當中獲取到它們:
1: WidgetMessage<WM_SIZE>::cx
2: WidgetMessage<WM_SIZE>::cy
有兩種方法可以針對不同的消息以不同的方式分解參數(shù),一個模板的特化。在這個例子當中,WidgetMessage<WM_SIZE> 實際上就是是對模板 WidgetMessage<UINT t_message> 的特化
WidgetMessage<UINT t_message>模板的定義如下:
1: template<UINT t_mess>
2: class WidgetMessage:public WidgetMessageBase<WidgetMessage<t_mess>,t_mess>
3: {
4: public:
5: WidgetMessage( UINT mess,WPARAM wpar,LPARAM lpar )
6: :_BaseMessage(mess,wpar,lpar)
7: {
8:
9: }
10: };
其中的 WidgetMessageBase 模板是一個提供消息自動映射的基類,所有需要自動映射的消息均從此模板派生,并且WidgetMessageBase 保存了三個未經(jīng)加工的原始參數(shù):
1: UINT message
2: WPARAM wparam
3: LPARAM lparam
因此所有的 WidgetMessage 模板的非特化版本所攜帶的參數(shù)都是這樣三個參數(shù)。可以通過特化的方式來從這三個參數(shù)當中進一步分解出想要的部分,測試代碼當中針對 WM_SIZE 來進行特化的代碼如下:
1: template<>
2: class WidgetMessage<WM_SIZE>:public WidgetMessageBase<WidgetMessage<WM_SIZE>,WM_SIZE>
3: {
4: public:
5: long cx;
6: long cy;
7:
8: template<typename TWidget>
9: WidgetMessage( TWidget* widget,UINT mess,WPARAM wpar,LPARAM lpar )
10: :_BaseMessage( mess,wpar,lpar )
11: ,cx( LOWORD(lpar) )
12: ,cy( HIWORD(lpar) )
13: {
14:
15: }
16: };
再舉例針對 WM_PAINT 消息利用特化來分解參數(shù):
1: template<>
2: class WidgetMessage<WM_PAINT>:public WidgetMessageBase<WidgetMessage<WM_PAINT>,WM_PAINT>
3: {
4: public:
5: Widget* widget;
6: Canvas canvas;
7: PAINTSTRUCT paintStruct;
8:
9: template<typename TWidget>
10: WidgetMessage( TWidget* w,UINT mess,WPARAM wpar,LPARAM lpar )
11: :_BaseMessage( mess,wpar,lpar )
12: ,widget(w)
13: ,canvas( ::BeginPaint(w->handle(),&paintStruct) )
14: {
15:
16: }
17:
18: ~WidgetMessage()
19: {
20: ::EndPaint(widget->handle(),&paintStruct);
21: }
22: };
這樣就從 WM_PAINT 當中分解出了需要的參數(shù)。然后在消息處理函數(shù)當中可以這樣使用:
1: LRESULT onMessage( WidgetMessage<WM_PAINT>& message )
2: {
3: message.canvas.drawText(100,100,_T("hello world"));
4:
5: return message.handled(this);
6: }
雖然看起來特化模板很麻煩,但是實際上這是“do it for once,use for ever“(麻煩一次,方便永遠)的事。
還有一個分解參數(shù)的方法,就是直接從模板 WidgetMessageBase 派生。比如利用派生的方式分解 WM_CREATE 的參數(shù):
1: struct WindowCreating:public WidgetMessageBase<WindowCreating,WM_CREATE>
2: {
3: CREATESTRUCT* createStruct;
4:
5: WindowCreating( UINT m,WPARAM w,LPARAM l)
6: :_BaseMessage(m,w,l)
7: ,createStruct( reinterpret_cast<CREATESTRUCT*>(l) )
8: {
9:
10: }
11: };
現(xiàn)在 WM_CREATE 消息處理函數(shù)的樣子是這樣:
1: LRESULT onMessage( WindowCreating& message )
2: {
3: CREATESTRUCT* createStruct = message.createStruct;
4:
5: return message.handled(this);
6: }
總結(jié):
世上沒有完美的東西,自己寫的東西在剛完成它的時候總覺得已經(jīng)完美沒有再優(yōu)化的可能了,但是每一次回頭再看,都會發(fā)現(xiàn)許多的不足。這個框架提供的消息處理機制很靈活和方便了,不過等過幾天再來看它,一定會發(fā)現(xiàn)能改進的地方。