随笔-341  评论-2670  文章-0  trackbacks-0
    从某种意义上来说,做图形也好,做GUI也好,做编译器也好,大概都是一种情结。其实只要稍微想一想就知道,能把它们三者有机统一起来的,就只有游戏。我很久以前的确是为了想开发游戏才对编程产生兴趣的,而学习游戏开发占据了我前六年的时间。虽然现在不做了,不过偶尔总是会觉得手痒。但是做游戏没美工做不好怎么办呢?就只好写游戏代码了。但是没有资源写出来的游戏又不好玩,于是就只好写库。那写什么库呢,自然就只有渲染器、界面引擎和脚本引擎了。我的博客的大部分文章也是围绕着这三件事情建立起来,而且在中间不断切换的。

    撒,所以今天就轮到GUI了。我一直很想做出一个自绘的GUI出来,无奈一直设计不出一个好的架构。后来尝试用原生API,但是却又很不喜欢MFC的设计,就尝试自己照着.NET Framework和Delphi那套VCL的样子封装了一个控件库出来。无奈原生API细节无敌多,后来没做把所有的功能都全部做完。因此后来一段时间凡是需要界面我都直接用C#做。然后CEGUI出了,WPF和Silverlight也出了,我发现如今要做一个漂亮的GUI非自绘已经做不到了,所以我又做了一次尝试。

    去年在美帝的时候曾经试图再设计一次,得到了一些结果。后来我发现其实根本没办法为GDI、DirectX、OpenGL和其它绘图设备抽象一个公用的接口,否则就会遭遇大量性能问题。因为在很多细节上,譬如说渲染文字,为了达到较高的性能,OpenGL和DirectX需要使用几乎相反的策略来做。因此这次我又换了一个方法,而且在Vczh Library++ 3.0的Candidate目录下已经checkin了一个试验品。

    我把一个自绘的GUI分成了下面若干个层次。
    1、NativeWindow。NativeWindow表示的是一个顶层窗口的实现。譬如说我们想用Windows的窗口作为自绘窗口的顶层窗口(游戏里面的很多顶层窗口是绘制在游戏窗口里面的,所以顶层窗口并不一定是Windows的窗口)。
    2、控件库。控件库包含了这个自绘GUI库的所有预定义控件。控件本身包含对用户输入的相应逻辑,但是每一个控件的绘制以及鼠标点中测试不在此范围内。
    3、控件皮肤接口。每一个最终控件都会拥有一个控件皮肤接口。每当控件的状态发生了变化,控件会调用皮肤接口更新控件的当前状态。每当控件需要知道某一个点是否位于一个控件里面的时候,他也会去调用该控件的皮肤获得结果。因此控件皮肤接口包含了一切关于绘制(因此理所当然也就包含了点中测试)的逻辑。

    为了达到最高的性能,一套皮肤的实现只能绑定在某种绘图设备上,也就是说缺省状态下一套为GDI设备设计出来的皮肤是不能直接使用在DirectX设备上面的。当然我这个框架的设计也是足够开放的,如果你非得用同一套代码来实现不同绘图设备上的皮肤,那么你是可以自己动手丰衣足食,做到给GDI和DirectX设计一个公共接口并插入我的GUI框架的(只不过这种做法一般情况下都会惨死)。

    那么如何添加绘图设备呢?目前NativeWindow有一个基于Windows窗口的实现,并且NativeWindow的接口要求该实现在创建、销毁、接收到很多窗口事件的时候都调用某一个回调对象。我们可以通过注册一个全局回调对象或者具体窗口的回调对象来获得NativeWindow状态的变更。基于Windows窗口的NativeWindow实现还提供了一个额外函数,可以让你获得一个NativeWindow的HWND(但这个函数并不被控件库依赖)。现在我还实现了一个基于HWND+HDC的绘图设备,主要方法就是先注册全局回调对象,每当知道一个NativeWindow被创建了,我就会注册一个NativeWindow的回调对象,用来维护一个窗口里面的一块32位DIBSections位图缓冲区。窗口的大小如果变化了,我也会在适当的时候重新创建一块合适的缓冲区。不过为了避免每一次大小变化都会创建新的缓冲区,我创建的缓冲区的大小总会比窗口大一点。然后这个GDI绘图设备就暴露了一个函数,可以获得一个NativeWindow的HDC和WinGDIElementEnvironment。

    WinGDIElementEnvironment是基于HWND+HDC的这一套实现上专有的、为了GDI皮肤设计出来的一个公共的资源库(譬如用来保存各种面向业务逻辑的pen啊brush什么的,比如说disable的时候什么颜色,选中的时候什么颜色等等)。如果你想设计一个基于HWND+DirectX的皮肤,那么类似WinGDIElementEnvironment的这套东西要重新做一次——因为为了达到相同的性能。具体细节相差太大。当然HWND+HDC上面可以有多套皮肤,WinGDIElementEnvironment是公用的。WinGDIElementEnvironment要求绘制是通过一个具体的WinGDIElement对象达到的,而一套皮肤可以有自己的一套WinGDIElement的实现。WinGDIElement被设计成面向业务的、一套皮肤的基本元素组成部分,譬如说按钮边框啦、焦点长方形啦、文字啦,而不是带有pen和brush的长方形啊,文字啊,各种乱七八糟的最低等级的绘图元素。举一个例子,按钮边框跟菜单边框很像,都可以用Rectangle来组成。但是Element里面就直接是按钮边框和菜单边框,而不是一个可以让你自由修改颜色的Rectangle。因为不同的控件要共享配色方案,而配色方案是由业务逻辑+空间状态的集合实现的,因此WinGDIElement还是一个比较高层次的概念。当一个WinGDIElement被渲染的时候,他会给你一个HDC,然后你根据被设置的状态来调用GDI函数绘制到HDC指向的32位DIBSections位图缓冲区上面。

    那么,当我们使用HWND+HDC的实现,创建了一个布满了控件的窗口,那实际上是发生了什么事情呢?首先控件自己会组成一棵树。其次,控件的皮肤也会组成一棵树。现在就有控件树跟皮肤树两颗树了。控件树负责所有用户输入变更状态的逻辑部分,而皮肤树负责绘图和点中测试。而一个HWND+HDC实现的皮肤树,会在皮肤组合成树的时候,在底下又组合出了一颗WinGDIElement树。因此大局上就是:
    控件树(负责相应输入变更状态)--> 皮肤树(负责储存控件状态的可视部分并决定什么时候需要刷新)-->WinGDIElement树(负责绘图整个窗口)

    这个时候,如果我们仅仅需要简单的重新绘制窗口的话,那么控件树跟皮肤树都不需要被访问到,底层仅需要让WinGDIElement树重新绘制一遍即可。而WinGDIElement的粒度实际上也不小,因此不会每一个图元都有一个WinGDIElement从而使得创建出了一大堆对象的。

    最后一个设计就是在什么时候才重绘窗口的问题。假设说我们现在收到了一个WM_KEYDOWN消息,最终传播到了控件树里面去,然后修改了10个控件上面的文字。每当你修改文字的时候实际上都需要重绘,那如何将无数次不可控的重绘合并成一次呢?SendMessage(WM_PAINT)是立刻执行的,所以Windows自带的合并WM_PAINT的方法在这个时候是无效的。我所采取的解决方法就是:反正控件树的所有消息来源都是从NativeWindow里面来的,那实际上控件树发出一个重绘请求的时候,我就会把NativeWindow的HWND实现里面的一个bool变量设成true,然后当NativeWindow每一个传播到控件树的消息结束传播之后,才读一次那个变量,如果是true,那么就调用WinGDIElement进行重绘并把变量设计成false。坏处是每一个传播到控件树的消息在处理完之后都必须检查是否需要重绘,好处是这个东西被封装在了NativeWindow的HWND实现里面里面,无论是控件树、皮肤树还是WinGDIElement树也好,都在也不需要关心绘图时机的事情了。

    因为GUI被分割成了很多层,而且每一层的都关心业务逻辑的不同部分,所以他们都是可以被替换的。譬如说我们可以做成:
    HWND+HDC实现:最普通的方法
    HWND+DirectX:WPF和Silverlight地方法
    单一HWND+多个虚拟窗口+DirectX:可以在游戏里面用

    无论下面的绘图设备和窗口实现如何发生变化,GUI控件的逻辑部分都跟这些实现严格分离,因此不会受到影响。而且大部分情况下,我们是不需要拥有一个跨绘图设备的皮肤库的,譬如说游戏和应用程序,外表总不能做成一样的。对于那些需要同时在DirectX和OpenGL上面运行的程序(譬如说3dsmax),它已经有DirectX和OpenGL的公共接口了,因此这些软件可以利用它们的公共接口来实现GUI的绘图设备部分,从而在上面构造起来的皮肤自然是可以跨DirectX和OpenGL的。

    这比起一年前作的GUI实现又进了一大步。上一次的GUI尝试为不同的绘图设备抽象一套公共接口,后来惨死。不知道这次实际上做出来的效果如何,拭目以待吧。
