cexer

cexer
posts - 12, comments - 334, trackbacks - 0, articles - 0
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

GUI框架:谈谈框架,写写代码

Posted on 2009-11-15 18:09 cexer 阅读(12219) 评论(176)  编辑 收藏 引用 所属分类: GUIpattern

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在高速路上飙豪车像少年啦飞驰。

  我认为写技术博客有三种境界,一种是一直在那绘声绘色地描述自己的鱼有多可口多美味,让读者只能垂涎兴叹,一种是授人以鱼的人,闷头就摆出来各种生猛海鲜,让读者难以消化,还有一种境界是授人以渔,怎么钓鱼怎么煮鱼都细细地教给读者。读博客的人有两个境界,一种是只吃鱼的,一上来就只要代码,一种是学打鱼的,想知其然更想知其所以然。读博客时我努力做学打鱼的类型,自己写博客时我会努力做到授人以渔的境界。

  另外要说明的是,同样作为尘世中的一个渺小个体,我大多数时候也是在为生存而奔波劳累着的。除此之外剩余的大多时候,更是要玩游戏,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年也不远了,我也赶紧在地球毁灭之前加把油把这些日志写完了吧。不管怎么样,今天哥先走了,请不要迷恋哥。

评论共2页: 1 2 

Feedback

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 18:12 by 陈梓瀚(vczh)
@OwnWaterloo
回避的是虚函数。举个例子
class Base<Derived>
{
public void NeedToInvokeOnDraw()
{
((Derived*)this)->OnDraw();
}
public void OnDraw(){}//空实现
}

class Derived1 : public Base<Derived1>
{
public void OnDraw(){}//我有新OnDraw
}

class Derived2 : public Base<Derived2>
{
//我用旧OnDraw
}

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 18:13 by 陈梓瀚(vczh)
@OwnWaterloo
如何在属性实现状态变化控制的技术嘛,没有说过命名……看来我们是互相误解了啊……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 18:14 by 陈梓瀚(vczh)
@cexer
如果你的库不用dll+lib+h提供的话,请尽情使用模板……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 18:16 by OwnWaterloo
@陈梓瀚(vczh)
@cexer

我上面也说了, 这和普通的继承实现的功能是完全一样的。
依然没有虚函数开销,依然有一点点"多态"。

用vczh的例子来说:
class Base
{
        void OnDraw() { 空 }
};

class Derived1 : public Base
{
        void OnDraw() { 我有新OnDraw }
};

class Derived2 : public Base
{
// 我用默认Draw
};

不也一样吗? 除了Base现在是一个类,而不是类模板。
所以,我现在唯一想出的ATL-style继承的优势就是:header-only。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 18:20 by OwnWaterloo
@陈梓瀚(vczh)
【如果你的库不用dll+lib+h提供的话,请尽情使用模板】……

如果ATL-style继承的好处就是为了header-only。

class Base
{
        void OnDraw() { /* inline */ }
};

或者:
base.h
class Base
{
        void OnDraw();
};


base.inl
#include "base.h"

inline void Base::OnDraw() {}


也可以是header-only ……


那么,ATL-style继承,还有什么好处么……
一开始,我也觉得很神奇…… 后来越想越不是那么回事…… 然后一直没想通……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 18:39 by OwnWaterloo
@cexer
管饭的人呢……

这问题反复困扰我很久了…… 一直没得到解答…… 于是尘封了……
用ATL/WTL的人很少,问都找不到人问……
今天终于找到管饭的了, 一定得追问到底~_~

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 18:51 by OwnWaterloo
@陈梓瀚(vczh)
如何在属性实现状态变化控制的技术嘛,没有说过命名……看来我们是互相误解了啊…… 

那你也说说你的想法嘛……
比如那个“效率问题”应该怎么理解?

有人说可以拿C/C++程序员作巴浦洛夫实验,看提到"效率"2字时他们是否会流口水……

我没有流口水…… 但是我很好奇……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 19:48 by 陈梓瀚(vczh)
@OwnWaterloo
撇开论点不讲,其实我对语法最感兴趣……因为我做编译器……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 19:49 by 陈梓瀚(vczh)
@OwnWaterloo
header-only已经不错了,我现在最烦一个类要将界面重复在两个文件里面。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 19:51 by 陈梓瀚(vczh)
@OwnWaterloo
话说我想尝试自己实现BOOST_ITERATE(只用宏复制代码)的方法,最后研究了好久,发现用C的宏是不对的,应该自己制造代码生成器……C的宏缺乏语法上的可维护性

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 19:55 by LOGOS
我是来看神仙打架的
cexer别删 哈哈

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 20:09 by OwnWaterloo
@陈梓瀚(vczh)
【其实我对语法最感兴趣……因为我做编译器……】
能否做一个,呃,怎么说呢……

visual assistant你肯定用的吧?类似的还有:
msvc,gcc :
synedit : http://synedit.sourceforge.net/
scintilla : http://www.scintilla.org/
还有一些C/C++源代码转换到html、pdf等格式的工具。

它们肯定都能分析C/C++源代码。
但是源代码分析后的结果,它们自产自销了,没有暴露出来。

你能否做做这方面的工作呢? 一个C/C++(或者其他语言)的源代码分析器,将分析的结果暴露出来。
方便其他用途使用,比如代码染色。

这样,很多编辑器、网页、甚至是pdf、doc都可以得益于你的工作。
而不是每个工具都自己实现一套(有的功能薄弱得……比如网页上的染色,基本就只能认识语言的关键字而已),然后自产自销。


你觉得这个提议怎样?

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 20:13 by OwnWaterloo
@陈梓瀚(vczh)
【header-only已经不错了,我现在最烦一个类要将界面重复在两个文件里面。】

头文件不行吗?为什么会这样?


话说我想尝试自己实现BOOST_ITERATE(只用宏复制代码)的方法,最后研究了好久,发现用C的宏是不对的,应该自己制造代码生成器……C的宏缺乏语法上的可维护性

我也觉得宏很不可靠……
有时候完全可以利用其他一些很方便的语言,来产生C/C++源代码。
凭借C/C++自身机制来进行元编程,有时候会很…… 很麻烦……

比如boost.pp,它用来产生模板类型参数列表的这个功能,其实也可以写一些其他语言的代码,用来生成C++源代码……

可读性应该会好很多……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 20:16 by cexer
@OwnWaterloo
模板有一大优势就是,不必在意类型是否已经存在,就能够任意调用它的任意成员。这是用虚函数也达不到的,因为虚函数也至少需要提供接口声明。
ATL这种方式,可以在不知道子类为何物的情况下调用它重写或者覆盖的成员。有时候可以完成用虚函数也无法达到的效果。
你试试不用虚函数,不用ATL的这种编译时的多态手法,在不知道子类为何物的情况下,在基类当中调用子类的方法试试?

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 20:29 by OwnWaterloo
@cexer
你说的是这种情况么?

non-template

class B
{
        virtual void f_do() = 0;
        void f()
        {
                ... // 有一些其他工作,不仅仅是转发
                f_do();
                ...
        }
};

B可以自己不实现f_do,并且具有这么一个限制:如果派生类不实现f_do,派生类将继续无法实例化。


ATL-style继承,提供"类似"的含义。
template<class D>
class B
{
        void f()
        {
                ... // 有一些其他工作,不仅仅是转发
                static_cast<D*>(this)->f_do();
                ...
        }
};

class D : public B<D>
{
        void f_do() {  }
}

如果不实现f_do,D就会产生编译错误。
类似于如果上面那种D如果不定义f_do就无法实例化。



普通继承无法提供这种限制语意:
class B
{
        void f()
        {
                ... // 有一些其他工作,不仅仅是转发
                f_do();
                ...
        }
        // void f_do();
        // 如果提供,派生类不不覆盖也能实例化,也不会产生编译错误。
        // 无法达到限制"派生类必须提供"的目的

        // 如果不提供,B就会产生链接错误。
};

是这样吗?

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 20:37 by OwnWaterloo
@cexer
【你试试不用虚函数,不用ATL的这种编译时的多态手法,在不知道子类为何物的情况下,在基类当中调用子类的方法试试? 】

应该是不行的。

只有template这种变态代码生成器才可以:
template<typename T>
void f(T v)
{
v.f(); // 让你先这么着了,等实例化的时候再做检查……
}

如果不是模板,父类是不能调用子类函数的。

那么,ATL-style继承的作用就是:实现静态的"template method"?

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 21:08 by OwnWaterloo
@cexer
有点感觉了。

ATL-style继承,一方面有点类类似concepts或者说ducking type的使用。
template<class D>
class B
{
        void f()
        {
                D* d = static_cast<B*>(this);
                d->must_have_this_function(); // 必须有某个成员函数
                d->must_have_this_variable;   // 必须有某个成员变量
                typedef typename D::must_have_this_type type;
                // 必须有某个嵌套类型
        }
};

B可以对D提出一些要求,D必须满足这些要求。
对这些要求,B可以不必提供"默认实现"。
这2点就是和普通继承很不相同的地方了。


同时,ATL并没有通过"组合"来复用库代码,而是通过"共有实现继承"来复用库的代码。
使得编写的代码会少一些。 就像那个著名的例子:

class person : public public nose, public mouth {};

person p;
p.eat(); mouth::eat
p.smell(); nose::smell


实际上呢,更"学术气息"一些的方案是:
class person
{
        nose n_;
        mouth m_;
        eye e_[2]; // left and right
public:
        // forwarding functions
        void eat() { m_.eat(); }
        void smell() { n_.smell(); }
};


但是在界面开发中……  这些转发函数会写死人的……
所以,就产生了ATL-style继承 —— concepts(ducking type) + 实现继承的结合。


只是为ATL-style继承举例的人的例子没有选好,无法充分说明它的优点。
我只粗看过《mfc程序员的wtl编程指南》还是什么的。
里面就是一个"没有体现出base对derived的需求",可以用普通继承改写的例子。


