本来是想从如何建立GacUI的VC++工程开始写的,不过最近网友普遍反映,怎么建工程都能从 Tutorial 里面看明白,就打算说到GacUI的XML资源怎么编译的时候在顺带讲一下。今天主要说的是GacUI的体系架构。
在构造一个GacUI工程的时候, CppXml 和 MVVM 是两个推荐的Hello World参考项目。虽然还有一些其他的方法,但那并不是使用GacUI的正确方法,那些Demo只是为了添加知识做出来的。
GacUI 类库参考
见:http://www.gaclib.net/Document.html#~/ 。这个网站host在Windows Azure上。如果要访问host在github io上面的镜像网站,可以使用 http://vczh-libraries.github.io/Document.html#~/ 。
这个网页里面的所有内容其实都在代码头文件的注释里,然后我写了一个工具把他们都抽了出来,做成了这个网页。至于为什么不用现成的工具,其实我一开始是直接使用VC++的XML注释功能的。VC++在编译的时候会帮我做完所有的事情,而且在开发的时候还会把注释写进intellisense里面,特别容易使用。
但是在这个过程中,我痛苦地发现,VC++要直接生成chm的话,必须使用托管项目。显然GacUI并不是托管项目,而且他要求托管项目的原因,仅仅是为了读取exe/dll里面的元数据。那么Native C++项目的元数据,当然就只能去pdb找了。于是我使用了VC++自带的一个处理pdb读写的COM库,把这个事情给做出来了。
后来我又痛苦地发现,VC++的XML文档,只给托管项目提供对模板的支持。我把注释写在模板类上,VC++在编译出最后的XML注释汇总文件的时候,写在模板上面的直接没有了!于是我只好放弃使用VC++提供的工具链,转而自己使用C#做了一个C++头文件的parser,从而解决了所有的问题。如果大家感兴趣的话,这个文档生成工具可以在 这里 找到。
不过我写的文档生成工具,只是产生了一系列的带元数据的XML注释文件。实际上这个网页是另外写的。网页会在访问者点击了一个类的超链接之后,去后台获取相应的XML,然后cache在客户端内存里。所以你点击第二次的话不需要重新加载——直到你关闭了这个网页。
为什么整个文档只有一个网页呢?因为那个时候我正在学习如何正确使用Javascript和CSS,就顺便练了练手。另一个原因是,我想把网站host在github io上,但是github io又不能后台跑程序,所以只好痛苦的使用Javascript把ASP.NET MVC里面我喜欢的部分做了出来,全部逻辑跑在浏览器里。
GacUI 源文件
可以在两个地方获得GacUI的源文件。
使用Release
第一个是Release。Release的正确下载方法是到 Release repo的Release页面 获取代码。目前Linux版本正在开发,OSX基本已经完工了,不过他们还没有集成到同一个Release repo下面。所以需要到 XGac 和 iGac 两个repo下载代码。
Windows版本的Release会包含以下几个文件,他们都是成对的.h和.cpp文件:
Vlpp:跨平台的C++基础库。
Workflow:一个可以靠反射访问C++类的脚本引擎,对象模型使用引用计数,跟C++的互操作性无比的好。
GacUI:GacUI的主要部分。
GacUIReflection:GacUI所有可以被脚本访问的类的元数据,用于反射。如果不链接这个文件的话,那么在初始化的时候,构造元数据的过程将被跳过。用户不需要写额外的代码来明确这个过程,只需要选择链接或者不链接这个文件就可以了。
GacUIWindows:GacUI在Windows平台下面的Window Provider和Renderer。
目前你需要使用所有的文件,因为GacUI会把XML资源直接编译成Workflow脚本引擎的字节码,嵌入到生成后的二进制资源文件里。在构造一个窗口的时候,实际上就是在跑脚本。尽管Workflow脚本远没有C++快,但是一个窗口再复杂也复杂不到哪里去,所以加载的时间难以察觉。
明年即将推出把GacUI的XML资源直接编译成C++的选项,到时候可以无限缩小文件体积,再也不需要带上GacUIReflection里面的元数据了,那么Workflow和GacUIReflection在大部分情况下就都不需要了。不过如果你的程序想要支持插件,那自然无法使用这个功能。
阅读 GacUI 代码
上面的每一个文件都十分的大。我这样做纯粹是为了程序员的使用方便。程序员只需要根据需求链接不同的文件就可以了,而不需要把整棵目录树都拖进来。不过我自己在开发的时候,显然不可能直接写这些文件的。这些文件是我在做Release的时候,调用我写的一个命令行工具拼装出来的。包括上面提到的文档也是。
所以如果需要阅读GacUI的代码的话,应该分别去 Vlpp 、 Workflow 和 GacUI 三个repo。
GacUI 体系架构
事实上GacUI的架构是分层的,从底层到顶层分别是:
Window Provider
Renderers
Elements + Compositions
Controls + Templates
XML Resource Compiler
Window Provider
Window Provider指的是如何操纵操作系统提供的原生的窗口、图片资源、鼠标资源、异步原语和一些其他的东西。毕竟GacUI怎么做,最顶层的窗口是没办法自己做的,最多通过Template来替换掉窗口边框。把GacUI移植到Linux和OSX的工作,主要就是写两个新的Window Provider,然后提供各个平台上不同渲染器的Renderer。其他的部分都是平台共享的代码。
Renderers
Renderers跟Window Provider是分开的。毕竟同一个操作系统上你可以使用不同的图像技术来绘图,而不同的操作系统上你可以使用相同的图像技术来绘图。举个例子,OpenGL和Cairo就是在很多平台上可以用的。不过OpenGL在每一个平台上都是二等公民,所以我并没有真的使用OpenGL来开发。一个GacUI程序在刚开始的时候,如果是Windows的话就是在WinMain函数里,需要首先选择一个Renderer,选择了之后就不能变了。
目前GacUI在不同的操作系统上使用的绘图技术如下所示:
Windows : GDI, Direct2D 1.0, Direct2D 1.1
Linux : Cairo + Pango
OSX : CoreGraphics + CoreText
Direct2D的1.0和1.1的版本虽然只有初始化的代码有区别,不过这关系到能不能直接跟Direct3D 11.0搅在一起,所以单独拿出来讲了。目前GacUI在Windows上,如果你选择了使用Direct2D技术:
WinMain.cpp
#include <GacUI.h>
#include <Windows.h>
int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int CmdShow)
{
return SetupWindowsDirect2DRenderer();
}
那么在GacUI初始化的时候会优先选择Direct1.1。如果在代码里面引用了GacUIWindows.h,那么你还可以得到每个窗口所使用的系统相关的对象,可以让你在不得不使用非跨平台技术的时候,提供一个机会。
Elements + Compositions
在GacUI里面Element和和Composition分别代表基础的图元和排版功能。每一个Element运行在具体的平台的时候,都需要具体的Renderer对象。这些对象是前面两层合作提供的。
Composition是GacUI的其中一个重要部分。这个部分提供了所有的排版功能。一个Composition对象代表了窗口上的一个长方形的区域。每一个区域可以嵌入一个Element。当一个Composition确定了他的位置的时候,那么Element会被填充到整个长方形的区域里面,从而渲染出来。
几乎所有的Element都是很简单的几何图形,除了渲染文字的 GuiColorizedTextElement 和 GuiDocumentElement 。不过在制作控件皮肤(也就是Template的一部分功能)的时候,文本框控件由于功能的复杂性,皮肤需要提供一个区域让控件放置这两个Element,而不是跟普通的控件一样,全权处理所有的渲染对象。
目前Composition支持直接定位、Stack、Table、Flow和一些其他的功能。一个比较特殊的就是,使用 GuiDocumentElement 的 GuiDocumentLabel 和 GuiDocumentViewer 可以在富文本文档的中间嵌入Composition。这个功能是其他的Element所不具备的。因此这两个控件构成了一种新的排版方法。
Element和Composition的具体介绍将在以后的文章中提供。
Controls + Templates
Control的结构比较复杂。一个典型的GacUI的Control,包含了用来代表控件本身的操作和数据逻辑的Control对象,和包含了如何渲染这个空间的IStyleController或者IStyleProvider对象。IStyleController拥有整个Composition和Element的控制权。如果当一个Control只决定让皮肤控制一部分的Composition和Element的时候,那么他会提供IStyleProvider对象。
不过在开发的时候,程序员不需要区分IStyleController和IStyleProvider,因为使用XML来编写皮肤的时候,都是使用Template来编辑。最后每个Template会自己去找一个合适的wrapper对象来把自己wrap成IStyleController或者IStyleProvider然后提供给Control。IStyleController / IStyleProvider 和 Template的区别,就在于一个是Pull模型的,一个是Push模型的。Push模型做data binding特别容易,因此在XML里面都是通过创建Template对象来修改一个Control是如何渲染的。
每一个Control类都有自己相应的Template类。
对于列表控件、譬如 GuiTextList、GuiListView、GuiTreeView 和 GuiVirtualDataGrid 等,除了Template以外,还有ItemTemplate。Template和ItemTemplate是可以分开指定的。Template确立了整个控件的外观,而ItemTemplate确定了每一个列表项的外观。
如果需要对容器的内容做数据绑定的话,那么需要使用上述4个控件的Bindable版本,分别是它们的子类:GuiBindableTextList、GuiBindableListView、GuiBindableTreeView 和 GuiBindableDataGrid 。在使用这些控件的时候,可以通过在XML里面的Workflow脚本——其实通常就是
<GuiBindableListView ItemSource-eval="ViewModel.Something"/>
这种简单的表达式——把一个C++的容器对象绑定到ListView上。每个ListViewItem拿到的容器的每一个对象,可能最终类型是不一样的。GacUI还提供了一个功能,你可以通过给ListView的ItemTemplate指定一系列的Template对象,通过在XML里面写好的这些Template的构造函数的参数的类型,来让ListView决定到底要使用哪个Template。于是一个异构的列表就这么轻松的造出来了。
Compositions 和 Controls 的生命周期
你们可能会注意到,Control并不在这一层里面。这是正确的。因为整个窗口就是由Element和Composition共同组成的一张超大的动态矢量图。每一个Control负责管理这颗Composition树的一些子树,每一个Control会告诉你他最外层的Composition和用来做容器的Composition分别是什么,然后把Control放进Composition、把Composition放进、把Control放进Control的这些动作,实际上都是在操作Composition。在实际的代码里面,你的确也是首选获取Control相应的Composition,然后去操作Composition的。
因此Control和Composition并不是平级的,你可以认为Control对于Composition使用了Builder和Facade模式,让你更容易的操作GUI。
当然这种做法对整个GacUI对象的生命周期会有一些影响。当你在C++代码里面delete一个Composition的时候,他会把下面的所有Composition子节点一起delete。当你在C++代码里面delete一个Control的时候,他会把下面所有的Control子节点,还有对应的所有Composition一起delete。
所以这个时候就会有一个疑问,那delete一个Composition的时候,如果Composition子节点上有Control怎么办?为了解决这个问题,我提供了这样的两个函数:SafeDeleteComposition 和 SafeDeleteControl 。
另外值得一提的是,如果你直接delete一个Control(通常情况下是你用完了一个 GuiWindow 直接把它删掉),他会先删掉整棵Composition树,然后再删除Control树。所以自己开发的Control在析构函数里面,千万不能访问Composition,否则直接GG。
XML Resource Compiler
GacUI目前提供的XML资源文件,支持让你构造Window、UserControl、Template、类似CSS那样的InstanceStyle(主要通过XPath来批量设置XML的属性,比选择器好用多了,而且精确控制起来更不费脑)和一些共享的Workflow脚本。共享的Workflow脚本可以用来定义一些窗口的逻辑代码,还有MVVM模式需要的ViewModel的接口和数据结构。
当你准备好一个XML资源的时候,Release里面提供的GacGen.exe会帮你把XML资源编译成一个二进制的资源文件,还有一系列的C++代码。生成的C++代码模拟了C#的partial class的能力,让你可以像Windows Forms一样,准备控件的事件处理,还有在窗口初始化的时候做一些任务等等。而且当你的XML需要更新的时候,GacGen.exe重新生成的C++代码会跟你修改后的那部分自动合并。
使用Workflow脚本写的ViewModel相关的接口和数据结构,也会被一并生成C++代码。在构造一个带有MVVM模式的窗口的时候,你只需要继承一下ViewModel接口,然后把这个类的实例当做窗口的参数填进去就好了。所有生成的代码都是强类型的,如果你对象给错了,会直接无法编译。特别安全。
目前GacUI把所有的用来构造窗口的那部分XML,在编译之后都转成了Workflow脚本的字节码,写进了二进制资源文件里面(这项功能将包含在即将到来的下一个Release里面)。窗口在初始化的时候,会去资源文件里面找到相应的脚本来运行,从而按照要求创建控件和data binding。
在后续的开发过程中,我还将为XML资源提供Visual State、Animation、State Machine和多语言字符串资源等重要部件。明年还计划让Workflow脚本可以被编译成C++,不仅可以大幅度的提高编写出来的GacUI程序的性能,还可以通过让你再也不需要链接GacUIReflection,让你的二进制文件的尺寸缩小到1/8。
当然了,还是会有一小部分情况是无法让你完全放弃在二进制文件里面带元数据的,举个例子,如果你编写出来的程序需要支持带GUI的插件,那么为了加载那些已经被编译成二进制资源的、在发布了之后用户自行制作的GacUI窗口,那你还是要保留反射的功能。不过这种需求在广大的GUI程序里面还是比较罕见的。
值得一提的是,GacUI的data binding的功能十分强大,你可以使用任何满足语法要求的Workflow脚本表达式(基本上就像C#一样丰富)来从ViewModel和控件之间做数据绑定。举个简单的例子,你完全可以写三个文本框,然后让第三个文本框永远等于前两个文本框的数字之和,并且在输入错误的情况下报错:
<SinglelineTextBox ref.Name="textBox1" Text="0"/>
<SinglelineTextBox ref.Name="textBox2" Text="0"/>
<SinglelineTextBox Text-bind="(cast int textBox1.Text) + (cast int textBox2.Text) ?? '<ERROR>'" />
这个例子要用WPF或者其他GUI框架来写就很蛋疼。那么我在编译XML资源的时候是怎么处理这个表达式的呢?其实这主要使用了语言爱好者们非常熟悉但是总是搞不明白的CPS变换(跟各种语言的玄乎的coroutine在编译的时候其实使用了基本相同的手法),然后把这种pull的代码转变成push的代码,这样就可以在textBox1的TextChanged发生的时候,跟换存起来的其他没有变化的属性的计算后的值(如cast int textBox2.Text)一起,做最少的计算,最后写到第三个控件的Text属性里面。
你还可以在这个表达式里面引用你在资源文件里面提供的Workflow脚本,或者干脆引用你自己用C++写的类和库函数,来帮助你做一些不属于ViewModel但是却十分蛋疼的、GUI相关的功能。举个例子,写一个统计学生成绩的程序,你可能需要给学生分优良中差。显然如何描述一个等级,使用中文和英文的方法就不一样。然而这并不是ViewModel的功能,ViewModel应该只负责计算等级,然后GUI再根据用户使用的系统所提供的语言信息来决定到底要如何显示。
这部分你就可以从ViewModel中间分离开,独立的写成Workflow脚本、XML资源或者C++代码,从而在定义窗口的XML里面使用。这样整个架构分层清晰、测试起来容易、而且需求变更的时候还特别好改。
尾声
这篇文章主要介绍了在使用GacUI的过程中需要了解的一些关于GacUI的体系架构的知识。里面的每一个知识点都会陆续在接下来的文章里面详细描述。除此之外,我还会偶尔写一些文章来介绍GacUI的、外部不可见的、跟实现紧密相关的内部架构,以及需要用到的一些编译原理、设计模式等知识,敬请关注。