posted on 2011-04-29 19:50 陈梓瀚(vczh) 阅读(5600) 评论(13)  编辑 收藏 引用 所属分类: 2D

评论:
# re: GUI真TAMA难做啊 2011-04-29 21:45 | 千暮(zblc)
- -bnr 嗯?那个“press me!” .........  回复  更多评论
  
# re: GUI真TAMA难做啊 2011-04-30 07:43 | misserwell
老实说,以我的能力可以做开发,但是我总不愿意去尝试, 游戏我总觉得是误人子弟, 浪费别人的时间无异于图财害命, 不知你可有同感  回复  更多评论
  
# re: GUI真TAMA难做啊 2011-04-30 08:49 | 千暮(zblc)
@misserwell
并非都如此 游戏是思维的润滑剂和情感的高密度模拟(好的剧情游戏能让你用最短时间体会到峰回路转以及学会站在不同人的角度考虑问题 在一种模拟情景 下去发挥你的判断的极限能力:你可以不用付出代价就在一定程度上训练人脑置信策略系统)

古代没有计算机游戏,但不见得就因此不浪费时间而高效;现代有了游戏,但信息获取更加便捷,这时候需要的是在吸收的同时不断使大脑适应不同的思维和反应方式,游戏能够一定程度上满足此点


PS:不过话说,vczh这次做的GUI不是游戏GUI,通用GUI要比游戏GUI难做  回复  更多评论
  