这样理解如何?


# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 21:13 by 陈梓瀚(vczh)
@OwnWaterloo
子类不需要非得实现什么,纯粹是模拟非纯虚函数。因为父类必须提供所有可以覆盖的界面的空实现,才可以让实现控件的人,只写他们关心的东西。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 21:15 by 陈梓瀚(vczh)
@OwnWaterloo
我重写VL++的目的就是想提供一个用来分析字符串(速度不用爆快,但肯定要好用)的库。虽然不一定会实现一个C++的语法分析器(实际上我可以告诉你我没法产生精确的语法树,如果不做语义分析我肯定不知道a<b,c>d;究竟是什么东西),但上下文无关着色什么的肯定是可以轻松实现的。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 21:27 by OwnWaterloo
@陈梓瀚(vczh)
【子类不需要非得实现什么,纯粹是模拟非纯虚函数。因为父类必须提供所有可以覆盖的界面的空实现,才可以让实现控件的人,只写他们关心的东西。】

上面已经说明了:
"父类提供所有可以覆盖的界面的空实现",或者说默认实现,并让实现控件的人,只写他们关心的东西 —— 这不需要ATL-style式的继承,普通继承就可以了。

只有在"向子类要求一些莫须有的东西"时,才需要依靠template。



【我重写VL++的目的就是想提供一个用来分析字符串(速度不用爆快,但肯定要好用)的库。虽然不一定会实现一个C++的语法分析器(实际上我可以告诉你我没法产生精确的语法树,如果不做语义分析我肯定不知道a<b,c>d;究竟是什么东西),但上下文无关着色什么的肯定是可以轻松实现的。】
编译原理那些东西几乎忘光……
我觉得为编辑器着色有一个相对于编译器来说很麻烦的地方……

编译器可以要求源代码必须是合乎语法的,否则就报错。
编辑器的着色分析就不能这么暴力了,它必须能够接受半完整的源代码,并且尽可能从中榨取出信息……

所以visual assistant还是很强大的……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 21:44 by 空明流转
@OwnWaterloo
GCC-XML,你可以搜搜这个工具,可以把C++分析成XML结构的东西。如果你需要在语言粒度上进行调整,可以在此基础上做。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 21:47 by OwnWaterloo
@空明流转
我靠!还有这种工具!
太感谢了~_~

其实我也不做什么的啦……
主要是写文档(设计文档、博客)的时候染染色啦……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-16 22:01 by OwnWaterloo
@空明流转
好东西啊,好东西,看得我心花怒放~_~
再次感谢~_~


顺便也感谢一下cexer。
果然是【阅尽MFC,WTL,SmartWin++,win32gui,jlib2 ,VCF ;看尽天下 GUI 的感觉。】

果然只看示例和书不写代码是不行的。就书上的示例代码,我还领会不了其精髓……
其实也不是我不想写…… 我写过一些……
然后就没机会写了……

# re: GUI框架:谈谈框架,写写代码[未登录]  回复  更多评论   

2009-11-17 17:03 by Loaden
强烈猛顶!
  写到这,我作为轮子制造爱好者,在这里向那些喊着"不要重复制造轮子的"批评家们承认错误。在有那么多好的轮子的情况下,我不值得浪费地球资源,浪费时间精力来自己手工重复打造。但是不值得归不值得,在值得和喜欢之间我还是选择后者。并且人生在世,什么才是值得?我觉得不是拯救人类,为世界和平做贡献,也不是努力奋斗,为地球人民谋福利,而是简单地做自己喜欢的事。

看来我和楼主是一样类型。
我也是从封装类似ATL框架开发,目前正在模仿jlib2...
还有很长的路要走!
向楼主学习!

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-17 18:04 by visualfc
c++源码分析,可以利用ctags,源代码可以看一下ctags,CodeBlocks的codecompletion,Qt Creator的cplusplus。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-17 20:36 by 陈梓瀚(vczh)
@OwnWaterloo
只要你不是在为一个IDE提供着色分析,那都很好做……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-17 20:54 by OwnWaterloo
@visualfc
Qt Creator我知道,Qt Creator的cplusplus是指的什么?
Qt Creator的自动完成功能吗?

我看过你的一些作品,果然对这方面的东西了解很多~_~


@陈梓瀚(vczh)
其实我的需求上面也有说…… 就是将代码染染色……
不然直接在blog上贴代码太丑陋了……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-17 21:23 by visualfc
CPlusPlus是Qt Creator中用于分析C++源文件的核心库
代码位于 Qt Creator源码的 src/libs/cplusplus/目录下
其中 C++ 语法分析/错误检查部分使用标准C++库完成,不使用QT库。
我也是刚开始看。

对于CodeBlocks的C++源码分析可以参看下面的wiki。
http://wiki.codeblocks.org/index.php?title=Code_Completion_Design



# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-17 21:28 by OwnWaterloo
@visualfc
谢谢分享~_~

英文…… sigh……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-17 21:42 by visualfc
@OwnWaterloo
:-)
QT的cplusplus写的要比CB的好。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-18 04:18 by OwnWaterloo
差不多敲定方案了,贴下代码吧:
 
class date
{
        
class impl
        {
                time_t time_;
                friend 
class date;
        };

        
int get_(int tm::*field) const { return localtime(&impl_.time_)->*field; }
        date
& set_(int tm::*field,int v)
        {
                tm t 
= *localtime(&impl_.time_);
                t.
*field = v;
                impl_.time_ 
= mktime(&t);
                
return *this;
        }

public:
        date() { time(
&impl_.time_); }

        
int get_hour() const { return get_(&tm::tm_hour); }
        date
& set_hour(int h) { return set_(&tm::tm_hour,h); }

        
int get_minute() const { return get_(&tm::tm_min); }
        date
& set_minute(int m) { return set_(&tm::tm_min,m); }

        
int get_second() const { return get_(&tm::tm_sec); }
        date
& set_second(int s) { return set_(&tm::tm_sec,s); }

        union
        {
                impl impl_;

                property::proxy
                        
<property::get<date,int,&date::get_hour>
                        
>               hour_read;

                property::proxy
                        
<property::set<date,date&,int,&date::set_hour>
                        
>               hour_write;

                property::proxy
                        
<property::get<date,int,&date::get_minute>
                        ,property::
set<date,date&,int,&date::set_minute>
                        
>               minute; // read write

                property::proxy
                        
<property::set<date,date&,int,&date::set_second>
                        ,property::
get<date,int,&date::get_second>
                        
>               second; // read write
        };
};

int main()
{
        typedef 
int ASSERT_EQUAL_SIZE[sizeof(date)==sizeof(time_t)?1:-1];

        date d;
        
volatile int v = 1212;
        
        v 
= d.hour_read;
        d.hour_write 
= v;
        
// v = d.hour_write;
        
// d.hour_read = v;

        v 
= d.minute;
        d.minute 
= v;

        v 
= d.second;
        d.second 
= v;

        
return 0;
}

 
这次首先重点show出来:
1. 0代价实现proxy
typedef int ASSERT_EQUAL_SIZE[sizeof(date)==sizeof(time_t)?1:-1];
 
2. 定义property的语法也还不算太坏吧?
使用一种"named template parameter"技巧,将read、writer、read-write合体。
hour_read, hour_write不用解释了。
second,minute需要解释一下:set和get出现在proxy的参数中出现的次序是随意的。
所谓named parameter是也。
 
不要拿hour_read用于写或者拿hour_write用于读,报出的错误可能很难读。
有空闲再处理报错好看问题吧……
 
 
 
proxy的其他技巧有请vczh同学介绍。
 
 
 
 
关于union中不能放非pod的问题……
1. 基于C-API来构造自己的类
那自然没什么问题。
如果还想进行访问控制,就像date::impl一样,包到一个private nested class中,并设置外部class为friend。
 
依然0代价。
 
 
2. 基于现有的非pod来构造自己的类
楼上已经提到了union base和union member两种方案。
不过property::proxy不支持这种用法,得手工实现……
遇到的问题也是楼上提到的"先鸡还是先蛋"……
为了解决鸡蛋问题,想了很久都没想出可以很清晰定义property的方案……
 
换种思路:使用boost.aligned_storage + placement new + 直接调用析构函数吧……
也算一种解决办法了…… 而且也是0代价的。
 
 
 
玩具代码,仅为展示上面的2个重点技术而已。实际应用还需自行编写。
property我以前也没用过…… 没什么经验,不知道通常会怎么使用。
 
测试编译器:msvc8、9、10,gcc3.4.x。
写命名模板参数就没打算支持vc6……  不测了……
gcc4.x懒得切换系统了…… 应该没什么问题……
 

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-18 04:43 by OwnWaterloo
为了避免vczh同学误会……
我再申明一下……

1. 这是彻底的玩具代码
所以对日期的合法性不作检查……

2. 代码还需在顶部加入两行:
#include <time.h>
#include "property.hpp"
才算完整

3. "property.hpp"
的搜索路径需要自己配置,或者就和date.cpp放一起。

4. 需要在<time.h>前加入:
#define _CRT_SECURE_NO_DEPRECATE
才能避免msvc上常见的4996警告。

5. 或者另一种方式,代码行数较多:
#ifdef _MSC_VER
#pragma warning(disable: 4996)
#endif

6. main的return 0可以省略
因为一定是C++代码,也不用兼容vc6



能想到的我都说了……
你就绕了我吧……

# re: GUI框架:谈谈框架,写写代码[未登录]  回复  更多评论   

2009-11-18 09:46 by Loaden
@ OwnWaterloo
@ vczh
(注:排名不分先后,按字母顺序)

你们两个都是超级牛人!!我看你们的争论时,更多的是自卑感!
因为你们讨论的很多东西我看不懂!!
唉!!
我把郁闷发在这里了,有空就过去给我签个名吧。
http://topic.csdn.net/u/20091117/19/54a04541-094f-4d7c-960e-c0ce34783821.html

