轉(zhuǎn)帖請(qǐng)注明出處 http://www.shnenglu.com/cexer/archive/2008/08/06/58169.html
我看過(guò)一些開(kāi)源 GUI 框架的源代碼,包括聲句顯赫的 WTL,win32gui 和 SmartWin,還有一些不知名但很優(yōu)秀的,包括 jlib2( java AWT 在 C++ 上的移植 ),F(xiàn)LTK (比較小跨平臺(tái)),甚至還曾鼓起勇氣去看過(guò) QT 那 n 萬(wàn)行的代碼(當(dāng)然沒(méi)看明白)。
其中我個(gè)人覺(jué)得最好的并不是 win32gui 或者 SmartWin,WTL 這些“名”庫(kù),而是那個(gè)名不見(jiàn)經(jīng)傳的 AWT c++ 移植版本的 jlib2--AWT 的設(shè)計(jì)相當(dāng)?shù)貎?yōu)雅。SmartWin 則多少有點(diǎn)名不附實(shí),里面的尖括號(hào)(模板)多到讓人難以忍受的程度,實(shí)際上很多是不需要的,我在自己的框架當(dāng)中只用了少量的模板就完成了 SmartWin 要完成的一部分功能。win32gui 是標(biāo)準(zhǔn)庫(kù)風(fēng)格的代碼風(fēng)格,看起來(lái)舒服,可是那接口也太古怪。
我想 SmartWin 和 win32gui,WTL 這些庫(kù)那么出名,更多地是因?yàn)樗鼈儗⒁恍┓浅S袆?chuàng)意的手法運(yùn)用到了 GUI 框架當(dāng)中,想必以后的 GUI 框架都會(huì)多少受其影響。
看這些庫(kù)都是為了“師夷長(zhǎng)技“,因?yàn)槲易约悍浅O矚g寫(xiě) GUI 框架,沒(méi)完沒(méi)了反反復(fù)復(fù)地寫(xiě)。
CPPBLOG 上關(guān)于 GUI 框架的東西不是很多,自己寫(xiě)框架的少之又少。GUI 框架是如此大而復(fù)雜的一個(gè)車輪,除了少數(shù)像我這樣的偏執(zhí)狂,誰(shuí)會(huì)愿意累得七竅流血去制作一個(gè)可能一開(kāi)動(dòng)就會(huì)車毀人亡的車輪呢?
寫(xiě)大一些的框架,是一種鍛煉同時(shí)也是挑戰(zhàn)。GUI 框架更是如此,要寫(xiě)一個(gè)好的 GUI 框架,就必須深入到很多的領(lǐng)域 ,比如線程,內(nèi)存管理,內(nèi)核對(duì)象,圖形處理等等,這些都是在程序設(shè)計(jì)當(dāng)中不容易駕馭的東西,如果還要完成諸如 序列化,腳本接口 這些東西,GUI 框架涉及的領(lǐng)域可以說(shuō)非常地深廣了。在與這些東西打交道的過(guò)程當(dāng)中,我覺(jué)得自己慢慢地成長(zhǎng)了不少。
不過(guò)寫(xiě)了數(shù)個(gè)版本,從來(lái)沒(méi)有自己覺(jué)得滿意的,想起以前的一個(gè)很牛的朋友說(shuō)過(guò):如果一個(gè)人看到自己很久以前寫(xiě)的代碼仍然覺(jué)得滿意,那么這個(gè)人要么很牛,要么很蹉,不過(guò)可以想像,大多有這樣感覺(jué)的人都屬于很蹉的那一部分,可見(jiàn)自己覺(jué)得不滿意并不是壞事。一直寫(xiě),就能一直進(jìn)步,總有一天會(huì)以牛人的身分回頭看看自己的代碼,而仍能覺(jué)得很滿意。
我寫(xiě)日志的一大問(wèn)題就是,每次都是廢話說(shuō)完一大堆,仍不知道怎么自然地進(jìn)入正題,這一次讓我要以強(qiáng)硬的態(tài)度開(kāi)門(mén)見(jiàn)山地進(jìn)入。
進(jìn)入正題:
介紹一下自己在寫(xiě)的一個(gè) GUI 框架。
這個(gè)框架是以前幾個(gè)版本的進(jìn)化版,它的消息處理機(jī)制比較有意思。下面是這個(gè)框架的消息映射部分的一個(gè)測(cè)試代碼,從這個(gè)代碼里可以看出來(lái)這個(gè)框架在消息處理上的一些創(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: }
消息自動(dòng)映射
不像 MFC,ATL 那樣的 BEGIN_MESSAGE_MAP ,MESSAGE_HANDLER 之類的一大堆宏來(lái)湊成一個(gè)巨大函數(shù),也沒(méi)有像 QT 那樣的 connect(xxxx,xxxx) 的顯示連接信號(hào)和槽的代碼,你在上面的測(cè)試代碼里找不到任何的映射痕跡,但是它的確工作得非常好。在這個(gè) GUI 框架當(dāng)中,你只需要定義消息處理函數(shù),框架就能自動(dòng)地幫你映射。例如上面定義的函數(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 消息的時(shí)候,會(huì)自動(dòng)地調(diào)用這個(gè)函數(shù)。
框架當(dāng)中的 WidgetMessageBase 和 WidgetCommandBase ,SystemCommandBase 等模板以基類的形式,給派生類提供消息自動(dòng)映射的能力。WidgetMessage,WidgetCommand,SystemCommand 等模板則提供了所有消息的一般實(shí)現(xiàn)。利用這些模板,只需要很簡(jiǎn)單的步驟即能完成消息映射和處理。
比如說(shuō)要實(shí)現(xiàn)對(duì)消息 WM_CREATE 的處理,只需定義一個(gè)函數(shù):
1: LRESULT onMessage( WidgetMessage<WM_CREATE>& message )
2: {
3: // do something
4: // ...
5:
6: return message.handled(this);
7: }
要實(shí)現(xiàn)對(duì) ID 為 IDOK 的按鈕的點(diǎn)擊事件的處理,只需定義一個(gè)函數(shù):
1: LRESULT onCommand( WidgetCommand<IDOK,BN_CLICKED>& okClicked )
2: {
3: // do something here
4: // ...
5:
6: return clicked.handled(this);
7: }
要實(shí)現(xiàn)系統(tǒng)菜單的關(guān)閉命令的處理,只需要定義一個(gè)函數(shù):
1: LRESULT onCommand( SystemCommand<SC_CLOSE>& command )
2: {
3: // do something
4: // ...
5:
6: return command.handled(this);
7: }
對(duì)于命令消息( WM_COMMAND),可以進(jìn)一步地利用派生進(jìn)行命令消息的分類處理,例如可以定義一個(gè)針對(duì)所有按鈕點(diǎn)擊事件的消息模板:
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: };
用這個(gè)模板同樣可以完成上面那個(gè) ID 為 IDOK 的按鈕的點(diǎn)擊事件的處理,并且進(jìn)一步地從消息當(dāng)中分解出了按鈕對(duì)象的指針:
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ù)名。例如還是那個(gè)按鈕點(diǎn)擊事件的處理函數(shù)可以寫(xiě)成:
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: }
消息的手動(dòng)映射:
在有的環(huán)境當(dāng)中(例如以 XML 為模板運(yùn)行過(guò)程當(dāng)中的某個(gè)時(shí)刻動(dòng)態(tài)創(chuàng)建界面),控件是動(dòng)態(tài)創(chuàng)建的,顯然這時(shí)候動(dòng)態(tài)生成的控件 ID 不是一個(gè)編譯期常數(shù),因?yàn)檫@樣的 ID 不能用于模板參數(shù),所以無(wú)法使用消息的自動(dòng)映射(自動(dòng)映射需要控件的 ID 作為模板參數(shù)),這種情況下框架必須要提供手動(dòng)映射的接口。
這個(gè)框架同時(shí)對(duì)控件的命令消息(WM_COMMAND)和通知消息(WM_NOTIFY )提供了手動(dòng)映射的接口。例如對(duì)按鈕提供了 onClicked,onDoubleClicked,對(duì)編輯框提供了 onChange,onUpdate 等這些用于消息映射的函數(shù)對(duì)象,客戶通過(guò)調(diào)用這些函數(shù)對(duì)象來(lái)實(shí)現(xiàn)手動(dòng)映射(像onClicked 的這種函數(shù)對(duì)象其實(shí)本身并不處理消息,只是執(zhí)行一次消息映射的動(dòng)作)
例如要處理名為 button1 和 button2 的按鈕的點(diǎn)擊事件:
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: }
手動(dòng)映射對(duì)消息處理函數(shù)的類型要求比較寬松,例如上面的函數(shù) buttonClicked 的參數(shù)不局限為 Button* ,也可以是 Widget* 或 MessageListener*,或其派生路徑上任何一種類型的指針。返回值則可以為任何類型。
消息參數(shù)分解
這個(gè)框架除了消息自動(dòng)映射,還能夠很輕松地針對(duì)不同消息分解出消息攜帶的信息。例如上面的函數(shù)當(dāng)中:
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: }
針對(duì) WM_SIZE 消息進(jìn)行了消息的分解,因此在消息處理函數(shù)當(dāng)中,不用再為了獲得 WM_SIZE 攜帶的信息(客戶區(qū)的寬度和高度)而一遍一遍地寫(xiě) LOWORD 和 HIOWRD ,而是直接就能從 WM_SIZE 消息參數(shù)的丙個(gè)成員當(dāng)中獲取到它們:
1: WidgetMessage<WM_SIZE>::cx
2: WidgetMessage<WM_SIZE>::cy
有兩種方法可以針對(duì)不同的消息以不同的方式分解參數(shù),一個(gè)模板的特化。在這個(gè)例子當(dāng)中,WidgetMessage<WM_SIZE> 實(shí)際上就是是對(duì)模板 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 模板是一個(gè)提供消息自動(dòng)映射的基類,所有需要自動(dòng)映射的消息均從此模板派生,并且WidgetMessageBase 保存了三個(gè)未經(jīng)加工的原始參數(shù):
1: UINT message
2: WPARAM wparam
3: LPARAM lparam
因此所有的 WidgetMessage 模板的非特化版本所攜帶的參數(shù)都是這樣三個(gè)參數(shù)。可以通過(guò)特化的方式來(lái)從這三個(gè)參數(shù)當(dāng)中進(jìn)一步分解出想要的部分,測(cè)試代碼當(dāng)中針對(duì) WM_SIZE 來(lái)進(jìn)行特化的代碼如下:
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: };
再舉例針對(duì) WM_PAINT 消息利用特化來(lái)分解參數(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 當(dāng)中分解出了需要的參數(shù)。然后在消息處理函數(shù)當(dāng)中可以這樣使用:
1: LRESULT onMessage( WidgetMessage<WM_PAINT>& message )
2: {
3: message.canvas.drawText(100,100,_T("hello world"));
4:
5: return message.handled(this);
6: }
雖然看起來(lái)特化模板很麻煩,但是實(shí)際上這是“do it for once,use for ever“(麻煩一次,方便永遠(yuǎn))的事。
還有一個(gè)分解參數(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é):
世上沒(méi)有完美的東西,自己寫(xiě)的東西在剛完成它的時(shí)候總覺(jué)得已經(jīng)完美沒(méi)有再優(yōu)化的可能了,但是每一次回頭再看,都會(huì)發(fā)現(xiàn)許多的不足。這個(gè)框架提供的消息處理機(jī)制很靈活和方便了,不過(guò)等過(guò)幾天再來(lái)看它,一定會(huì)發(fā)現(xiàn)能改進(jìn)的地方。