GacUI与设计模式
说起 GacUI( http://HdhCmsTestgaclib.net/ , gac.codeplex测试数据 ) ,其实这个想法在我还在上大三的时候就已经有了。但是由于经验不足,在当时并没能够把这个东西给做出来,直到去年( 2011 )的国庆节为止。想想到现在也做了快一年了, GacUI 也可以用来写一些不是特别残暴的 C++GUI 程序了。前几天有人问道,为什么在 PC 都快完蛋了并且大部分 GUI 都已经用 C# 来做的时候,我还要做这个东西呢?其实,这有两个原因:第一个我喜欢折腾 C++ ;第二个 C++ 好像也没什么特别好的 GUI ,因此也想尝试一下,如果做成了就维护下去,做不成了好歹还可以提高自己的水平,总之是不会浪费时间的。所以我就在想, GacUI 写到现在也快一年了,并且我最近也看到 cppblog 上面有几个人也想搞搞 GUI ,因此我想把 GacUI 的一些设计思想,和我得到这些思想的过程写出来,顺便也介绍一下 GacUI 的架构,让一些有兴趣的人(特别是装配脑袋)也可以来折腾折腾。
GacUI 的架构的最重要一点就是要跨平台。当然这不一定意味着我将来一定会把 GacUI 移植到别的什么操作系统去,但至少 Windows 的 Classic Desktop 和 Metro 的两套 API 就毫无相似之处,同时搞定他们,也算是跨平台了。而且就算是基于同一种 API ,上面还有不同的渲染器的 API ,譬如说 GDI ,譬如说 Direct2D ,他们也是截然不同。 GacUI 的设计至少要可以屏蔽掉他们的区别。当然,这在技术上有一个很好的方法来保证,就是 GacUIIncludes.h 里面不包含 Windows.h 的任何内容——因此至少在头文件里面,所有的东西都是跟 Windows 无关的。当然在非 GUI 的部分,我们还是需要 Windows.h 的,并且有些人喜欢对 GacUI 做点 hack 的操作,因此我还是在 GacUI.h 里面提供了几个额外的依赖于 Windows.h 的函数来暴露一些内部细节。那这样如何跨 Classic Desktop 和 Metro 呢?有一个简单的方法,就是可以在编译的时候给些宏开关,譬如说 GACUI_WINDOWS_CLASSIC_DESKTOP (缺省)或者 GACUI_WINDOWS_METRO 之类的东西,来屏蔽掉不需要的部分。当然这部分在移植到 Metro 之前我不会加进去。
基于这个想法,如果大家阅读了 GacUI 的代码的话,会发现在文件 \Libraries\GacUI\Source\NativeWindow\GuiNativeWindow.h 里面定义了一个 INativeController 接口,而且目前只有 Windows Classic Desktop 一个实现。 INativeController 的内容很多,提供了跟具体的平台有关的操作,譬如说读写图片文件啦、创建消灭窗口啦、显示器操作啦、还有各种其他的输入输出等等。实现一个从头 INativeController 还是比较繁琐的,因为 GUI 这种对操作系统重度依赖的东西,想剥离开来,就会发现他依赖了一大坨 API 。这也解释了为什么 INativeController 的各个 XXXService 函数返回的对象的方法的总和有上百个。不过从 Classic Desktop 移植到 Metro 还是相对比较简单的,因为大部分内容还是可以共享的。
其次就是渲染器了。渲染器跟平台是交叉依赖的。譬如说 OpenGL 在 linux 上和 Classic Desktop 上都可以用, Direct2D 在 Classic Desktop 上和 Metro 上都可以用, GDI 只能在 Classic Desktop 上面用。因此这就是为什么我最终没有把渲染器也写在 INativeController 里面,而是把渲染器整个给屏蔽掉了,根本没有在 GacUIIncludes.h 里面给出他的接口。但是考虑到 GacUI 是一个支持换肤的 GUI 库,因此肯定需要让皮肤来自己决定如何绘图。后来我就想了一个办法,把渲染器的结构整个拿掉,替换成各种各样的图元( IGuiGraphicsElement )。所谓的图元就是类似于方形啊,圆形啊,填充啊,渐变啊,文字之类的东西。皮肤自己把图元按照一定的排版关系(在下文中有描述)拼装好,然后 GacUI 内部的一个小系统会利用 Bridge 和 Abstract Factory 两个模式的结合体(参考 \Libraries\GacUI\Source\GraphicsElement\GuiGraphicsElement.h )来为这些图元分配好渲染器对象( IGuiGraphicsRenderer )。然后图元和渲染器之间用了 Listener 模式在交换信息。这样的好处是,当图元受到改动的时候,这个图元对象的专用渲染器对象可以选择 cache 一些信息,然后在窗口渲染的时候,只需要访问所有的渲染器对象(在排版对象 GuiGraphicsComposition 的组合项形成了一棵树),让他们渲染自己就可以了。
图元包含了所有需要渲染的数据,但是唯独没有把尺寸写进去,因为尺寸这种东西不应该让渲染器来负责,而应该让排版对象来负责。排版对象自己是一棵树,然后节点根节点之间有一些关系,这样就可以实现堆栈排版、表格排版、对齐(到某一些边上的)排版等等具体的排版算法。一个排版对象可以放置一个图元对象并让这个图元充满他,所以显而易见,有一些排版对象仅仅是用来计算尺寸的中间结果,上面不一定有图元对象的。当渲染开始的时候,排版对象首先跟图元对象获取数据,然后递归计算好整棵排版树的尺寸,最后把尺寸交给附着在上面的图元对象的专用渲染器对象来渲染。
大家可能会想,如果渲染一次都需要调用成千上万个虚函数的话,会不会性能低下啊?当然编译成 Release 运行会发现 GacUI 的性能还是相当高的。原因有两个。第一个是我对排版对象做了一些优化。举个例子,一个对象的尺寸至少要大于所有子对象的尺寸,这个事情计算起来是相当快的,不需要做 cache 。但是一个表格排版里面的所有小格子会互相挤来挤去,这个东西计算起来相当复杂(复杂度大越是平方,而且系数也不笑),所以结果要做 cache 。但是什么时候需要重新计算呢?度量方法很简单,就是每一个格子的最小尺寸发生了变化的时候。而且事实上大部分皮肤都是用表格来排版的,所以等于说大部分结果都有 cache 。所以排版部分的尺寸在每一次渲染的时候只需要做一些小计算就可以了。复杂的排版每一个排版对象相互之间都是有关系的,一个排版对象发生了变化,有可能导致另一个排版对象的尺寸需要修改,所以最简单的方法就是,不保存尺寸,每一次都直接重新算一次就可以了。在这个基础上,表格排版做一下 cache ,整个计算过程就会变得飞快。所以尽管每一次拖动窗口,或者鼠标滑过一次窗口,都要进行相当多的计算,但是因为有一个智能的 cache ,使得不仅运算速度变快,而且在添加新的排版对象类型的时候也根本不需要考虑自己会不会被 cache 的问题,开发起来也相当愉悦。
所以上面的三大模块(操作系统 API 隔离、渲染器、排版对象)已经足以让我在系统里面开一个窗口然后在上面放各种各样的东西了,譬如说组合成一个非常接近 Windows7 的按钮外观的一个矢量图。那控件要怎么办呢?其实一个控件,就是通过接收用户的输入,对一个排版对象上承载的一大堆图元进行更改。用户的输入和控件( GuiControl )本身的状态进行互动,然后控件把状态的变更提交给控件的皮肤( GuiControl::IStyleController ),最后皮肤通过修改图元来把状态变更最终展现给用户。一个典型的例子就是,在使用 Windows7 皮肤的时候,鼠标移动到按钮上面去,他会触发一个动画慢慢变成蓝色。
GacUI 的大体架构就是这个样子了。在接下来的几篇文章里面,我会详细介绍每一个子系统的内部结构,顺带做以下代码导读,大家敬请期待。
分类: 其他
作者: Leo_wl
出处: http://HdhCmsTestcnblogs测试数据/Leo_wl/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
版权信息