两位不要伤了和气!技术问题,有争论总是好的!!
我就在旁边学习好了...

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-18 09:51 by 矩阵操作
@OwnWaterloo
好长的回帖,年度最强了吧?
看了一下,似乎
union
{
class
{
time_t time_;
};
....
};
就可以了吧?impl感觉多余了。当然我没有试,在上班,嘿。
好像前面OwnWaterloo提到过隐藏union中成员问题,
像上面那样写应该可以吧?


如果非要依赖标准cpp来实现某些东西,例如属性、委托,动则搞个几千行,那我就宁愿承认自己无能,用VC的扩展了,哈哈

# re: GUI框架:谈谈框架,写写代码[未登录]  回复  更多评论   

2009-11-18 09:54 by Loaden
@ OwnWaterloo
能否QQ、gmail、Live联系?有问题请教。
我的QQ:1090833
我的GMail、Live:loaden AT gmail or live dot com
之前在gmail是向vczh请教时,受益匪浅。
想结识牛人:不会啰嗦,只想在关键的疑惑上得到点拨。

@ cexer
非常期待你的GUI设计。
我现在倾向于使用thunk封装,但由于还碰到很多问题,没办法拿出来讨论。
非常期待能与你在QQ or GMail or Live上探讨GUI框架设计。
我目前实现的消息映射是这个样子:
[code]class MainFrm : public Frame
{
public:
MainFrm() : Frame(_T("Test Demo!"), _T("MainFrame")), m_testBtn(_T("Test!"))
{
Add(&m_testBtn);
m_testBtn.EvtLButtonDown.Bind(this, &MainFrm::OnLBtnClicked);
this->EvtCreate.Bind(this, &MainFrm::OnCreate);
}

~MainFrm()
{
this->EvtCreate.UnBind(this, &MainFrm::OnCreate);
}

qpEvt OnCreate(evt::Create& evt)
{
evt.handled = true;
qpDbgInt(evt.handled);
}

qpEvt OnLBtnClicked(evt::LButtonDown& evt)
{
qpDbgInt(evt.owner);
}

protected:
Button m_testBtn;
};[/code]

# re: GUI框架:谈谈框架,写写代码[未登录]  回复  更多评论   

2009-11-18 09:56 by Loaden
另,任何一个类如果要处理消息,需要这样继承:
class Test : public EvtReceiver<Test>
{
public:
Test();
~Test();

void UnBind();

private:
qpEvt OnCreate(evt::Create& evt);
};

然后这样注册:
Test::Test()
{
FrameEvt *sender = App::Module().GetEvtSender<FrameEvt>(_T("MainFrame"));
if (sender != NULL)
{
sender->EvtCreate.Bind(this, &Test::OnCreate);
}
}

Test::~Test()
{
}

void Test::UnBind()
{
FrameEvt *sender = App::Module().GetEvtSender<FrameEvt>(_T("MainFrame"));
if (sender != NULL)
{
sender->EvtCreate.UnBind(this, &Test::OnCreate);
}
}

qpEvt Test::OnCreate(evt::Create& evt)
{
qpDbgInt(evt.handled);
}

但其中:
FrameEvt *sender = App::Module().GetEvtSender<FrameEvt>(_T("MainFrame"));
是极度龌龊的!可是我找不到更多的办法了。

# re: GUI框架:谈谈框架,写写代码[未登录]  回复  更多评论   

2009-11-18 09:57 by Loaden
其中qpEvt的定义:void __stdcall
因为使用thunk,所以回调函数必须是__stdcall的调用约定。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-18 12:19 by OwnWaterloo
@矩阵操作
union里面好像不能出现private的东西。具体得查标准……

我那个,也是权宜之计……
客户代码访问不了这个类型:date::impl
但可以被推导……
template<typename T>
void set_default(T& v) { v = T(); }

date d;
set_default(d.impl_); // 挂了


不过,private也不是万能的……
指针算术,宏,都有可能让它失效。
取名为impl_, 应该能起到警示作用 —— 不要使用这个名字 —— 吧……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-18 12:38 by OwnWaterloo
@Loaden
对框架设计,我不熟…… 因为一般来说,我讨厌那个……

为什么?
框架和类库有什么区别?
类库是帮助你的,框架是限制你的……

作为一个框架的设计者,他必须对这个领域十分了解:哪些过程是常用的,哪些是有害的。
才能准确的做出限制。
我对gui编程经验不多,所以就不做这方面的活了……



说说win32下gui库/框架必须解决的一个问题吧: user_data,或者叫context。
因为那个万恶的WndProc没有传给你……

thunk是一种办法,一种i386(其他平台我不知道怎么实现)上通用的办法 —— 只要是因为回调设计不正确,没有带context参数,就可以用thunk解决。
实际上…… 我毕业设计就是做的这个……
__stdcall也不是必须的…… __cdecl也可以……
比如qsort, 它就是需要一个 __cdecl的。

这里有一部分代码:
http://code.google.com/p/callback-currying/

主要是做自由函数的thunk,而不是成员函数。
一方面是可以给C程序员用。
第2个原因是因为this和成员函数指针的可移植性很糟糕。
第3个原因是因为可以通过自由函数,转发到成员函数中,对成员函数的支持不是绝对必要的。

直接thunk成员函数有一个好处。我论文里面有提到。
返回一个大结构体的函数,直接thunk成员函数可以,thunk自由函数不行……

stdcall2stdcall 需要12字节
cdecl2cdecl 需要23字节
thiscall2stdcal(msvc) 需要10字节
其他记不清楚了


设计好thunk结构体,只是关键技术之一。
之后还有很多很多的麻烦…… 主要是DEP(数据执行保护)

在win32下可以用VirtualAlloc来分配可执行内存,posix下使用mmap。
但是…… 不可能为了这么小一块内存,就去分配1整页吧? win32下还要保留64k……
内存池是必须做的…… 而且现有内存池都无法被复用

所以,那个gcode后来就没有维护了。
svn管理实验代码很麻烦…… 转git了……

最后的版本还没有敲定,等过年后我会发一个版本出来……

named template parameter就是为了解决callback currying中模板参数混乱(不仅是多,真的是混乱,论文里面好像有详细解释)而设计的。



跑题了…… 上面的中心思想是: thunk是一种通用技术,但麻烦多多。
如果仅仅是为了解决WndProc, 有专有技术。
Set/GetWindowLongPtr
还有前几天学到的…… Set/GetProp
见这里:http://www.cppblog.com/kyelin/archive/2009/11/13/100880.aspx

需要注意的是,Set/GetWindowLongPtr网上很多文章都说错了。
具体的使用方式去查msdn。
cbExtra是一个0 based array —— 我记得msdn是有这么一句话的。

GWL_USERDATA是另外一回事。


用这2种方案之一就可以了(或者win32还提供了其他附加数据的方案?)。
mfc的线程局部映射表…… 哎……

# re: GUI框架:谈谈框架,写写代码[未登录]  回复  更多评论   

2009-11-18 13:36 by Loaden
@ OwnWaterloo
因为要兼容x64平台,所以统一使用__stdcall。
原因是x64下只有__stdcall。

DEP不是问题,很容易解决:
void* operator new(size_t size)
{
return ::VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
}

void operator delete(void* p)
{
::VirtualFree(p, 0, MEM_RELEASE);
}

而:
Set/GetWindowLongPtr
还有前几天学到的…… Set/GetProp
由于使用static函数,效率是一个大问题,而且有潜在的危险性。

谢谢你分享:callback-currying

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-18 13:42 by OwnWaterloo
补充一下: 关于DEP ,做thunk可能会用到。

win32可以用win32 堆(HeapAlloc那套),创建时可以指定内存权限。
这几乎是最省心的做法了。

不过我想做posix移植(论文啊…… thunk已经烂大街,不拿点新东西出来怎么过得去……)
所以就要自己实现 ……
做着做着嘛…… 发现内存池可以再分出几部分……

比如其中的一部分是intrusive data structure。
小对象内存池其实只是intrusive date structure的使用方式之一。

然后打算重构,然后…… 就毕业了…… 然后就无限延期了……

过年之后再做吧……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-18 13:44 by OwnWaterloo
@Loaden
兄弟,你知道这会分配多少吗?

DEP不是问题,很容易解决:
void* operator new(size_t size)
{
return ::VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
}


这里用new placement也不太妥当。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-18 13:54 by OwnWaterloo
@Loaden

Set/GetWindowLongPtr
还有前几天学到的…… Set/GetProp
由于使用static函数,效率是一个大问题,而且有潜在的危险性。


static 函数怎么影响效率了? 多传递了一次参数?
有缺陷的回调肯定是C的,参数中肯定都是bitwise-copy。
所以,还好吧……

其实你需要的就是一个context而已。
thunk, Set/Get WindowLongPtr/Prop 都是设置/得到context的手段。

thunk的效率不见得比Set/Get WindowLongPtr高,而且用着很麻烦。

WndProc(HWND hwnd,UINT msg,WPARAM w,LPARAM l)
{

CWindow* w = (CWindow*)GetWindowLongPtr( ... );
// 编译器知道这个调用处的上下文,可以进行优化
return w->handle(hwnd, msg, w, l );

}


thunk的话,要么每个窗口对应一个窗口类,该窗口类对应一个thunk。
要么若干窗口对应同一个窗口类,每个窗口再
SetWindowLongPtr(hwnd, GWL_PROC , ) 去改窗口处理过程。

调用一个chunk是绝对得不到优化的,因为编译器根本就不可能知道thunk的目的地在哪。

# re: GUI框架:谈谈框架,写写代码[未登录]  回复  更多评论   

