GUI框架:谈谈框架


1 开篇废话

  我喜欢用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



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值