转帖请注明出处 http://www.cppblog.com/cexer/archive/2008/08/06/58169.html
我看过一些开源 GUI 框架的源代码,包括声句显赫的 WTL,win32gui 和 SmartWin,还有一些不知名但很优秀的,包括 jlib2( java AWT 在 C++ 上的移植 ),FLTK (比较小跨平台),甚至还曾鼓起勇气去看过 QT 那 n 万行的代码(当然没看明白)。
其中我个人觉得最好的并不是 win32gui 或者 SmartWin,WTL 这些“名”库,而是那个名不见经传的 AWT c++ 移植版本的 jlib2--AWT 的设计相当地优雅。SmartWin 则多少有点名不附实,里面的尖括号(模板)多到让人难以忍受的程度,实际上很多是不需要的,我在自己的框架当中只用了少量的模板就完成了 SmartWin 要完成的一部分功能。win32gui 是标准库风格的代码风格,看起来舒服,可是那接口也太古怪。
我想 SmartWin 和 win32gui,WTL 这些库那么出名,更多地是因为它们将一些非常有创意的手法运用到了 GUI 框架当中,想必以后的 GUI 框架都会多少受其影响。
看这些库都是为了“师夷长技“,因为我自己非常喜欢写 GUI 框架,没完没了反反复复地写。
CPPBLOG 上关于 GUI 框架的东西不是很多,自己写框架的少之又少。GUI 框架是如此大而复杂的一个车轮,除了少数像我这样的偏执狂,谁会愿意累得七窍流血去制作一个可能一开动就会车毁人亡的车轮呢?
写大一些的框架,是一种锻炼同时也是挑战。GUI 框架更是如此,要写一个好的 GUI 框架,就必须深入到很多的领域 ,比如线程,内存管理,内核对象,图形处理等等,这些都是在程序设计当中不容易驾驭的东西,如果还要完成诸如 序列化,脚本接口 这些东西,GUI 框架涉及的领域可以说非常地深广了。在与这些东西打交道的过程当中,我觉得自己慢慢地成长了不少。
不过写了数个版本,从来没有自己觉得满意的,想起以前的一个很牛的朋友说过:如果一个人看到自己很久以前写的代码仍然觉得满意,那么这个人要么很牛,要么很蹉,不过可以想像,大多有这样感觉的人都属于很蹉的那一部分,可见自己觉得不满意并不是坏事。一直写,就能一直进步,总有一天会以牛人的身分回头看看自己的代码,而仍能觉得很满意。
我写日志的一大问题就是,每次都是废话说完一大堆,仍不知道怎么自然地进入正题,这一次让我要以强硬的态度开门见山地进入。
进入正题:
介绍一下自己在写的一个 GUI 框架。
这个框架是以前几个版本的进化版,它的消息处理机制比较有意思。下面是这个框架的消息映射部分的一个测试代码,从这个代码里可以看出来这个框架在消息处理上的一些创新。
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 之类的一大堆宏来凑成一个巨大函数,也没有像 QT 那样的 connect(xxxx,xxxx) 的显示连接信号和槽的代码,你在上面的测试代码里找不到任何的映射痕迹,但是它的确工作得非常好。在这个 GUI 框架当中,你只需要定义消息处理函数,框架就能自动地帮你映射。例如上面定义的函数:
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 消息的时候,会自动地调用这个函数。
框架当中的 WidgetMessageBase 和 WidgetCommandBase ,SystemCommandBase 等模板以基类的形式,给派生类提供消息自动映射的能力。WidgetMessage,WidgetCommand,SystemCommand 等模板则提供了所有消息的一般实现。利用这些模板,只需要很简单的步骤即能完成消息映射和处理。
比如说要实现对消息 WM_CREATE 的处理,只需定义一个函数:
1: LRESULT onMessage( WidgetMessage<WM_CREATE>& message )
2: {
3: // do something
4: // ...
5:
6: return message.handled(this);
7: }
要实现对 ID 为 IDOK 的按钮的点击事件的处理,只需定义一个函数:
1: LRESULT onCommand( WidgetCommand<IDOK,BN_CLICKED>& okClicked )
2: {
3: // do something here
4: // ...
5:
6: return clicked.handled(this);
7: }
要实现系统菜单的关闭命令的处理,只需要定义一个函数:
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: }
这些消息函数的名字并不是一定得命名为 onMessage 和 onCommand。它们可以是任何合法的 C++ 函数名。例如还是那个按钮点击事件的处理函数可以写成:
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: }
消息的手动映射:
在有的环境当中(例如以 XML 为模板运行过程当中的某个时刻动态创建界面),控件是动态创建的,显然这时候动态生成的控件 ID 不是一个编译期常数,因为这样的 ID 不能用于模板参数,所以无法使用消息的自动映射(自动映射需要控件的 ID 作为模板参数),这种情况下框架必须要提供手动映射的接口。
这个框架同时对控件的命令消息(WM_COMMAND)和通知消息(WM_NOTIFY )提供了手动映射的接口。例如对按钮提供了 onClicked,onDoubleClicked,对编辑框提供了 onChange,onUpdate 等这些用于消息映射的函数对象,客户通过调用这些函数对象来实现手动映射(像onClicked 的这种函数对象其实本身并不处理消息,只是执行一次消息映射的动作)
例如要处理名为 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: }
手动映射对消息处理函数的类型要求比较宽松,例如上面的函数 buttonClicked 的参数不局限为 Button* ,也可以是 Widget* 或 MessageListener*,或其派生路径上任何一种类型的指针。返回值则可以为任何类型。
消息参数分解
这个框架除了消息自动映射,还能够很轻松地针对不同消息分解出消息携带的信息。例如上面的函数当中:
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 消息进行了消息的分解,因此在消息处理函数当中,不用再为了获得 WM_SIZE 携带的信息(客户区的宽度和高度)而一遍一遍地写 LOWORD 和 HIOWRD ,而是直接就能从 WM_SIZE 消息参数的丙个成员当中获取到它们:
1: WidgetMessage<WM_SIZE>::cx
2: WidgetMessage<WM_SIZE>::cy
有两种方法可以针对不同的消息以不同的方式分解参数,一个模板的特化。在这个例子当中,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 保存了三个未经加工的原始参数:
1: UINT message
2: WPARAM wparam
3: LPARAM lparam
因此所有的 WidgetMessage 模板的非特化版本所携带的参数都是这样三个参数。可以通过特化的方式来从这三个参数当中进一步分解出想要的部分,测试代码当中针对 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 消息利用特化来分解参数:
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 当中分解出了需要的参数。然后在消息处理函数当中可以这样使用:
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“(麻烦一次,方便永远)的事。
还有一个分解参数的方法,就是直接从模板 WidgetMessageBase 派生。比如利用派生的方式分解 WM_CREATE 的参数:
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: };
现在 WM_CREATE 消息处理函数的样子是这样:
1: LRESULT onMessage( WindowCreating& message )
2: {
3: CREATESTRUCT* createStruct = message.createStruct;
4:
5: return message.handled(this);
6: }
总结:
世上没有完美的东西,自己写的东西在刚完成它的时候总觉得已经完美没有再优化的可能了,但是每一次回头再看,都会发现许多的不足。这个框架提供的消息处理机制很灵活和方便了,不过等过几天再来看它,一定会发现能改进的地方。