2009-11-18 14:10 by Loaden
@ OwnWaterloo
CWindow* w = (CWindow*)GetWindowLongPtr( ... );
这个调用不仅仅是返回一个值而已。
而且不同的窗口类,要返回不同的值。
所以,这里编译器如何优化呢?
另外,static函数、全局函数也不利于C++封装。
而thunk,从其汇编代码可以看到,代价只是多花1~3条CPU指令而已。
显然要高效。

DEP的问题,我想过内存池来实现。
不过由于技术不过硬,暂时还没想过去实现它。

先把GUI框架稳定下来再说。
谢谢你的指点!如果有好的解决thunk的内存池(ATL就是这么干的,只是不开源),请介绍一个给我。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-18 14:31 by OwnWaterloo
@Loaden
忘写了…… 联系方式,同名gmail邮箱……


CWindow* w = (CWindow*)GetWindowLongPtr( ... );
这个调用不仅仅是返回一个值而已。
而且不同的窗口类,要返回不同的值。

你的意思应该是,不同窗口“实例”,返回不同的值,是这样吗?
那就对了,GetWindowLongPtr( hwnd, ... );本来就是不同窗口实例返回不同的值嘛……



所以,这里编译器如何优化呢?

我说的优化是它下面的一个调用。
w->handle( ... ); // 可能会被inline,避免参数传递。

因为编译器知道这里肯定调用的是handle,不会是其他函数。

但thunk,调用的地址是运行时的,所以编译器无法优化。



DEP的问题,我想过内存池来实现。
不过由于技术不过硬,暂时还没想过去实现它。

先把GUI框架稳定下来再说。
谢谢你的指点!如果有好的解决thunk的内存池(ATL就是这么干的,只是不开源),请介绍一个给我。

其实 …… atl的源代码是能看到的…… 至少thunk部分是这样。
我上面有一个回帖, 估计你看掉了。
如果只想支持win32, 可以用HeapAlloc那套。
一个版本中的atl就是这么干的,还有个版本是自己实现的池 —— 具体记不太清楚了。


如果想在posix平台下,得到分配可执行内存的高效的池 ……
等过年吧,哈哈哈



而thunk,从其汇编代码可以看到,代价只是多花1~3条CPU指令而已。
显然要高效。

你看到thunk的分配了吗? 考虑到thunk的复杂性了吗?
要全局的看~_~
cbExtra可以被实现得很高效。当然,我只是猜……



另外,static函数、全局函数也不利于C++封装。

free-function才是范化形式……
member-function只是free-function的一个特例。
是否封装,是看你是否仅通过被数据认可的操作去使用数据,而不是自己去拨弄数据。
跟语法无关。 语法只是个便利。

这个话题…… 估计要扯很远了……
你不接受很正常,我可以理解……
我只是提一下……

# re: GUI框架:谈谈框架,写写代码[未登录]  回复  更多评论   

2009-11-18 14:49 by Loaden
因为编译器知道这里肯定调用的是handle,不会是其他函数。
但thunk,调用的地址是运行时的,所以编译器无法优化。
-----
thunk不用调用,直接处理消息后,调用默认窗口过程。
比如:
LRESULT CALLBACK Frame::WndProc(UINT msg, WPARAM wpa, LPARAM lpa)
{
LRESULT res = 0;
bool ret = ProcessMessage(msg, wpa, lpa, res);
if (ret) return res;

if (msg == WM_SIZE && m_layout != NULL)
{
m_layout->DoLayout(0.0f, 0.0f, static_cast<float>(X_LPARAM(lpa)),
static_cast<float>(Y_LPARAM(lpa)), true);
}

switch (msg)
{
EVTCREATE(m_wnd, wpa, lpa);
}

return ::CallWindowProc(m_defWndProc, m_wnd, msg, wpa, lpa);
}

static函数...
-----
可能是个人喜好吧。
我喜欢尽最大可能的 不 在C++中使用全局函数和静态函数。
我喜欢将什么东西都放在类里。
哪怕是无法实例化的静态类。

我对你在前面给的“低代价”例子非常感兴趣,下午准备好好学习你的例子。
谢谢!!

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-18 15:05 by OwnWaterloo
@Loaden

thunk不用调用,直接处理消息后,调用默认窗口过程。

肯定要调用的……
不然怎么处理消息?

只是我没看出你的代码中哪句是调用……


我喜欢将什么东西都放在类里。

最大的毛病:这会将优秀的算法埋葬到类中。


强调一下,是"零"代价,不是低代价~_~
union base, union member是低代价的。

# re: GUI框架:谈谈框架,写写代码[未登录]  回复  更多评论   

2009-11-18 15:37 by Loaden
@ OwnWaterloo

肯定要调用的……
不然怎么处理消息?
-----

消息处理在这个宏里。
写宏的原因是:不同消息处理类,可以通用。

// WM_CREATE
#define EVTCREATE(wnd, wpa, lpa) \
case WM_CREATE: \
{ \
evt::Create evt = {wnd, this, reinterpret_cast<LPCREATESTRUCT>(lpa), false}; \
EvtCreate(evt); \
} \
return 0;
struct Create
{
HWND wnd;
Object* owner;
LPCREATESTRUCT createStruct;
bool handled;
};

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-18 15:45 by OwnWaterloo
果然没有错误处理是邪恶的……

经过Loaden的强力测试(其实也就随手一测),崩了……

先上代码:
time_t t1 = time(0);
tm t2 = *localtime(&t1);
t2.tm_hour = 348294832;
t1 = mktime(&t2);
printf("%d\n",(int)t1);
tm* pt3 = localtime(&t1);
printf("%p\n",(void*)pt3);

t2.tm_hour = 348294832; // 这里对于32位的int来说,没有溢出。
但hour的"权值"比较重,最后转化为time_t的表达可能会溢出。


t1 = mktime(&t2);
printf("%d\n",(int)t1);
C89要求:mktime如果不能将tm转换到正确的time_t格式,就返回(time_t)-1。

而vc8的运行库不是这么处理的,它会直接崩掉,而不是返回-1。
触发的断言是:
loctime64.c (78)
(*ptime <= _MAX_TIME64_T)

所以,在vc8下,mktime那行代码就会崩。
在vc9、mingw3.4.x下,返回-1。


tm* pt3 = localtime(&t1);
printf("%p\n",(void*)pt3);
如果time_t不是正确格式,C89好像没说应该怎么做……
vc8、vc9、mingw3.4.x一致返回空指针。


再看date的代码:

int get_(int tm::*field) const
{

return localtime(&impl_.time_)->*field;
//当time_t不正确时,返回空指针,然后被解引用,崩。

}

date& set_(int tm::*field,int v)
{

tm t = *localtime(&impl_.time_);
t.*field = v;
impl_.time_ = mktime(&t);
// 当t格式不正确时,vc8不会返回-1,而是直接崩。
return *this;

}


错误处理还是需要的……
不过这仅仅是示例代码,还是不加了吧……
以免影响清晰…… 突出重点……

感谢Loaden的测试~_~

# re: GUI框架:谈谈框架,写写代码[未登录]  回复  更多评论   

2009-11-18 15:56 by Loaden
@ OwnWaterloo
纠正一下,是随手 三 测 ^)^
第三次改输出代码时崩溃的。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-18 15:59 by OwnWaterloo
@Loaden

