我喜欢用C++写 GUI 框架,因为那种成就感是实实在在地能看到的。从毕业到现在写了好多个了,都是实验性质的。什么拳脚飞刀毒暗器,激光核能反物质,不论是旁门左道的阴暗伎俩,还是名门正派的高明手段,只要是 C++ 里有的技术都试过了。这当中接触过很多底层或是高级的技术,像编译时类型检测,运行时代码修改等等,按实现的不同 GUI 涉及的东西是没有边际的。从最开始模仿 MFC,ATL 那样的实现学到很多东西,然后开始看一些开源的著名的 GUI 框架,像 MFC,WTL,SmartWin++,win32gui,jlib2 ,VCF 获得很多启发,到现在似乎有一种已看尽天下 GUI 的感觉。在学习别人的框架和自己的实现过程中,真真实实地感觉自己成长了不少,也有很多感悟。
写到这,我作为轮子制造爱好者,在这里向那些喊着"不要重复制造轮子的"批评家们承认错误。在有那么多好的轮子的情况下,我不值得浪费地球资源,浪费时间精力来自己手工重复打造。但是不值得归不值得,在值得和喜欢之间我还是选择后者。并且人生在世,什么才是值得?我觉得不是拯救人类,为世界和平做贡献,也不是努力奋斗,为地球人民谋福利,而是简单地做自己喜欢的事。
写过的那些代码很多都消失在硬盘的海洋里了,但那些挑灯苦想来的感悟还在。在它们也消失之前,我想利用空闲时间把这些觉得有点用处的经验写出来,正好这个博客也已经快一年没更新了。另外也算是对那些发我邮件的朋友的回应。
我的想法是用一系列日志,按照实现一个 GUI 框架的具体思维递进过程来阐述实现一个 GUI 框架的具体思维递进过程。这样说好像有点递归,简单地解释就是这一系列日志不是想用《记忆碎片》那样错乱的叙述方式来说明一个多有意思的故事,而是尽量简单自然地记录一下写 GUI 框架过程中我的思考。这个递进过程也就是实现一个 GUI 框架的过程,一系列日志之后,我们将会看到一个长得漂亮眼,极富弹性,能干又节约的 GUI 框架。
虽然写的内容都是在 Windows 的 GUI 系统之上,但其原理是触类旁通的,其它基于消息的 GUI 系统也都大同小异。所用的代码也都是阐述原理的,自知绝对达不到商业巨作的水准,所以请不要一上来就批判,要知道我只是想分享而已。之所以先这样说一下,是很害怕那种一上来就"怎么不跨平台啊?","怎么都还看得到HWND啊?","怎么不能用成员函数处理消息啊?"的同志。不喜欢站在高处指着别人的天灵盖说话的人。要知道车轮也是一步步造出来的,不要一开始就想载着MM在高速路上飙豪车像少年啦飞驰。
2 基本概念
基于消息的 GUI 框架的封装,一切都围绕消息展开。复杂的框架设计,明确了需求之后,第一步首先是划分模块。所以,要阐述一个设计过程,第一步也应该是先说清最基本的概念和模块划分,而不是一上来就用广义相对论把读者全部放倒。GUI 框架是干什么的当然是地球人都知道的,但 GUI 框架没有什么已经划分的标准概念,我是按照设计的需要来划分的。如果把 GUI 框架看作一个单位,那么这个单位里最重要的角色有这几个:
- 消息发送者(message sender)
- 消息监听者(message listener)
- 消息检查者(message checker)
- 消息处理者(message handler)
- 消息分解者(message cracker)
- 消息映射者(message mapper)
下面分别说明。
2.1 消息发送者和消息(message sender,message)
消息发送者其实只是在这里友情客串一下,它不在框架设计之内,由操作系统扮演这个劳苦功高的角色,它的工作是将消息发送到消息监听者。在这里面隐含了一下最重要的角色,消息。其实剩余的所有角色说到底也只是死跑龙套的,真正领衔的是消息本身,比如窗口大小改变了的消息,按钮被点击了的消息等等,所有人都高举旗帜紧密团结在它周围进行工作。但消息本身只是一个很简单的数据结构,因为再复杂的 GUI 系统,它的消息也不过是几个参数,所以框架的实现重点在其它的角色。在此之前简单地封装一下消息,一个最简单的封装可能是这样:
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: };
就这样的我们的公司已经有了核心角色了。从概念上讲,我们的这个基于消息的 GUI 框架已经完成了 99% 。然后我们可以以它为中心,按功能划分进行详细讨论,一步步完成那剩余的 1% 的极富创意和挑战的工作。在此之前,先得简单解释一下这几个角色都各是什么概念。消息传送者如上所述,将不在讨论范围内。
2.2 消息监听者(message listener)
消息监听者完成的工作是从操作系统接收到消息,消息是从这里真正到达了框架之内。最简单的消息监听者是一个提供给操作系统的回调函数,比如在 Windows 平台上这个函数的样子是这样:
1: //我是最质朴的消息接收者
2: LRESULT CALLBACK windowProc( HWND window,UINT id,WPARAM wparam,LPARAM lparam );
一个好 GUI 框架当然不能赤祼祼地使用这个东西,我们要在此之上进行面向对象的封装。消息监听者能想到的最自然的封装模式是观察者模式(Observer),这样的模式下的监听者实现看起来像这个样子:
1: //我是一个漂亮的观察者模式的消息监听者
2: class MessageListener
3: {
4: public:
5: virtual LRESULT onMessage( Message* message ) = 0;
6: };
7:
8: //监听者这样工作
9: MessageListener* listener;
10: window->addListener( listener );
11:
jlib2 和 VCF 的实现就是这种模式。但现实当中大多数框架没有使用这种模式,比如 SmartWin++ 和 win32gui ,甚至没有使用任何模式比如 MFC 和 WTL 。我想它们所以不采用观察者模式,有些是因为框架整体实现的牵制,有的则可能是因为没能解决某些技术问题。我们的 GUI 框架将实现观察者模式的消息监听者,所以这些问题我们后面也会遇到,到时候再详述。
2.3 消息检查者(message checker)
消息检查者完成的工作很简单。当收到消息的时候,框架调用消息检查者检查这个消息是否符合某种条件,如果符合,则框架再调用消息处理者来处理这个消息,所以有点类似一个转换者,输入(消息),输出一个(是/否)的值。最简单的检查者可能就是一个消息值的比较,比如:
1:
2: /最简单的消息检查者
3: essage.id == /*消息值*/
4:
5: /比如
6: essage.id == WM_CREATE
展开MFC 和 ATL 的消息映射宏,可以看到它们的消息检查就是用堆积起来的消息值比较语句完成。这就是消息检查者最原始最自然最简单的实现方式,但这种方式缺陷太多。我们的框架将实现一个自动化,具有扩展性的消息检查者,后文详细讨论。
2.4 消息处理者(message handler)
消息处理者是我们最终的目的。GUI 框架所做的一切努力都只是前期的准备,直到消息处理者运行起来那一刻,整个公司才算是真正地运转起来了。消息处理者的具体实现可能是自由函数,成员函数或者其它可调用体,甚至可以是外部脚本,处理完毕可能需要给操作系统返回一个结果。最简单的消息处理者可以就是条语句,比如:
1: //消息处理
2: alert( "窗口创建成功了!" );
3:
4: //返回结果
5: message.result = TRUE;
上面代码中"显示消息框"的动作就是一个消息处理,以上两行代码可视为消息处理者。最常见的消息处理者是函数,比如:
1: //消息处理
2: _handleCreated( message );
代码中的函数 _handleCreated 就是一个典型的消息处理者。消息处理者的实现难处在于,既要支持多样性的调用接口,又要支持统一的处理方式。我们的框架将实现一个支持自由函数,成员函数,函数对象,或者其它可调用体的消息处理者,并且这些可调用体可以具有不同参数列表。后文将进行消息处理者的详细讨论。
在这里有必要再说明一下。一个判断语句的大括号之前(判断部分)是消息检查的动作,大括号之内(执行部分)是实际的消息处理。因此一个判断语句虽简单,却包含消息检查者和消息处理者,以及另外一个神秘的部分(见后文),一共三个部分。代码像这样:
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)
消息分解者是为消息处理者服务的。不同的消息处理者需要的信息肯定不一样,比如一个绘制消息(WM_PAINT)的消息处理者可能需要的是一个图形设备的上下文句柄(HDC),而一个按钮点击消息(BN_CLICK)的消息处理者则可能需要的是按钮的ID,它们都不想看到一个赤祼祼的消息杵在那里。从消息中分解出消息携带的具体信息,这就是消息分解者的工作。最简单的消息分解者可能是一个强制转换,比如:
1: // WM_CREATE 消息参数分解
2: CREATESTRUCT* createStruct = (CREATESTRUCT*)message.lparam;
3:
4: // WM_SIZE 消息参数分解
5: long width = LOWORD( message.lparam );
6: long height = HIWORD( message.lparam );
上面的的代码虽然简单但 100% 完成了消息分解的任务,所以它也是合格的消息分解者。我的框架将实现一个自动化,可扩展的消息分解者。后文将以此为目标进行详细讨论。
2.6 消息映射者(message mapper)
消息映射者是最直接与框架外部打交道的部分,顾名思义,它的工作就是负责将消息检查者与消息处理者映射起来。最简单的映射者可以是一条判断语句,这个判断语句,如代码所示:
1: // if 语句的框架就是一个消息映射者
2:
3: // 消息映射者
4: if ( /*消息检查者*/ )
5: {
6: /*消息处理者*/
7: }
1: // if 语句将消息检查者 message.id==WM_CREATE 和消息处理者 _handleCreated(message) 联系起来了
2: if ( message.id == WM_CREATE )
3: {
4: _handleCreated( message );
5: }
上面的代码 的if 语句中,判断的部分是消息检查者,执行的部分是消息处理者。if 语句把这两个部分组成了一个映射,这是最简单的消息映射者。到这里可以发现,这个简单的 if 语句有多不简单。它低调谦逊但独自地完成了很多工作,就像公司的小张既要写程序,又要扫地倒茶,还义务地给女同事讲笑话。MFC 和 WTL 的消息映射宏展开就是这样的 if 语句。像 jlib2 那样的框架,虽然处理者都虚函数,但在底层也是用 if 语句判断消息然后来进行调用的。当然还有华丽一点的消息映射者,像这样:
1: // 华丽一点的消息映射者
2: window.onCreated( &_handledCreated );
这个 onCreated 也是一个消息映射者,在它的内部把 WM_CREAE 消息和 _handleCreated 函数映射到一起,这种方式最有弹性,但实现起来也比宏和虚函数都要困难得多。SmarWin++ 就是使用的这种方式,它的消息映射者版本看起来一样的阳光帅气,但内部实现有些细节稍嫌猥琐。我们的 GUI 框架将实现一个看起来更美,用起来很爽的消息映射者像这个样子:
1: // 将消息处理者列表清空,设置为某个处理者
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();
值得说一下,这种神奇的映射者是接近零成本的,它没有数据成员没有虚函数什么都没有,就是一个简单的空对象。就像传说中的工作能力超强,但却不拿工资,不泡公司MM,甚至午间盒饭也不要的理想职员。在后文当中会具体详述这个消息映射者的实现。
3 结尾
到目前为止我们的框架已经完成了 99% 。下篇准备开始写最简单的消息检查者
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