查看原文
其他

GacUI 基本概念(一) by Vczh

vczh SegmentFault 2019-06-11

【编者按】轮子哥(Vczh)长期开发跨三大 PC 平台的 GUI 库,本文主要讲的是 GacUI 的体系架构。

本来是想从如何建立 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 + Composition

在 GacU I里面 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 和 ItemTemplat是可以分开指定的。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 的、外部不可见的、跟实现紧密相关的内部架构,以及需要用到的一些编译原理、设计模式等知识,敬请关注。


-EOF-


阅读原文 到轮子哥的文章里了解更多 GacUI 的事儿。


专业的开发者技术社区

多样化线上知识交流

丰富线下活动和给力工作机会

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存