GacUI与设计模式(二)——渲染系统
所有关于渲染的部分的代码可以在http://gac.codeplex.com下载下来之后,在\Libraries\GacUI\Source\GraphicsElement目录下面找到。
整个渲染系统的主要思想就是,图元(IGuiGraphicsElement)和渲染器(IGuiGraphicsRenderer)分开,而且粒度根据性能的要求粗细都有。为什么要这么设计呢?在前言里面说过,不同的渲染设备,譬如GDI和DirectX,需要的渲染策略和cache资源的方法都不太一样。因此为了让各个渲染设备的渲染器可以充分自定义渲染的策略,于是做出了这样的设计。
但是具体是怎么做的呢?在GacUI里面,首先可以用GetGuiGraphicsResourceManager来获取一个全局的资源管理器(GuiGraphicsResourceManager)对象。这个对象的主要作用就是注册各种创建图元和渲染器的工厂对象。为了让整个渲染系统运行起来,首先我们要把各种图元工厂(IGuiGraphicsElementFactory)注册进去。每一个图元工厂有自己的一个全局的名字。这样当你把一个图元工厂注册金资源管理器之后,从此就可以用图元的名字从资源管理器里面取出注册进去的图元工厂对象了。
其次,因为在运行的时候,每一个图元对象都会在内部保存一个专门给这个图元对象用的渲染器对象,具体的渲染设备的渲染器可以在这个渲染器对象里面cache一些资源,就可以达到为某个图元cache特殊的资源的目的了。因此为了给图元对象创建合适的渲染器对象,我们还需要将图元工厂的名字和一个渲染器工厂(IGuiGraphicsRendererFactory)关联起来。当这一步完成之后,我们就可以通过下面的代码来给一个图元关联上正确的渲染器对象:
IGuiGraphicsElement* element = xxxx;
IGuiGraphicsElementFactory* elementFactory = element->GetFactory();
IGuiGraphicsRendererFactory* rendererFactory = GetGuiGraphicsResourceManager()
->GetRendererFactory(elementFactory->GetElementTypeName());
IGuiGraphicsRenderer* renderer = rendererFactory->Create();
renderer->Initialize(element);
这样我们就从一个IGuiGraphicsElement对象构造出了对应的IGuiGraphicsRenderer对象,并且将这个渲染器对象和这个图元对象关联了起来。这一步完成之后,渲染器对象就会开始根据需要cache被关联的图元对象所需要的资源。然后我们只需要把渲染器对象的指针告诉图元对象,那么图元对象就可以在自己被更新的时候,通过调用renderer->OnELementStateChanged()适当通知一下渲染器对象,而且也可以用renderer->GetMinSize()来说的显示这个图元所需要的最小的矩形尺寸了。为什么尺寸要通过渲染器来计算呢?主要是因为具体怎么渲染是渲染器来控制的,所以尺寸当然也是需要让渲染其计算的,其中一个例子就是文字渲染了。
接下来就是如何规划图元的问题了。目前GacUI所有的图元如下所示:
Gui3DBorderElement
Gui3DSplitterElement
GuiGradientBackgroundElement
GuiImageFrameElement
GuiPolygonElement
GuiRoundBorderElement
GuiSolidBackgroundElement
GuiSolidBorderElement
GuiSolidLabelElement
GuiColorizedTextElement
我们可以看到,大部分的图元都是很简单的。GuiSolidLabelElement就稍微复杂一点,具有了一些诸如自动换行啊省略号这样的设置。而最复杂的就是GuiColorizedTextElement了,里面按行保存了文本之后,还按行给每一个字符分配了存放颜色的缓冲区,然后实现了字符串修改的时候缓冲区的分配释放更新等操作。为什么不设计一个GuiCharElement,而是做成了这两个东西呢?因为在普遍情况下,渲染器都支持对复杂的文字一次性渲染完成,如果我们把每一个字符都设计成一个图元,让排版引擎去渲染字符串的话,性能低下不说,效果可能还不如渲染器自己渲染出来的好。关于这里的一个典型的例子就是Windows所支持的可以连笔的OpenType技术了。另一个原因就是,在开发着色文本框的时候,如果所有的渲染过程不包含在一个图元,而是分散在各个字符图元的话,那更新文字和颜色的时候,无疑十分浪费内存,并且操作起来非常的麻烦,为了灵活性牺牲了太多的性能,得不偿失。
说完了图元和渲染器,最后一个要介绍的就是渲染目标对象(IGuiGraphicsRenderTarget)了。尽管渲染目标可以指向很多种地方,但是在一般情况下,渲染目标所指向的都是一个窗口的客户区域(client area)。尽管在设计上这样看起来仅仅是很自然,但是实际上这么一个对象却是必须的,因为Direct2D的一个render target创建出来的画刷等资源不能直接用在另一个render target上面,而且当render target挂掉的时候,那些资源要全部干掉,重新创建render target,并且重新创建资源。这一步作为一个bug登记在了GacUI里面,还没实现,所以现在Direct2D渲染的时候,把窗口最小化再打开,有时候会变黑。
渲染目标对象的另一个功能就是计算clipping了。在形成父子关系的排版对象绑定的图元在渲染的时候,子图元是不能超出父排版对象的矩形范围的。而且鉴于大量的对象可能处于不可见的位置,所以外围的驱动渲染的代码要在渲染对象完全被clip没了的时候(譬如说在一个具有滚动条的容器里面,一个因为滚动条的关系看不见的按钮),停止渲染看不见的那颗子树,加速渲染过程。而且各个渲染设备也需要处理类似于一个文字只有上半部分能看见这样的情形。所以排版对象就可以通过提供他自己的矩形范围给渲染目标对象,从而让渲染目标对象自己计算可见的矩形范围,从而配合整个渲染流程的进行。鉴于有一部分的渲染器需要的资源是从渲染目标对象来的,因此IGuiGraphicsRenderer还有一个叫做SetRenderTarget的函数,用于在渲染对象发生变化的时候,譬如说因为窗口最小化从而造成Direct2D的render target的时效,需要重新创建的时候,通知每一个图元绑定的渲染器说,整个渲染目标对象已经换掉了,一些资源可能要重新创建。
当然在这里需要提出的就是,在GacUI的GDI和Direct2D渲染器的实现里面,是有一些依靠引用计数全局cache的资源。譬如说在同一个渲染目标对象里面渲染的两个同样颜色的矩形,他在内部使用的具体的画刷就不会真的重复创建两次。尽管GDI和Direct2D的策略不同,GDI的画刷是全局的,而Direct2D的话刷只对一个render target有效,GacUI还是提供了一个通用的资源cache算法模板,让实现类似的功能更加方便。