# re: GUI真TAMA难做啊 2011-04-30 08:50 | 陈梓瀚(vczh)
@misserwell
玩Portal2吧,这个游戏会改变你对游戏的定义的。  回复  更多评论
  
# re: GUI真TAMA难做啊 2011-04-30 08:51 | 千暮(zblc)
@陈梓瀚(vczh)
你到处跟人推销此款游戏 - -bnr 收取了多少好处费  回复  更多评论
  
# re: GUI真TAMA难做啊 2011-04-30 08:54 | 陈梓瀚(vczh)
@千暮(zblc)
可见该游戏的好玩程度令我愿意免费推销  回复  更多评论
  
# re: GUI真TAMA难做啊 2011-05-16 23:02 | simfe
简单点 ,有各duilib可以看看  回复  更多评论
  
# re: GUI真TAMA难做啊 2012-05-21 16:31 | clonne
您好,对于GUI库,我是这么看的:

现在已经是2012年了,我发觉整个软件行业都在急速改变,现代的软件风格明显和以前的不一样了,在GUI库中,我认为微软的WTL库非常值得参考,C++的模板就是一个减轻大量细节复杂度的强力工具。

根据我的看法,楼主完全可以不用任何本地控件,不用本地的事件消息系统,不用调用太多本地API,所有的绘制工作都使用3D加速:DirectX/OpenGL,如果是这样,那么GUI库的架构就是:

[GUI]
|
[交互]
...
[渲染]
|
[接口] - [DirectX|OpenGL]

当然,我没描述好,但是可以显示出,如果按照这种方式,那么基本上就是和游戏差不多了。你只要封装一些“虚拟控件”的绘制就行了,甚至可以达到很多绚丽的效果而还不影响性能(3D的性能是操作系统的API所不能比的)而且你根本就不用封装Win32的消息处理、窗口创建等等,这些甚至直接可以让DirectX/OpenGL帮你完成,而GUI的交互这些都是有API给你提供的!

为什么我推介你用这种方式,是因为如果你想跨inux/Win32/FreeBSD/Debian/..等等平台使用,即使你能做好一个通用抽象架构,也是会为大量的细节所击倒的。而使用3D库,你只要处理DirectX/OpenGL的调用就行了,现在的计算机行业流行的3D库就这2个,有大量的扩展能使用。

这是我个人的看法,我自己现在都不用GUI库了,Qt越来越臃肿,GTK不方便发行,其他库不想学,所以我直接自己调用本地API,或者封装下。我也很想把我设想的GUI库做出来。  回复  更多评论
  
# re: GUI真TAMA难做啊 2012-05-21 16:38 | clonne
接LS

楼主,我现在即使是调用本地的API,也有一个原则:只创建一个窗口!不使用Win32的资源、不使用Win32的控件。这让我减轻了很多细节上的烦恼,以前做界面,需要学习控件使用、大量的莫名其妙的细节问题等等等等。而我现在根本就不理系统,除了那个必要的窗口外,所有的"控件"都是我自行绘制(自己绘制控件的开发时间还没有以前掌握那些控件的时间多)。现在是调用的系统API来绘制,以后想开始做这个GUI库了,那就是完全使用3D了。  回复  更多评论
  
# re: GUI真TAMA难做啊 2012-05-22 09:10 | 陈梓瀚(vczh)
@clonne
我真的只创建窗口剩下的都用Direct2D的说,而且我的菜单和下拉框也是真的可以弹出窗口外的……不过尽管如此,架构也是十分复杂的。我倒是不怕复杂,因为win32那套我也封装过。我之所以想这么干,就是因为WPF功能那么强大本地C++却用不了太不爽了,所以我重做一个——当然也就跟你说的一样了。  回复  更多评论
  
# re: GUI真TAMA难做啊 2012-05-22 09:15 | 陈梓瀚(vczh)
@clonne
新版本已经放倒了gac.codeplex.com  回复  更多评论
  
# re: GUI真TAMA难做啊 2012-05-22 17:31 | 三国杀
感觉是大一统思想,这么多库有必要搞成统一接口嘛?舍弃一些有什么不好的!  回复  更多评论
  
# re: GUI真TAMA难做啊[未登录] 2012-05-22 23:20 | 陈梓瀚(vczh)
@三国杀
那不舍弃有什么不好?  回复  更多评论
  

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