#define EVTCREATE(wnd, wpa, lpa) \
case WM_CREATE: \
{ \
evt::Create evt = {wnd, this, reinterpret_cast<LPCREATESTRUCT>(lpa), false}; \
...

this是怎么得到的?


LRESULT CALLBACK Frame::WndProc(UINT msg, WPARAM wpa, LPARAM lpa)
{
...

这是静态还是非静态的? 函数定义看不出来,只有声明才行。
估计也是非静态的吧? 因为有m_wnd这些东西。
那么,this是怎么得到的??



追根究底,要从【注册窗口类】时给它的【WndProc】查起。
如果你传递给它的不是一个真正的函数,而是一个thunk,这个thunk就会被调用。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-18 16:00 by OwnWaterloo
@Loaden

纠正一下,是随手 三 测 ^)^
第三次改输出代码时崩溃的。


随手1测就错了,非常不靠谱……
随手3测就错了,稍微靠谱一点点……
反正代码很不靠谱的,免责声明我也写了的……
出了任何问题我不负责哦~_~

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-18 16:06 by 陈梓瀚(vczh)
不容易啊,变成水炉跟罗登在聊了……咱才走开了一个上午……工作啊……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-18 16:07 by 陈梓瀚(vczh)
@OwnWaterloo
我就继续做我的combinator去了,这是一个专门对付上下文无关语法分析的库,可以在C++里面写文法,绑定functor和error-recover来做错误恢复或者输出更加贴心的错误信息等等。完了会搞一个小demo,也是有用的,造一套比宏顺眼的代码生成器,就用combinator做……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-18 16:08 by OwnWaterloo
@陈梓瀚(vczh)
我就一闲人……

我和罗登在gtalk上聊得更欢……
要不要来插一脚?

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-19 00:51 by 空明流转
@陈梓瀚(vczh)
唉,你们这版聊的。。。我现在还是用Python做Code Generator,这样省心。。。不过就是测试起来很麻烦。。。。

# re: GUI框架:谈谈框架,写写代码[未登录]  回复  更多评论   

2009-11-19 09:07 by Loaden
@ vczh
我的网名叫老邓(非罗登^_^),曾经在Live上向你请教过GUI框架设计。

@ OwnWaterloo


evt::Create evt = {wnd, this, reinterpret_cast<LPCREATESTRUCT>(lpa), false}; \
...
this是怎么得到的?


这是一个宏替换。宏所在的类的this就是这里的this。


LRESULT CALLBACK Frame::WndProc(UINT msg, WPARAM wpa, LPARAM lpa)
{
...
估计也是非静态的吧? 因为有m_wnd这些东西。
那么,this是怎么得到的??

非静态。但只有三个参数,第一个参数通过汇编,写到了m_wnd成员变量上了。没办法:兼容x64平台,只能这样thunk。

另,昨天你教了我一下午,晚上我受到了启发,想到了一个利用bitset的方法来降低成本。
由于是动态注册,所以运行成本增加了。

f.Get<EvtCreate>(Frame::EvtCreate)->Bind(this, &A::test);

而且,看起来不舒服。
所以,今天准备结合你的union设计,把Get模板函数搞成属性来访问。
如果Demo通过的话,我发gmail给你。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-19 17:48 by cexer
@Loaden
thunk 完成的功能只是消息从系统到窗口类的很小一步,也是最需要完成的第一步。在这一步上我尝试过很多方法,包括这种 thunk 技术,甚至还用过TLS做映射。我目前用的方法很简单就是 SetProp,GetProp 两个函数,有点重剑换木剑的感觉。效率上肯定没有 thunk 的直接调用高,但是心里更踏实一点,在内存当中写二进制代码总有在犯罪的感觉。

“过早的优化是一切罪恶的根源”。基于这一步只是整个GUI框架当中是很微小的一步,几乎不影响框架设计,可以先把框架搭好,再来从安全,效率,可移值性各个方面进行考虑。反正不要选择 GetWindowLong ,GWLP_USERDATA 那一套,如果发布出去后客户也使用这个东西就一切全完了,“过时的悲观毫无益处”。

你的消息封装看起来很舒服,肯定在 GUI 框架上也是下过很多功夫,喜欢重复制造车轮的的同志,我对这个兴趣也比较大,希望以后能多与你多交流互相学习,革命路上并肩携手一同进步!

# re: GUI框架:谈谈框架,写写代码[未登录]  回复  更多评论   

2009-11-19 17:50 by megax
我认为WTL是目前最LITE最高效,简洁易懂,易于使用和维护的基于windows的GUI框架,其它的要么就是技巧用的太多,要么就是不直观。要么就是效率不好。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-19 17:56 by OwnWaterloo
@Loaden

非静态。但只有三个参数,第一个参数通过汇编,写到了m_wnd成员变量上了。没办法:兼容x64平台,只能这样thunk。

你用的是和atl一样的方式?
将hwnd"替换"为this? 而非"插入"一个this?
是吗?

1~3cpu指令只是"调整栈"而已,调整完了,肯定会继续执行thunk中的call,来到你的函数。
这个call或者jmp是免不到的。
同时,编译器得不到thunk的目的地的信息,所以不可能进行任何优化。
本来也没什么优化的余地了,都机器码了……


x64的我也有点想做……
但对x64上的汇编和调用约定不熟悉,而且也没有x64的测试平台……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-19 18:07 by OwnWaterloo
@cexer

thunk 完成的功能只是消息从系统到窗口类的很小一步,也是最需要完成的第一步。在这一步上我尝试过很多方法,包括这种 thunk 技术,甚至还用过TLS做映射。我目前用的方法很简单就是 SetProp,GetProp 两个函数,有点重剑换木剑的感觉。效率上肯定没有 thunk 的直接调用高,但是心里更踏实一点,在内存当中写二进制代码总有在犯罪的感觉。

“过早的优化是一切罪恶的根源”。基于这一步只是整个GUI框架当中是很微小的一步,几乎不影响框架设计,可以先把框架搭好,再来从安全,效率,可移值性各个方面进行考虑。反正不要选择 GetWindowLong ,GWLP_USERDATA 那一套,如果发布出去后客户也使用这个东西就一切全完了,“过时的悲观毫无益处”。


我就知道……
大多数人对GetWindowLongPtr/GWL_USERDATA的理解是有误的。
你先仔细看看msdn是怎么介绍cbExtra、GetWindowLongPtr,还有GWL_USERDATA再考虑要不要否定这套方案。


线程局部存储映射表?
先不说效率,mfc不能在线程之间传递CWnd*,就是一个使用上的劣势。


mfc需要在文档中记录: 不能将CWnd*传递到另一个线程。
使用SetProp需要在文档中记录:不要SetProp("cexer" , ...)
使用GetWindowLongPtr需要在文档中记录:不要SetWindowLongPtr( hwnd, 0, ... );


3种方案有区别吗?文档中都必须有记录 —— 什么是库使用的,用户不能随意使用 —— 没一个逃得掉。
可能只有thunk,不需要在文档中特别说明什么,比如ATL/WTL。

区别还是有的,效率。
tss-table肯定是最低,编程也最复杂的,完全不值得使用。
SetProp需要查找,效率肯定没有另外两种高,而且,它很可能也需要分配内存。

thunk和GetWindowLongPtr旗鼓相当。很难说谁高谁低。
但后者要简单很多,而且是在所有Windows上都可以使用,不用考虑汇编的问题。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-19 21:50 by cexer
@OwnWaterloo
就像你把火箭的点火系统装到拖拉机上,轰轰烈烈地点燃了拖拉机,它还是不能以宇宙第一速度脱离地球获得自由一样。Windows 系统维护消息队列的时间远远多于消息到窗口那一弹指一挥间,我们再努力地挤时间出来也只是寻求心理上的安慰。所以在写 GUI 框架时我尽量避免的是空间消耗而不是时间。

不过最佩服C++程序员们的一大优点就是,就算只有一点效率也是要打破脑袋挤出来的,有总比没有好嘛。你们讨论的我都认真看到,在效率和安全上 thunk 确实是很明显的最佳选择,我在考虑以后用这个了。在利益的诱惑下,心里的负罪感是可以克服的!

还是不敢选 GetWinowLongPtr 。它的好用是世人皆知的,就像一个大美女,人人都想染指一把,很难说在哪个月黑风高的晚上,哪个不爱看文档的程序员就忍不住用了它,于是崩溃蓝屏,整个世界清静了。。。这样的事一定会发生的,因为不爱看文档的人太多了。

SetProp 虽然也有危险,但是那概率小得多。世上的 Windows 程序员有多少,这些 Windows 程序员当中写 GUI 的又有多少,写 GUI 的程序员们直接用 API 写的又有多少,直接用 API 写的用到 SetProp 的又有多少,用到 SetProp 偏偏又用到和我一样参数的又有多少呢。我感觉遇到这样的概率比走路时被不明飞行物砸中的概率还要小。

当然作为一个库的作者,不能把完全安全性交给概率去解决。不像 GetWindowLongPtr 你只能眼睁等着崩溃,用 SetProp 为了增大安全性可以有很多手段。可以用一个 GUID 来 SetProp( guid .... ) ,可以用圆周率3.14159265。。。。,甚至可以用对女朋友的爱情宣言全文,爱是独一无二的嘛!

MFC的窗口指针可以在线程间传递,只是除了 SendMessage 之外能干的事不多。这跟那个 TSS 表没多大关系,GUI 很多 API 本身就是不能跨线程。真正不能在线程间传递的是 TSS 表本身,我们用 TSS 的目的就是避免线程间的牵扯,当然是不会在线程间传来传去的,而且这个表对 GUI 框架的客户而言是看不到的,想传也传不了。TSS 映射表的速度也不如想像中的慢,一个再巨型的 GUI 软件,能有多少窗口可供映射的呢。

这些方法我都用过。权衡起来,还是觉得 SetProp 是最简单好用的一个,有兴趣的同志可以测试一下在 SetProp 和 thunk 的实现效率差别有多大。我个人觉得在消息队列吊车尾的情况下,差别不大。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-19 22:07 by OwnWaterloo
@cexer
如果不考虑效率,最好的办法就是不用C++做gui了。
开发又慢又容易出错,何必呢? 图什么?


GetWindowLongPtr的问题真的很容易解决。
只要程序员用这个函数之前,看过GetWindowLongPtr的文档。
如果一个库的用户绕过库并且不看文档而直接使用win32api,库是怎么都做不到,也花太多心思防止他用错的。

他甚至可以不用看这个gui库的文档 —— 因为你可以把this放在array的最后,而不是最前。
甚至还可以在this前后设置一些guard来检测使用库的人员是否出轨。
并且,如果将this放在最后,正好也可能需要一些padding来对齐。



SetProp和thunk的效率就不用比了……
同样要分配内存。
thunk是直接调用,SetProp调用之后还有活要干……
当然,效率不是最主要的…… 也不是可以完全不顾的……


我觉得将this放在GetWindowLongPtr所在array最后,既安全又快速又简便……


当然,最终权衡在你。
而且…… 只要这个方式不对用户公开……
随时可以改……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-19 22:18 by OwnWaterloo
@cexer
void f( CWnd* w)
{

CWnd* d = w->GetItem( ... );
d->GetWindowTitle( ... );

}

具体是不是叫这个名字不记得了。
由一个窗口得到上面的控件的窗口,然后取一下标题。
如果w来自另一个线程 …… 等着程序挂吧……


d是一个指针,有主动释放吗?
mfc的消息循环还会在odle时清除这种"临时对象"的指针。
就为了满足它的table机制,需要在动态内存中创建对象,并使用类似gc的机制去清理……
mfc的大、慢就是这么一点一点的积累出来的。
table这种方式绝不可取……


这个例子只是举例,可能并不正确,因为我很久都不用mfc了……
太out……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-19 22:38 by cexer
@OwnWaterloo
我可没说不考虑效率哈。别说C++,就算用石头写程序,都得考虑效率的。只是效率不是影响一切的标准,特别是当效率的差别微乎其微的时候。我不是不考虑效率,而是觉得用 thunk 实现 GUI 框架的效率不一定就能高多少,因为真正吊车尾的是消息队列, SetProp 和 thunk 这点微末的时间,相对系统得到事件,生成消息,翻译消息,发送消息,排队消息的这一大堆的内部操作的时间来说,短得不值一提。GUI 线程只是用来跑 GUI 的,不能用 GUI 线程来完成分步式计算啊。

用 GetWindowLongPtr 来作为实现框架确实也很简单,效率也最高。但因为GWL_USERDATAR 的众所周知而又独一无二,这个问题是很严重的,而且很明显,OwnWaterloo兄啊,何苦这么执着地为它辩护。。。。要是在广告中说道,“此 GUI 框架物美价廉童叟无欺,但请不要使用 GWL_USERDATA 因为本框架要使用”,怎么卖得出去啊。

三种方式当中 thunk 和 SetProp 确实是前者优一点,我个人放弃效率而选择标准一点的实现。至于 GetWindowLongPtr ,现在你就算用左轮手枪指着我的脑袋,我也还是坚持不能用的哈 。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-19 22:48 by cexer
@OwnWaterloo
你说的:“
oid f( CWnd* w)
{
CWnd* d = w->GetItem( ... );
d->GetWindowTitle( ... );
}
具体是不是叫这个名字不记得了。
由一个窗口得到上面的控件的窗口,然后取一下标题。
如果w来自另一个线程 …… 等着程序挂吧……
d是一个指针,有主动释放吗?
mfc的消息循环还会在odle时清除这种"临时对象"的指针。
就为了满足它的table机制,需要在动态内存中创建对象,并使用类似gc的机制去清理……
mfc的大、慢就是这么一点一点的积累出来的。
table这种方式绝不可取……


你说的这个是 MFC 的实现嘛,MFC 还在系统中加了钩子呢。它的这个指针之所以有诸如线程的限制,也是因为实现的功能太杂。我们的 TSS 表可是压根没想过要实现这么个“临时指针”的功能。关于跨线程调用,应该是 API 是怎么样,跨线程调用成员函数也应该能干啥。TSS 表只是消息到窗口类当中很小的一步,不该影响到窗口类本身的工作。所以函数调用跟 TSS 表一点关系都没有的,如果一个函数调用因为 TSS 表而崩溃,那就是有问题的实现了。以前用过这种方式实现的,正是考虑到多线程才使用 TSS。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-19 22:57 by OwnWaterloo
@cexer
我一直都在强调……
GetWindowLongPtr, GWL_USERDATA是普遍被误解了的……


我从学Windows开始…… 就把这个用错了……
因为我的老师给我们的课件上用错了……
网络上【我没见过用对的文章】,也全都用错了:
p = (Window*)GetWindowLongPtr( hwnd , GWL_USERDATA );



不是这样的……
GWL_USERDATA有其本身的含义(在对话框中)。
应该怎么用还是怎么用。
库是不需要占用它的……


GetWindowLongPtr返回的是一个array中的元素。
1.
它可以是一个以0开始的,大小为cbExtra / sizeof(void*)的数组。
2.
或者是以0开始的,大小为cbExtra / sizeof(long) —— 如果以GetWindowLong取得。
3.
或者是一些系统定义的index, 比如GWL_USERDATA, GWL_WNDPROC —— 这些系统定义的index全是负数。

我上面写的代码全是:
GetWindowLongPtr( hwnd, 0 );
或者:
GetWindowLongPtr( hwnd, index );
而不是:
GetWindowLongPtr( hwnd, GWL_USERDATA );是错的 —— 对于塞this来说的话 —— 确实会占用掉用可以留给用户的机制。


库只要将注册窗口类的过程截获,并且给cbExtra增加一点点 —— padding + sizeof(this) + guard。

库的用户还是可以自由使用GetWindowLongPtr, 只要他没有越轨 —— 不超过他填入的cbExtra。
用户要越轨,那就责任自负……


内存分配:
cbExtra应该是和window一起被分配,不需要专门分配thunk结构体或者SetProp用的数据。
速度:
直接通过index寻址array,也几乎是最快的查找方式。


所以我说从整体上thunk和GetWindowLongPtr的效率可能差不多。
thunk如果one window one WNDCLASS,会注册很多WNDCLASS。
如果多个window共享一个WNDCLASS,就需要转发一次。
这些都是有效率损耗的。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-19 23:03 by OwnWaterloo
@cexer
我没有细想过 HWND -> this的映射的方案……
因为它一开始就在我排除的范围内……

——【一个设计缺陷不应该由另一个设计缺陷来弥补】。

幸好,最后我发现WndProc(hwnd, ... )还不算彻底的设计缺陷。
因为hwnd可以有cbExtra。


我再想想table(hwnd,this)是否一定需要mfc那样的机制……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-19 23:04 by cexer
@OwnWaterloo
你说:“我一直都在强调……
GetWindowLongPtr, GWL_USERDATA是普遍被误解了的……”

哈哈,原来如此。看来我对 GetWindowLongPtr 真是有深深的误解!和你一番讨论挺有收获的,这样看来我得再多权衡一下了!这样的 GetWindowLongPtr 是一个诱人的选择。看来不看书,只听道听途说来的东西真是不行的。可以回头去睡觉了,多谢了!

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-19 23:12 by OwnWaterloo
@cexer
哈哈,性感诱人吧? 所以我极力推荐这妞……
也正因为误解很严重……
所以用户代码访问 [0, length ) 的几率还真不大……


抄一下msdn……

The GetWindowLongPtr function retrieves information about the specified window. 【The function also retrieves the value at a specified offset into the extra window memory.】

LONG_PTR GetWindowLongPtr
(
HWND hWnd,
int nIndex
);

nIndex
[in] Specifies the zero-based offset to the value to be retrieved. 【Valid values are in the range zero through the number of bytes of extra window memory, minus the size of an integer.】
To retrieve any other value, specify one of the following values.
... 后面就是其他很多系统定义index了。


GetWindowLongPtr的另一个作用 —— 0-based array —— 只有这么2句话……
还隐藏在对系统自定义index的大篇幅的介绍之中……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-19 23:14 by OwnWaterloo
还有一句:

Remarks
Reserve extra window memory by specifying a nonzero value in the cbWndExtra member of the WNDCLASSEX structure used with the RegisterClassEx function.


原来叫cbWndExtra…… 不叫cbExtra……
很有没用…… 写错了……
也对,因为相对的还有cbClassExtra……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-19 23:21 by cexer
@OwnWaterloo
你说:“性感诱人吧? 所以我极力推荐这妞……
也正因为误解很严重……
所以用户代码访问 [0, length ) 的几率还真不大……”

确实非常性感诱惑人,我们不要宣扬了,好东西我们两知道就好了,哈哈!MSDN真是有点猥琐。。。实现这么好个功能,却不知道大书特书,让那 GWL_USERDATA 忽悠了不知道多少程序员。OwnWaterloo 兄弟,该睡觉了,身体是革命的本钱啊,再聊。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-20 00:13 by OwnWaterloo
@cexer
确实有点猥琐……

没这功能,WndProc的设计真的就是缺陷了……
有这功能,WndProc还是很麻烦……


我刚吃完晚饭…… 囧……
习惯没养好…… 夜猫惯了…… 慢慢改吧……
可能哪天不写代码了,生活会规律一些……


那个array的index到底是怎样,msdn我没看怎么明白……
待会验证一下……

tss + table是否可以调用成员函数, 我觉得有困难……
我再想想……
毕竟,要证明很容易 —— 想出一个实现。
要证伪 —— 证明不可能实现出来 —— 要困难不少……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-20 00:44 by OwnWaterloo
我说一下思路吧,不算证明 tss+table不行,只是说一下这个方案可能遇到的陷阱。

假设这就是注册窗口类时使用的函数:
LRESULT CALLBACK proc(HWND hwnd, UINT msg, WPARAM, LPARAM )
{

if ( msg == WM_XXX ) // 处理某条消息时需要得到this
{
table = get_tss_table(); // 线程相关映射表
w = table.lookup( hwnd ); // 查找hwnd对应的object。
w->f() // 调用一个成员函数...
}

}

如果另一个成员函数:
C::g()
{

SendMessage( this->m_hwnd , WM_XXX , ... );
// 而且这个消息、WM_XXX、在proc处理过程中需要取得this

}

那这个成员函数g,就不能在另一个线程中使用。
因为table = get_tss_table();是线程相关的。


是否会有这种成员函数…… 就不知道了…… 没这方面的经验,也想不出例子……



mfc返回一个CWnd* ,可能也是基于这种考虑:
CWnd* w = father->getChild (...);
w->g(); // 这里可能需要查表。

mfc中也可以这样:
HWND hwnd = ::getChind( father->m_hwnd, ... );
CWnd w;
w.Attach( hwnd ); // 插入表
w.g(); // 可查询
w.Detach(); // 从表中删除

可能afx考虑到最后的w.Detach会被忘记调用。一旦w离开作用域,那个hwnd -> object的表,就指向一个无效对象。

所以就引入了一个类似gc的方式…… 返回CWnd* 同时插入表中。

纯猜测……


# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-20 01:53 by OwnWaterloo
关于GetWindowLongPtr。


1. 系统定义index
有配套的GWLP_xxx系列…… 以前真没注意……
在32位上GWL_xxx和GWLP_xxx的值可能相同…… 但还是应该使用配套的……
自从我用Get/SetWindowLongPtr开始,就没写对一次过……
囧……

幸好cexer的某个回复中有GWLP_USERDATA……
顺手再查了一下……


2. array index
array就是一个byte的array。
这个array的地址是不会返回给用户的 —— 应该如此。

大致应该是如下伪代码:
LONG_PTR GetWindowLongPtr( hwnd , index )
{

char* array = get_extra(hwnd);
LONG_PTR r;
if ( index + sizeof(r) > get_extra_size(hwnd) )
{
SetLastError(ERROR_INVALID_INDEX); // 1413
return 0;
}

memcpy(&r , array+index , sizeof(r) );
return r;

}

越界会返回0,LastError是无效索引,1413。

有效索引应该是:
GetWindowLong, [0, cbWndExtra - sizeof(LONG) ], msdn说-4,也算对。
GetWindowLongPtr, [0, cbWndExtra - sizeof(LONG_PTR) ] , msdn说-sizeof integer, 这就是我困惑的地方……
integer是啥? short算不算…… 应该是sizeof(LONG_PTR)才对……


对齐用户(winapi的用户)也不需要特别关心。


反正array的地址是不可能暴露出来的,上面有些地方可能说错了。

SetWindowLong/Ptr也是会检查无效索引。


3. 创建窗口时收到的消息:

测试时顺便得到的:
WM_GETMINMAXINFO = 0x24;
WM_NCCREATE = 0x81;
WM_NCCALCSIZE = 0x83;
WM_CREATE = 0x1;

可能还和窗口风格什么的有关,以上不准,以msdn为准。


4. GetWindowLongPtr的其他限制
cbWndExtra在对话框中有一些规定。
MDI也有一些特殊规定 —— 针对CreateWindow是传入context。
可能还有一些特殊情况……

总之…… win32 gui是很浩瀚的事情…………
cexer…… 精神上支持你……


# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-20 10:00 by cexer
@OwnWaterloo
你说“那这个成员函数g,就不能在另一个线程中使用。因为table = get_tss_table();是线程相关的。”
首先用表肯定是可以的,比如说我们一个全局表。针对映射表的操作有三种,一种窗口创建时的插入,一种窗口销毁时的删除,还有就是窗口消息来时的查询。创建和销毁是在调用的 GUI 线程当中进行的,做不到跨线程的创建和销毁。而查询时是在 WNDPROC 中进行的,这个函数是由系统调用,也是在 GUI 线程中运行的。既然所有的操作都在同一个 GUI 线程,那何必要用全局表呢,还要加锁,这不正是 TSS 派上用场的地方嘛。

你说:“GetWindowLongPtr的其他限制”
GetWindowLongPtr 确实有比较大的限制。除了你所说的,对话框,按钮这类的已经注册过的窗口类,其 cbWndExtra 都已经是固定的,要想使用 GetWindowLongPtr 来存取额外数据的话,就必须要超类化,这样的就又麻烦了。所以综合考虑,SetProp 和 thunk 是最优选择。

你说:“总之…… win32 gui是很浩瀚的事情…………“
当然是的,不浩翰就没有辟波斩浪的快感嘛。OwnWaterloo 晚上两点还坚持在前线?深更半夜的,都在研究些什么高深课题呢?多好的睡觉时间啊,晚上睡得香,白天不嗑睡,早起早睡效率才更高!

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-20 14:47 by OwnWaterloo
@cexer
关于tss+table, 我理解错了……

SendMessage( hwnd , .... );
如果调用SendMessage所属线程T1和hwnd所属线程T2不是同一个线程,则依然是由T2,而不是T1去执行hwnd的wndproc。
我想成T1去了……

哦,还有个GetWindowThreadProcessId……
如果tss真有问题,可以不用那么自动的tss,手动+GetWindowThreadProcessId也行。

那mfc又是怎么挂掉的呢……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-20 15:06 by OwnWaterloo
@cexer

GetWindowLongPtr 确实有比较大的限制。除了你所说的,对话框,按钮这类的已经注册过的窗口类,其 cbWndExtra 都已经是固定的,要想使用 GetWindowLongPtr 来存取额外数据的话,就必须要超类化,这样的就又麻烦了。所以综合考虑,SetProp 和 thunk 是最优选择。


已经注册过的窗口类,如果要塞context,都需要subclassing。
SetWindowLongPtr( hwnd, GWLP_WNDPROC, insert_context );
然后在insert_context中将context塞进去,SetProp或thunk。
只是GetWindowLongPtr塞不了。
对窗口类注册不由自己控制的情况,GetWindowLongPtr确实不适合。


对这些预定义的窗口类需要做一些什么工作,有什么需求,我没怎么细想……
用win32 api写gui的事,我没做几回……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-20 15:08 by OwnWaterloo
@cexer
白天思维很乱…… 晚上思维清晰一些。
在想C++中怎么弄出匿名函数…… 无果……

如果没有这种东西, gui的语法是不会漂亮的……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-20 15:16 by OwnWaterloo
也不算匿名函数吧。
就是,如果一个调用需要传入一个函数作为回调,如何在调用点上创建这个回调函数,而不是在调用点所在函数外。

void handle_xxx( callback );

void f()
{


handle_xxx
( /* 1. 如何就地创建一个函数,传递给handle_xxx */
{ ... }
/* 估计C++0x才行 */
);


/* 2. 或者次一点,再handle_xxx的调用点所在函数中创建 */
my_handler () { ... }
handle_xxx( my_handler );

}


/* 3.最次的,就是在调用处所在函数外定义 */
my_handler() { ... }
void f()
{

handle_xxx( my_handler );

}

C++可以在函数内创建一个嵌套class, 并定义该class的函数 —— 可以是static的,但就是不能直接定义一个嵌套函数。
这可能是C++98、03的唯一手段了。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-20 16:42 by cexer
@OwnWaterloo

你说:“已经注册过的窗口类,如果要塞context,都需要subclassing。
SetWindowLongPtr( hwnd, GWLP_WNDPROC, insert_context );
然后在insert_context中将context塞进去,SetProp或thunk。
只是GetWindowLongPtr塞不了。”

可以塞的,只是需要超类化(superclassing)之后再塞。子类化(subclassing)在这里不行。但超类化是更需要谨慎使用的东西。


你说:“如何在调用点上创建这个回调函数,而不是在调用点所在函数外。“

C++不能创嵌套函数的,但可以在函数当中创建一个函数对象。你试试看?

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-20 16:52 by OwnWaterloo
@cexer
超类化?


可以创建嵌套类,嵌套类中可以有静态或非静态成员。比较间接一点。
直接写嵌套函数是不行的。

跟是否可以函数对象没什么关系。
主要需求是定义这个回调(或者函数对象)的地方最好和使用这个回调的地方比较接近。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-20 16:57 by OwnWaterloo
@cexer
GetClassInfo( DIALOG , &wc );
wc.lpProc = my_proc;
不修改wc.name
RegClass( &wc );

这样?

这……

如果在修改wc并注册之前,已经创建了一些窗口呢?
会怎样? 也会跟着被修改? 不会吧……?

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-20 16:58 by cexer
@OwnWaterloo
你说:“超类化?”

随手找了篇文章:http://hi.baidu.com/combojiang/blog/item/45e968b7b8a510f131add11c.html@OwnWaterloo


你说:“跟是否可以函数对象没什么关系。主要需求是定义这个回调(或者函数对象)的地方最好和使用这个回调的地方比较接近。”

我的意思是,可以定义函数对象代替函数来作为回调。实在是需要纯粹的回调函数,也可以在外面定义一个公用的模板,然后在内部定义一个函数对象为参数来具现化这个模板,就获得了真正的函数地址。不知这个是否与你的需求有所出入?

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-20 17:05 by cexer
@OwnWaterloo

你说:“如果在修改wc并注册之前,已经创建了一些窗口呢?
会怎样? 也会跟着被修改? 不会吧……?”

确实不会。Windows 还没实现那么神奇的功能,而且没必要。如果需要同时修改已经创建的窗口,就只能列举出来一个个地子类化。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-20 17:19 by OwnWaterloo
@cexer
那篇文章我晚点再看看。

我觉得是函数指针还是函数对象关系都不大,我并不排斥函数指针~_~

关键是那个共用的模板定义不出来啊……

假设,一个xxx控件,用来调节一个整数,它提供这么一个通知:

void xxx::on_change_add( void (*listener)(void* context,int pos), void* context );

我觉得最好的语法:
void f1()
{

xxx x;
int i = 0;
x.on_change_add( void (void* c,int pos){ *(int*)c = pos; }, &i );
wait();
printf("%d\n",i);

}
应该只有等C++0x的lambda才可以这样写了。


次之:

void f2()
{

xxx x;
int i = 0;
struct local { static void set_pos(void* c,int pos) { *(int*)c = pos; } };
x.on_change_add( &local::set_pos, &i );
wait();
printf("%d\n",i);

}

这是符合C++语法的,比较丑陋了。
但如果on_change_add是函数模板,就会很麻烦。
函数模板的实际类型参数不能是这种内嵌的类型。


最难看的:
void set_pos(void* c,int pos) { *(int*)c = pos; }
void f3()
{

xxx x;
int i = 0;
x.on_change_add( &set_pos, &i );
wait();
printf("%d\n",i);

}


set_pos只是一个示例而已。
代表的是一簇简单(代码就2、3行),但逻辑多变 ——根本不可能预定义一些函数对象或者函数模板 —— 只能是随写随用、一处使用、不被复用的代码来表现需要的逻辑。

类似的例子, std::for_each如果没有lambda的支持, 就是鸡肋。


当然,这只是个审美的问题……
也有人觉得第3种(定义到外部)还不错……

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-20 17:33 by cexer
@OwnWaterloo

你的这个需求有点像 java 的 AWT 的 添加 listener 的方式
addWindowListener ( new WindowListener(){
  void windowOpened(){ ...... }
  void windowClosing(){ ...... }
} );

也有点类似自动化脚本的
onclick = "javascript:{......}"

或者 lua 的
window.onClick = function( arg )
begin
  --......
end

确实是很诱人的语法。可惜三种 C++ 都不能实现,boost::lamda 生成的函数对象好像是临时的吧?也不能这样保存起来以后再使用。

# re: GUI框架:谈谈框架,写写代码[未登录]  回复  更多评论   

2009-11-20 17:59 by OwnWaterloo
boost.lambda好像只是表达式。 没仔细研究过。
要保存可以有auto。。。 或者boost.typeof。

本来我需要的仅仅是一个函数,却不得不定义一个类…… 很……
被其他语言惯坏了,就觉得c++写gui非常不方便……

我打算等c++0x流行之后再考虑是不是要用c++写gui了~_~
在哪之前,如果有得选择,c++就不是第1人选……

# re: GUI框架:谈谈框架,写写代码[未登录]  回复  更多评论   

2009-11-20 23:29 by Loaden
@ OwnWaterloo
你用的是和atl一样的方式?
将hwnd"替换"为this? 而非"插入"一个this?
是吗?
====================
是的。因为这样才能兼容x64平台:x64前8个参数通过寄存器传递。

@ cexer
SetProp 结合 GUID更理想,而GUID可以用API获取。
以后多交流!期待你的GUI框架下一篇...^_^

================
|||||||||||||||||||||

发现一个类中有6万个指针时,如果指针初始化了,还是占用内存。
没办法,为了降低成本,不能在类中放那么多指针了。
所以,使用bitset来帮忙。

先看消息注册(比原来的难看多了,没找到好方法,详见:http://topic.csdn.net/u/20091119/15/07d7e755-c733-4195-8cc4-306560d6fbc4.html

class MainFrm : public Frame
{
public:
MainFrm() : Frame(_T("Test Demo!"), _T("MainFrame")), m_testBtn(_T("Test!"))
{
Add(&m_testBtn);
m_testBtn.Get<EvtLButtonDown>(m_testBtn.IdLButtonDown).Bind(this, &MainFrm::OnLBtnClicked);
Get<EvtCreate>(IdCreate).Bind(this, &MainFrm::OnCreate);
}

~MainFrm()
{
Get<EvtCreate>(IdCreate).UnBind(this, &MainFrm::OnCreate);
}

qpEvt OnCreate(EvtCreate& evt)
{
qpDbgInt(evt.handled);
}

qpEvt OnLBtnClicked(EvtLButtonDown& evt)
{
qpDbgInt(evt.owner);
}

protected:
Button m_testBtn;
};

用bitset + map<short, void*> + enum来降低成本:
#define IMPLEMENT_EVENT(X, Y) \
public: \
template <typename T> \
Event<T>& Get(X id) \
{ \
if (m_evtMap.get() == NULL) \
{ \
qpNewPtrEx(p, EventMap); \
m_evtMap.reset(p); \
} \
if (m_evtFlag[id] == 0) \
{ \
qpNewPtrEx(p, Event<T>); \
if (p != NULL) \
{ \
m_evtFlag[id] = 1; \
m_evtMap->insert(std::make_pair(static_cast<short>(id), p)); \
return *static_cast<Event<T>*>(p); \
} \
} \
else \
{ \
EventMap::iterator it = m_evtMap->find(static_cast<short>(id)); \
if (it != m_evtMap->end()) return *static_cast<Event<T>*>(it->second); \
} \
qpASSERT(false); \
qpNewPtrEx(p, Event<T>); \
return *std::auto_ptr<Event<T>>(p); \
} \
protected: \
typedef std::map<short, void*> EventMap; \
std::auto_ptr<EventMap> m_evtMap; \
std::bitset<Y> m_evtFlag;

std::bitset的使用,可以判断相应消息是否注册,不用每一个消息都进入一次Event了,但由于使用map来查找消息,效率上还是下降了。
没办法:只能用时间换空间!

# re: GUI框架:谈谈框架,写写代码[未登录]  回复  更多评论   

2009-11-20 23:33 by Loaden
相应类的消息注册类
class ButtonEvt
{
public:
enum EventId
{
IdLButtonDown,
TotalEvent
};

IMPLEMENT_EVENT(EventId, TotalEvent);
};

class FrameEvt : public EvtSender
{
protected:
FrameEvt(const String& keyEvt) : EvtSender(keyEvt) {}

public:
enum EventId
{
IdCreate,
TotalEvent
};

IMPLEMENT_EVENT(EventId, TotalEvent);
};

这样,只需要往enum里放不占内存的消息ID就行了。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-21 02:04 by OwnWaterloo
@Loaden
同学生日,聚会喝了点酒…… 看代码很晕…… 像不认识C++似的……

关于你在csdn上的问题。

函数模板可以通过实际参数来推导类型。
template<typename T>
T max(T v1,T v2);

max(1212 , 326); // max<int> T=int
max(1212.0 , 326.0); // max<double> T=double

max(1212, 326.0); // 歧义 T=int or double
max<double>(1212, 326.0); // 显示指定 T=double


在模板参数列表中,最后一个不能被推导的参数以及它之前的所有参数都必须显示指定:
template<typename D,typename S>
D union_cast(S src)
{

union
{
S src;
D dst;
} u = { src };
return u.dst;

}

union_cast<long long>(&C::f); // D= 一个成员指针,但S不能通过实际参数推导出来,必须显式指定。


操作符重载,其实也是一个函数,只是通常不以函数的形式调用。
所以你那个需求,可能真的只能这么写:
a.operator[]<T,U>( index );


如果真的想实现:
T* p = a[ i ];
肯定能实现,不过很恶心…… 也可能很危险……

class A;

struct proxy
{
A* a_;

template<typename D>
operator D*() const { return new D; }

};

class A
{

template<typename T>
proxy operator[](T i)
{
proxy p = { this };
return p;
}

};

A a;
proxy p = a[ i ]; // 调用a.operator[]<int>; 返回一个proxy
E<TBase>* o = p; // proxy.operator<D>

E<TBase>* o = a[ i ]; // 连起来就是这样……


template<typename T>
operator T() 是很邪恶的东西,尽量少用……



是的。因为这样才能兼容x64平台:x64前8个参数通过寄存器传递。

确实,i386是个特例,主要通过栈来传递参数。
这样,无论被partial application的函数的参数如何 —— 只要不是返回大结构体 —— 插入一个指针参数都可以使用相同的thunk结构体。

我考虑过ppc(我唯一能搞到的非i386的机器),主要通过寄存器传递。
视被partial application的函数的参数,插入一个指针需要不同的thunk结构体。
但替换某个参数,thunk结构体就和其他参数的数目无关了。

如果被partial application的函数的参数已经确定,为其设计一个thunk结构体,插入一个指针参数也应该是可行的。

# re: GUI框架:谈谈框架,写写代码[未登录]  回复  更多评论   

2009-11-21 08:53 by Loaden
@ OwnWaterloo
谢谢!
我目前的方案,实现了每个可以发送消息的类,例如Button、Frame,至少需要8byte的代价,但:

50个事件供Bind:至少12字节
100个事件供Bind:至少20字节
1000个事件供Bind:至少132字节
10000个事件供Bind:至少1256字节
而我之前每个事件用一个指针的方法,分别对应:
200
400
4000
40000
代价下降了20倍。

不过每个注册的事件,还要多一个short + void*。
由于用户在一个程序中处理的事件不会很多,估计是总事件1/1000左右。
所以也是很值得的。

就算是一种低代价的实现吧。

# re: GUI框架:谈谈框架,写写代码[未登录]  回复  更多评论   

2009-11-21 10:06 by Loaden
@ OwnWaterloo
现在在理论上还可以有一种办法将:
m_testBtn.Get<EvtLButtonDown>(m_testBtn.IdLButtonDown).Bind(this, &MainFrm::OnLBtnClicked);
Get<EvtCreate>(IdCreate).Bind(this, &MainFrm::OnCreate);
简化为:
m_testBtn.Get<EvtLButtonDown>().Bind(this, &MainFrm::OnLBtnClicked);
Get<EvtCreate>().Bind(this, &MainFrm::OnCreate);

即不传入enum的id,改由Get函数根据typeid(T).name去判断id的值。
但如何去实现呢?

struct Create
{
bool handled;
};

class A
{
public:
enum
{
IdCreate,
TotalEvent
};

template <typename T>
void Get()
{
// 这条语句输出:struct Create
printf("%s", typeid(T).name());
// 可是,如何通过上面语句的输出,来访问: IdCreate呢?
// 从“struct Create中获取Create,在前面再加上Id,可以得到字符串IdCreate
// 如何将这个字符串转化为枚举型变量的一个值,例如:IdCreate?
//
// 这两行语句如何实现?
// int i = IdCreate;
// printf("%d", i);
}
};

int main()
{
A a;
a.Get<Create>();
return 0;
}

一种比较容易想到的方案是:
if (name=="A") id = ;
else if (name=="B") id = ;
但如果事件很多,这样去比较,代码写起来很麻烦。
而且编译后的程序体积也是个问题。

放到一个const字符串数组里,根据index判断,由于const数组是占用内存的(VC占用,测试过,GCC不占用),所以这样反而得不偿失了。

# re: GUI框架:谈谈框架,写写代码[未登录]  回复  更多评论   

2009-11-21 11:30 by Loaden
typeid.name()的问题,通过CSDN解决了。
帖一下Demo。

struct Create
{
bool handled;
template<class T>
static int GetEventId()
{
return T::IdCreate;
};
};

struct Close
{
bool handled;
template<typename T>
static int GetEventId()
{
return T::IdClose;
};
};

class A
{
public:
enum
{
IdCreate,
IdClose,
TotalEvent
};

template <typename T>
void Get()
{
printf("%d\n", T::GetEventId<A>());
}
};

int main()
{
A a;
a.Get<Create>();
a.Get<Close>();
return 0;
}

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2009-11-22 20:56 by 暗涌
@OwnWaterloo
那么,ATL-style继承的作用就是:实现静态的"template method"?

建议看看boost里Singleton的模板,类似。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2010-04-29 09:04 by ValentineANA
All people deserve wealthy life and <a href="http://lowest-rate-loans.com/topics/credit-loans">http://www.lowest-rate-loans.com</a> or just small business loan will make it better. Because people's freedom is grounded on money state.

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2010-05-09 10:43 by iwobz
牛人啊,膜拜了,回头我也去尝试着写一个

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2010-08-25 12:24 by 法官
牛人啊,膜拜了,学习了

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2010-12-19 01:12 by 溪流
来考古了,太多了~~~~囫囵看一遍都花这么久。

# re: GUI框架:谈谈框架,写写代码  回复  更多评论   

2014-12-11 19:08 by lx20050809
真的很精彩!
评论共2页: 1 2 

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理