轉帖請注明出處 http://www.shnenglu.com/cexer/archive/2009/11/22/101591.html
1 胸口碎大石
緊接上話:GUI框架:談談框架,寫寫代碼 。廢話是肯定首先要說的,既為了承前啟后點明主題,也為了拉攏人心騙取回復。本來我想像自己上篇博文寫出來勢必像胸口碎大石一樣威猛有力,在街邊拉開陣勢,大吼一聲舉起錘子正要往下砸的時候,卻看到幾位神仙手提醬油瓶優雅地踏著凌波微步路過,聽他們開口閉口說的都是六脈神劍啊,九陰真級啊這些高級東西,我的威猛感一下消失于無形,取而代之的是小孩子玩水槍的渺小。但是不管怎么樣攤子都鋪開了,這一錘子不砸下去,對不起那涼了半天石頭的胸肌。
在此之前首先得感謝一下各位醬油眾。無論你們是看熱鬧的還是砸場子的,你們的圍觀都令我的博文增光不少。特別要感謝那幾位打架的神仙,你們使上篇博文真正變得有思想交鋒的精彩。我覺得你們的那些想法和爭論都非常有價值,建議你們不要只讓它們在這個角落里藏著,都寫到自己的博客上去讓更多的人看到吧。
走過路過不要錯過,有錢的捧個錢場,沒錢的繼續揮舞你的醬油瓶加油吶喊,我這一錘要砸下去了!
2 實現消息檢查者
上文將消息框架分為幾個部分,這篇博文實現其中的消息檢查者。經典的用 API 編寫 GUI 程序的方式當中,消息檢查都是用 if 或者 switch 語句進行的:
1: // 經典的 API 方式
2: switch( message )
3: {
4: case WM_CREATE:
5: // ......
6: break;
7: case WM_PAINT:
8: // ......
9: break;
10: default:
11: // ......
12: }
13:
14: // MFC 映射宏展開
15: if ( message == WM_CREATE )
16: {
17: // ......
18: }
19: if ( message == WM_PAINT )
20: {
21: // ......
22: }
見過的很多的 GUI 框架并沒有在這原始的方式上進步多少,"只是將黑換成暗"。比如 MFC 和 WTL 的消息映射宏,就像是披在 if 語句上的皇帝的新衣。這種消息檢查方式的好處是速度快,不用額外的空間消耗,但壞處更明顯:不容易擴充。我覺得在好處和壞處之間的取舍很容易,有必要單獨給消息檢查的過程實現一個更具 OO 含義的執行者:消息檢查者(MessageChecker )。
2.1 其實很簡單
要有消息檢查者,首先得有個消息(Message)。上篇博文中的消息定義雖然非常簡單,卻完全可以勝任目前需要的工作,因此我們直接復制過來。
1: typedef LRESULT MessageResult;
2: typedef UINT MessageId;
3: typedef WPARAM MessageWparam;
4: typedef LPARAM MessageLparam;
5:
6: // 簡單的消息定義
7: class Message
8: {
9: public:
10: Message( MessageId id_=0,MessageWparam wp_=0,MessageLparam lp_=0 )
11: :id( id_ )
12: ,wparam( wp_ )
13: ,lparam( lp_ )
14: ,result( 0 )
15: {}
16:
17: public:
18: MessageResult result;
19: MessageId id;
20: MessageWparam wparam;
21: MessageLparam lparam;
22: };
有了消息,現在開始定義消息檢查者。消息檢查者的職責:根據某種條件檢查消息并返回(是,否)的檢查結果,所以它既應該有用于檢查的數據,也應該有用于檢查的動作函數,并且該函數檢查返回布爾值。這不是很容易就出來了嗎:
1: // 領銜的消息檢查者
2: class MessageChecker
3: {
4: public:
5: MessageChecker( MessageId id_=0,MessageWparam wp_=0,MessageLparam lp_=0 )
6: :id( id_ )
7: ,wparam( wp_ )
8: ,lparam( lp_ )
9: {}
10:
11: public:
12: // 用于檢查消息的函數
13: virtual bool isOk( const Message& message ) const;
14:
15: public:
16: // 用于檢查消息的數據
17: MessageId id;
18: MessageWparam wparam;
19: MessageLparam lparam;
20: };
其中 MessageChecker::isOk 很明顯應該可以被后繼者重寫以實現不同的檢查方式,所以它應該是虛函數,這樣后繼者可以這樣的形式擴充檢查者隊伍:
1: // 命令消息檢查者
2: class CommandChecker:public MessageChecker
3: {
4: public:
5: virtual bool isOk( const Message& message );
6: };
7:
8: // 通知消息檢查者
9: class NotifyChecker:public MessageChecker
10: {
11: public:
12: virtual bool isOk( const Message& message );
13: };
看著 MessageChecker,CommandChecker,NotifyChecker 這些和諧的名字,感覺消息檢查者就這樣實現完成了,我們的 GUI 框架似乎已經成功邁出了重要的第一步。但是面對函數 isOk ,有經驗的程序員肯定會有疑問:真的這樣簡單就 ok 了?當然是 no。要是真有那么簡單,我何苦還在后面寫那么長的篇符呢,cppblog 又不能多寫字騙稿費的。
2.2 堆上生成 & 對象切割
看著虛函數 isOk 會想聯到兩個關鍵詞:多態,指針。多態意味著要保存為指針,指針意味著要在堆上生成。消息檢查者保存為指針的映射像下面這樣子(假設消息處理者叫做 MessageHandler ):
1: // 允許一個消息對應多個處理者
2: typedef vector<MessageHandler> _HandlerVector;
3:
4: // 為了多態必須保存 MessageChecker 的指針
5: typedef pair<MessageChecker*,_HandlerVector> _HandlerPair;
6:
7: // 消息映射
8: typedef vector<_HandlerPair> _HandlerMap;
堆上生成是萬惡之源。誰來負責銷毀?何時銷毀?效率問題怎么辦?有的人此時可能想到了引用計數,小對象分配技術,內存池。。。只是一個消息檢查的動作就用那么昂貴的實現,就像花兩萬塊買張鼠標墊一樣讓人難以接受。所以我們想保存消息映射者的對象 MessageChecker 而不是它的指針,就像這個樣子:
1: // 允許一個消息對應多個處理者
2: typedef vector<MessageHandler> _HandlerVector;
3:
4: // 保存 MessageChecker 對象而不是指針
5: typedef pair<MessageChecker,_HandlerVector> _HandlerPair;
6:
7: // 消息映射
8: typedef vector<_HandlerPair> _HandlerMap;
但這樣的保存方式帶來了一個新的問題:對象切割。如果往映射中放入派生類的對象比如 CommandChecker 或者 NotifyChecker,編譯器會鐵手無情地對它們進行切割,切割得它們體無完膚搖搖欲墜,只剩下 MessageChecker 子對象為止 。砍頭不要緊,只要主義真,但是要是切割過程中切掉了虛函數表這個命根子就完蛋了,在手執電鋸的編譯器面前玩耍虛函數,很難說會發生什么可怕的事情。
有一種解決方案是我們不使用真正的虛函數,而是自己模擬虛函數的功能。具體辦法是在 MessageChecker 當中保存一個函數指針,由子類去把它指向自己實現的函數,非虛的 MessageChecker::isOk 函數去調用這個指針。修改 MessageChecker 的定義:
1: // 領銜的消息檢查者,用函數指針模擬虛函數
2: class MessageChecker
3: {
4: // 模擬虛函數的函數指針類型
5: typedef bool (*_VirtualIsOk)( const MessageChecker* pthis,const Message& message );
6:
7: public:
8: MessageChecker( MessageId id_=0,MessageWparam wp_=0,MessageLparam lp_=0,_VirtualIsOk is_=&MessageChecker::virtualIsOk )
9: :id( id_ )
10: ,wparam( wp_ )
11: ,lparam( lp_ )
12: ,m_visok( is_ )
13: {}
14:
15: public:
16: // 非虛函數調用函數指針
17: bool isOk( const Message& message ) const
18: {
19: return m_visok( this,message );
20: }
21:
22: protected:
23: // 不騙你,我真的是虛函數
24: static bool virtualIsOk( const MessageChecker* pthis,const Message& message )
25: {
26: return pthis->id == message.id;
27: }
28:
29: public:
30: // 用于檢查消息的數據
31: MessageId id;
32: MessageWparam wparam;
33: MessageLparam lparam;
34:
35: protected:
36: // 模擬虛函數的函數指針
37: _VirtualIsOk m_visok;
38: };
如代碼所示的,MessageChecker::virtualIsOk 的默認實現是只檢查消息id,這也是 MFC 的映射宏 MESSAGE_HANDLER 干的事情。現在解決了不要堆生成與要求多態的矛盾關系,真正完成了消息檢查者的定義。
2.3 擴充隊伍
要擴展消息檢查者隊伍,可以從 MessageChecker 派生新類,定義自己的檢查函數(virtualIsOk 或者其它名字都可以)并將 MessageChecker::m_visok 指向這個函數。要注意的是因為存在對象切割問題,所以派生類不應該定義新的數據成員,畢竟切掉花花草草也是非常不好的。舉例說明派生方法。
比如從消息檢查者派生一個命令消息(Command)的檢查者 CommandChecker:它定義了一個函數 virtualIsOk ,此函數檢查消息id是否為 WM_COMMAND ,并進一步檢查控件id和命令code,然后將指針 Message::m_visok 指向這個函數 CommandChecker::virualIsOk,這樣 MessageChecker::isOk 實際上就是調用的 CommandChecker::virtualIsOk 了。CommandChecker 的功能類似于 MFC 的宏 COMMAND_HANDLER:
1: // 命令消息檢查者
2: class CommandChecker:public MessageChecker
3: {
4: public:
5: // 將函數指針指向自己的函數 CommandChecker;:virtualIsOk
6: CommandChecker( WORD id,WORD code )
7: :MessageChecker( WM_COMMAND,MAKEWPARAM(id,code),0,&CommandChecker::virtualIsOk )
8: {}
9:
10: protected:
11: // 檢查消息id是否為 WM_COMMAND,并進一步檢查控件id和命令code
12: static bool virtualIsOk( const MessageChecker* pthis,const Message& message )
13: {
14: WORD idToCheck = LOWORD( message.wparam );
15: WORD codeToCheck = HIWORD( message.wparam );
16:
17: WORD id = LOWORD( pthis->wparam );
18: WORD code = HIWORD( pthis->wparam );
19:
20: return message.id==WM_COMMAND && idToCheck==id && codeToCheck==code;
21: }
22: };
同理定義一個通知消息(Notification)的檢查者 NotifyChecker,其功能類似于 MFC 的宏 NOTIFY_HANDLER :
1: // 通知消息檢查者
2: class NotifyChecker:public MessageChecker
3: {
4: public:
5: // 將函數指針指向自己的函數 NotifyChecker;:virtualIsOk
6: NotifyChecker( WORD id,WORD code )
7: :MessageChecker( WM_NOTIFY,MAKEWPARAM(id,code),0,&NotifyChecker::virtualIsOk )
8: {}
9:
10: public:
11: // 檢查消息的 id 是否為 WM_NOTIFY ,并進一步檢查控件 id 和命令 code
12: static bool virtualIsOk( const MessageChecker* pthis,const Message& message )
13: {
14: WORD idToCheck = reinterpret_cast<NMHDR*>(message.lparam)->idFrom;
15: WORD codeToCheck = reinterpret_cast<NMHDR*>(message.lparam)->code;
16:
17: WORD id = LOWORD( pthis->wparam );
18: WORD code = HIWORD( pthis->wparam );
19:
20: return message.id==WM_NOTIFY && idToCheck==id && codeToCheck==code;
21: }
22: };
發揮想像進行擴展,這個消息檢查者可以做很多事情。比如定義一個范圍id內命令消息的檢查者 RangeIdCommandChecker,其功能類似于 MFC 的 ON_COMMAND_RANGE 宏:
1: // 范圍 id 命令消息檢查者
2: class RangeIdCommandChecker:public MessageChecker
3: {
4: public:
5: // 將函數指針指向自己的函數 RangeIdCommandChecker;:virtualIsOk
6: RangeIdCommandChecker( WORD idMin,WORD idMax,WORD code )
7: :MessageChecker( WM_COMMAND,MAKEWPARAM(idMin,idMax),MAKELPARAM(code,0),&RangeIdCommandChecker::virtualIsOk )
8: {}
9:
10: protected:
11: // 檢查消息 id 是否為 WM_COMMAND,并進一步檢查控件 id 范圍和命令 code
12: static bool virtualIsOk( const MessageChecker* pthis,const Message& message )
13: {
14: WORD idToCheck = LOWORD( message.wparam );
15: WORD codeToCheck = HIWORD( message.wparam );
16:
17: WORD idMin = LOWORD( pthis->wparam );
18: WORD idMax = HIWORD( pthis->wparam );
19: WORD code = LOWORD( pthis->lparam );
20:
21: return message.id==WM_COMMAND && codeToCheck==code && idToCheck>=idMin && idToCheck<=idMax;
22: }
23: };
定義一個按鈕點擊消息的消息檢查者:
1: // 按鈕點擊的命令消息檢查者
2: class ButtonClickingChecker:public MessageChecker
3: {
4: public:
5: // 將函數指針指向自己的函數 ButtonClickChecker;:virtualIsOk
6: ButtonClickingChecker( WORD id )
7: :MessageChecker( WM_COMMAND,MAKEWPARAM(id,BN_CLICKED),0,&ButtonClickingChecker::virtualIsOk )
8: {}
9:
10: protected:
11: // 檢查消息id是否為 WM_COMMAND,并進一步檢查命令code是否為 BN_CLICKED 以及按鈕id
12: static bool virtualIsOk( const MessageChecker* pthis,const Message& message )
13: {
14: WORD idToCheck = LOWORD( message.wparam );
15: WORD codeToCheck = HIWORD( message.wparam );
16:
17: WORD id = LOWORD( pthis->wparam );
18: WORD code = BN_CLICKED;
19:
20: return message.id==WM_COMMAND && idToCheck==id && codeToCheck==code;
21: }
22: };
2.4 數據不夠用
這一節是新加的。因為有人提出數據容納不下的問題:MessageChecker 目前的定義只能容納 WPAWAM,LPARAM 兩個參數大小的數據,而因為在存在對象切割問題,子類又不能定義新的數據,那如果 WPARAM,LPARAM 裝不下需要的數據了怎么辦?難道就只能把數據在子類當中定義,然后每次都在堆上生成 MessageChecker 嗎?
我在這里的解決方案是,在 MessageChecker 里面定義一個多態的數據成員,這個成員大多數時候是空的,當需要的時候從堆上生成它,用它來裝下數據。如代碼所示:
先定義一個多態的數據類 MessageData:
1: // 消息數據
2: class MessageData
3: {
4: public:
5: virtual ~MessageData(){}
6: virtual MessageData* clone() const = 0;
7: virtual void release(){ delete this; }
8: };
在 MessageChecker 當中加入這個數據成員:
1: // 加入多態的數據成員
2: class MessageChecker
3: {
4: //......
5: public:
6: MessageData* data;
7: }
8:
9: // 拷貝構造時同時深拷貝數據
10: MessageChecker::MessageChecker( const MessageChecker& other )
11: :id( other.id )
12: ,wparam( other.wparam )
13: ,lparam( other.lparam )
14: ,m_visok( other.m_visok )
15: ,data( other.data?other.data->clone():NULL )
16: {}
17:
18: // 拷貝時同時深拷貝數據
19: MessageChecker& MessageChecker::operator=( const MessageChecker& other )
20: {
21: if ( this != &other )
22: {
23: id = other.id;
24: wparam = other.wparam;
25: lparam = other.lparam;
26: m_visok = other.m_visok;
27:
28: if ( data )
29: {
30: data->release();
31: data = NULL;
32: }
33:
34: if ( other.data )
35: {
36: data = other.data->clone();
37: }
38: }
39: return *this;
40: }
41:
42: // 析構時刪除
43: MessageChecker::~MessageChecker()
44: {
45: if ( data )
46: {
47: data->release();
48: data = NULL;
49: }
50: }
舉例說明怎么樣使用 data 成員。例如要在一個消息檢查者需要比較字符串,則在其中必須要保存供比較的字符串。先從 MessageData 派生一個保存字符串的數據類 MessageString:
1: // 裝字符串多態數據類
2: class MessageString:public MessageData
3: {
4: public:
5: MessageString( const String& string )
6: :content( string )
7: {}
8:
9: virtual MessageData* clone() const
10: {
11: return new MessageString( content );
12: }
13:
14: public:
15: String content;
16: };
然后在這個消息檢查者中可以使用這個類了:
1: // 檢查字符串的消息檢查者
2: class StringMessageChecker:public MessageChecker
3: {
4: public:
5: StringMessageChecker( const String& string )
6: :MessageChecker( WM_SETTEXT,0,0,&StringMessageChecker::virtualIsOk )
7: {
8: data = new MessageString( string );
9: }
10:
11: public:
12: static bool virtualIsOk( const MessageChecker* pthis,const Message& message )
13: {
14: if ( message.id != pthis->id )
15: {
16: return false;
17: }
18:
19: MessageString* data = (MessageString*)pthis->data;
20: if ( !data )
21: {
22: return false;
23: }
24:
25: std::string stirngToCheck = (const Char*)( message.lparam );
26: return stirngToCheck == data->content;
27: }
28: };
為了使用方便可以定義一個數據類的模板:
1: // 數據類的模板
2: template <typename TContent>
3: class MessageDataT:public MessageData
4: {
5: public:
6: MessageDataT( const TContent& content_ )
7: :content( content_ )
8: {}
9:
10: virtual MessageData* clone() const
11: {
12: return new MessageDataT( content );
13: }
14:
15: public:
16: TContent content;
17: };
然后可以將字符串數據類的定義簡化為:
1: // 利用模板生成數據類
2: typedef MessageDataT<String> MessageString;
2.5 龍套演員
接下來可以進行消息檢查者的測試了。但因為消息檢查者的工作牽到其它兩個角色,所以測試之前必須要先簡單模擬出這兩個龍套角色:消息處理者和消息監聽者。
消息處理者(MessageHandler )的作用顧名思義不用多作解釋,它在 GUI 框架當中的作用十分重要,實現起來也最復雜,但在這本文中它不是主角,所以可以先隨便拉個路人甲來跑跑龍套,路人甲長得像這個樣子:
1: // 路人甲表演的消息處理者
2: typedef void (*MessageHandler)( const Message& message );
消息監聽者(MessageListener )保存有消息檢查者與消息處理者的映射,并提供接口操作這些映射,當監聽到消息的時候,消息監聽者調用映射中的檢查者進行檢查,如果通過檢查則調用消息處理者來進行處理。消息監聽者干的都是添加/刪除/查找這樣的體力活,看似實現起來用不著大腦,可是當涉及到真正的消息處理時情況會變得復雜,會遇到消息重入之類的問題。所以真正的實現留待日后再說,這里也先給出一個簡單的模擬定義含混過去:
1: // 消息監聽者
2: class MessageListener
3: {
4: typedef vector<MessageHandler> _HandlerVector;
5: typedef pair<MessageChecker,_HandlerVector> _HandlerPair;
6: typedef vector<_HandlerPair> _HandlerMap;
7: typedef _HandlerVector::iterator _HandlerVectorIter;
8: typedef _HandlerMap::iterator _HandlerMapIter;
9:
10: public:
11: virtual ~MessageListener(){}
12:
13: // 消息從這里來了
14: virtual void onMessage( const Message& message );
15:
16: public:
17: // 操作映射的接口
18: bool addHandler( const MessageChecker& checker,const MessageHandler& handler );
19: bool removeHandler( const MessageChecker& checker,const MessageHandler& handler );
20: bool clearHandlers( const MessageChecker& checker );
21:
22: protected:
23: // 消息檢查者與消息處理者的映射
24: _HandlerMap m_map;
25: };
其中調用消息檢查者的是關鍵函數是 MessageListener::onMessage( const Message& mesage ) ,實現很簡單,查找出消息處理者列表然后逐個調用其中處理者:
1: // 調用檢查者檢查,通過則調用處理者處理
2: void MessageListener::onMessage( const Message& message )
3: {
4: for ( _HandlerMapIter it = m_map.begin(); it!=m_map.end(); ++it )
5: {
6: if ( (*it).first.isOk(message) )
7: {
8: _HandlerVector* handers = &(*it).second;
9: if ( handers && !handers->empty() )
10: {
11: for ( _HandlerVectorIter it=handers->begin(); it!=handers->end(); ++it )
12: {
13: (*it)( message );
14: }
15: }
16: }
17: }
18: }
2.6 進行測試
龍套都已就位,導演喊:"action!",現在可以寫點測試代碼測試一下了:
1: void handleCreated( const Message& message )
2: {
3: cout<<"::handleCreated";
4: cout<<"\n"<<endl;
5: }
6:
7: void handleCommand( const Message& message )
8: {
9: cout<<"::handleCommand\t";
10: cout<<"id:"<<LOWORD(message.wparam);
11: cout<<"\t";
12: cout<<"code:"<<HIWORD(message.wparam);
13: cout<<"\n"<<endl;
14: }
15:
16: void handleClicked( const Message& message )
17: {
18: cout<<"::handleClicked\t";
19: cout<<"id:"<<LOWORD(message.wparam);
20: cout<<"\n"<<endl;
21: }
22:
23: void handleRangeCommand( const Message& message )
24: {
25: cout<<"::handleRangeCommand\t";
26: cout<<"id:"<<LOWORD(message.wparam);
27: cout<<"\t";
28: cout<<"code:"<<HIWORD(message.wparam);
29: cout<<"\n"<<endl;
30: }
31:
32: void handleString( const Message& message )
33: {
34: cout<<"::handleString\t";
35: cout<<"string:"<<(const char*)message.lparam;
36: cout<<"\n"<<endl;
37: }
38:
39:
40: #define ID_BUTTON_1 1
41: #define ID_BUTTON_2 2
42: #define ID_BUTTON_3 3
43: #define ID_BUTTON_4 4
44:
45: int main( int argc,char** argv )
46: {
47: MessageListener listener;
48: listener.addHandler( MessageChecker(WM_CREATE),&handleCreated );
49: listener.addHandler( CommandChecker(ID_BUTTON_1,BN_CLICKED),&handleCommand );
50: listener.addHandler( RangeIdCommandChecker(ID_BUTTON_1,ID_BUTTON_3,BN_CLICKED),&handleRangeCommand );
51: listener.addHandler( StringMessageChecker( "I love this game" ),&handleString );
52:
53: Message message( WM_CREATE );
54: listener.onMessage( message );
55:
56: message.id = WM_COMMAND;
57: message.wparam = MAKEWPARAM( ID_BUTTON_2,BN_CLICKED );
58: listener.onMessage( message );
59:
60: message.id = WM_COMMAND;
61: message.wparam = MAKEWPARAM( ID_BUTTON_1,BN_CLICKED );
62: listener.onMessage( message );
63:
64: message.id = WM_COMMAND;
65: message.wparam = MAKEWPARAM( ID_BUTTON_3,BN_CLICKED );
66: listener.onMessage( message );
67:
68: const char* string = "I love this game";
69: message.id = WM_SETTEXT;
70: message.lparam = (LPARAM)string;
71: listener.onMessage( message );
72:
73: return 0;
74: }
3 收場的話
這第一錘終于砸完了,石頭一裂為二,胸口完好無損。其實砸的時候心想,這一錘的分量砸下去不轟動神州也要震驚天府吧。但是回頭看看上面所有的文字,覺得這個東西怎么這么簡單,甚至連模板參數都沒有用到一個,更沒有談到效率,優化什么的,肯定是不足以誘惑技術流的 cpper 們的。
想起自己曾經寫過的幾個消息框架,可以算是把 C++ 的編譯期技術發揮得淋漓盡致了,但是出來的東西卻并不理想,后來慢慢領悟到一個道理:高尖的技術雖然炫酷,并不是處處都合適用。我的版本的消息檢查者就止于這個程度了,肯定有比這個更好的實現,希望走過路過的高手們不要吝嗇自己的好想法,提出來與廣大醬油眾分享。
消息檢查總算寫完了。沒選上好季節,電腦前坐了大半天手腳都冰涼的。上床睡覺去了,養足精神希望能看到新一輪的神仙打架。文章涉及的所有代碼項目下載:MessageChecker_200911251055.rar