好久没用c+写代码啦
我喜欢用C++写 GUI 框架,因为那种成就感是实实在在地能看到的。从毕业到现在写了好多个了,都是实验性质的。什么拳脚飞刀毒暗器,激光核能反物质,不论是旁门左道的阴暗伎俩,还是名门正派的高明手段,只要是 C++ 里有的技术都试过了。这当中接触过很多底层或是高级的技术,像编译时类型检测,运行时代码修改等等,按实现的不同 GUI 涉及的东西是没有边际的。从最开始模仿 MFC,ATL 那样的实现学到很多东西,然后开始看一些开源的著名的 GUI 框架,像 MFC,WTL,SmartWin++,win32gui,jlib2 ,VCF 获得很多启发,到现在似乎有一种已看尽天下 GUI 的感觉。在学习别人的框架和自己的实现过程中,真真实实地感觉自己成长了不少,也有很多感悟。
写到这,我作为轮子制造爱好者,在这里向那些喊着"不要重复制造轮子的"批评家们承认错误。在有那么多好的轮子的情况下,我不值得浪费地球资源,浪费时间精力来自己手工重复打造。但是不值得归不值得,在值得和喜欢之间我还是选择后者。并且人生在世,什么才是值得?我觉得不是拯救人类,为世界和平做贡献,也不是努力奋斗,为地球人民谋福利,而是简单地做自己喜欢的事。
写过的那些代码很多都消失在硬盘的海洋里了,但那些挑灯苦想来的感悟还在。在它们也消失之前,我想利用空闲时间把这些觉得有点用处的经验写出来,正好这个博客也已经快一年没更新了。另外也算是对那些发我邮件的朋友的回应。
我的想法是用一系列日志,按照实现一个 GUI 框架的具体思维递进过程来阐述实现一个 GUI 框架的具体思维递进过程。这样说好像有点递归,简单地解释就是这一系列日志不是想用《记忆碎片》那样错乱的叙述方式来说明一个多有意思的故事,而是尽量简单自然地记录一下写 GUI 框架过程中我的思考。这个递进过程也就是实现一个 GUI 框架的过程,一系列日志之后,我们将会看到一个长得漂亮眼,极富弹性,能干又节约的 GUI 框架。
虽然写的内容都是在 Windows 的 GUI 系统之上,但其原理是触类旁通的,其它基于消息的 GUI 系统也都大同小异。所用的代码也都是阐述原理的,自知绝对达不到商业巨作的水准,所以请不要一上来就批判,要知道我只是想分享而已。之所以先这样说一下,是很害怕那种一上来就"怎么不跨平台啊?“,“怎么都还看得到HWND啊?“,“怎么不能用成员函数处理消息啊?“的同志。不喜欢站在高处指着别人的天灵盖说话的人。要知道车轮也是一步步造出来的,不要一开始就想载着MM在高速路上飙豪车像少年啦飞驰。
我认为写技术博客有三种境界,一种是一直在那绘声绘色地描述自己的鱼有多可口多美味,让读者只能垂涎兴叹,一种是授人以鱼的人,闷头就摆出来各种生猛海鲜,让读者难以消化,还有一种境界是授人以渔,怎么钓鱼怎么煮鱼都细细地教给读者。读博客的人有两个境界,一种是只吃鱼的,一上来就只要代码,一种是学打鱼的,想知其然更想知其所以然。读博客时我努力做学打鱼的类型,自己写博客时我会努力做到授人以渔的境界。
另外要说明的是,同样作为尘世中的一个渺小个体,我大多数时候也是在为生存而奔波劳累着的。除此之外剩余的大多时候,更是要玩游戏,K歌,看电影,陪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% 。下篇准备开始写最简单的消息检查者,但说实话我也不知道下一篇什么时候开始。看看上一篇日志,竟然是一年前写的,这一年内发生的事情很多,但自己浑浑噩噩地的好像一眨眼就到了现在,看着 CPPBLOG 上的好多其它兄弟出的很多很有水准的东西,心里真是惭愧。昨天看了《2012》,现在心里还残留有那种全世界在一间瞬间灰飞烟灭的震撼,2012年也不远了,我也赶紧在地球毁灭之前加把油把这些日志写完了吧。代码难写。
回避的是虚函数。举个例子
class Base
{
public void NeedToInvokeOnDraw()
{
((Derived*)this)->OnDraw();
}
public void OnDraw(){}//空实现
}
class Derived1 : public Base
{
public void OnDraw(){}//我有新OnDraw
}
class Derived2 : public Base
{
//我用旧OnDraw
}
执行代码编译吧