GacUI:跨平台和渲染器
https://github.com/vczh/GacUIBlog
UI库跨平台的方法无非就是每个平台写一次。而如何把更多的共同点抽取出来,尽量的减少每个平台写一次的部分,是每一个跨平台的UI库的重点之一。GacUI的设计比较直接,所有平台相关的部分被集中到了几个接口里。每一次把GacUI一直到一个新的平台,就把所有的这些接口都重新实现一遍。目前GacUI能在Windows与macOS跑起来,以后还要逐渐支持UWP、命令行以及通过WASM让他跑到浏览器上去。简单地讲,所有的系统调用都从INativeController
开始,这是一个单例。通过观察这个接口提供的各个服务,它本身就会慢慢引导你实现各种GacUI需要的与系统交互的功能,包括创建窗口等等。
其中最值得提到的是渲染的部分。实际上我并不想抽象渲染器。因为每一个渲染器的运行过程实在是大相径庭,如果为了开发简便而过度抽象,势必会牺牲某些平台的性能。不过我还是打算在2.0里推出一个2D绘图API,这组API就只是用来给app画画的,跟绘制控件本身并没有关系。设计这套架构的一个挑战,是渲染器本身也可能是跨平台的。一个平台上面可以有多个渲染器(譬如Windows上你就可以用GDI和Direct2D),一个渲染器也可能跨多个平台(譬如OpenGL)。所以为系统调用开发的代码,以及为渲染器开发的代码,势必本身也是解耦的。你不能说明明都是OpenGL,但是为Windows和Linux你都要各写一遍使用OpenGL绘制控件的代码,那就显得太愚蠢了。
设计都得从需求谈起,要讲GacUI如何把系统调用和渲染器都从实现里面隔离开,就得谈一谈GacUI是如何渲染控件的。
几次设计GUI库的尝试
第一次设计GUI库
我第一次做通用的控件库还是大一的时候。以前受到VB6以及Delphi的影响,对UI库的理解无非都是,用户创建一大堆控件接成一棵树,窗口修改大笑的时候自己去想办法布局,IO操作来了就直接dispatch到控件里,需要绘图了就从Windows地Paint函数一直调用洗澡去,每个控件自己负责自己的绘制。后来我尝试直接从Win32 API开始写app,发现也是这样的,那个时候就没怎么多想。因此那年第一次做一个UI库的时候,我也就理所当然地这么设计了,秉承着为开发游戏写library的精神,我毅然决然地使用了OpenGL,最后在做高级文字渲染的时候被劝退了。大三的时候我又不死心,又做了一遍。但是我的设计水平并没有得到提高,仅仅因为技术水平的提高而实现了更复杂的UI,于是得到了下面的东西,然后项目流产。
上面的设计有一个显著的坏处,在把Win32 API发挥到极致的.net Windows Forms里面就体现了出来,控件和布局混为一谈。最典型的就是TableLayoutPanel
控件。这是一个控件,但是这个控件是没有交互功能的,他唯一的目标就是做layout。但是因为他被建模成了一个控件,就被赋予了渲染的义务,你依然可以修改它的边框和背景颜色什么的。第二个显著的坏处,就是既然每一个控件都要负责绘制自己,那不管你选择什么渲染技术,你都要把这套API需要的所有东西都带进控件里。于是在当年的这套控件库里面,每一个控件都会有一大堆OpenGL的概念放在private成员变量里。所有的代码被混淆了起来,越写越复杂。由于我大一的时候,算法数据结构设计模式什么都还没开始接触,最后便陷入了混乱,于是项目流产。当然就算是流产了,那也是像模像样的,只是再也无法重构下去了。
这个项目留下来的遗产,就是现在GacUI里面的WinNativeWindow.cpp。有些人看着这份文件可能会问,为什么抽象了操作系统的接口,还要在里面再封装一次Win32 API。其实就是因为这份代码并不是从头写的。
第二次设计GUI库
于是一个很自然的想法,就是要把绘图的部分从控件里拿掉。想拿掉就得彻底,所有OpenGL的东西都不能出现在控件里。但是控件又必须把自己的信息都传递过去,否则就没办法绘图了。而且窗口也是特殊的,因为所有的绘图设备都是窗口创建的,而控件也仍然需要拿到这些设备才能绘制。因此当初我就做了一个实验,把使用Win32 API创建窗口的部分抽象掉。UI库需要一个本地窗口的时候,拿到的就只是一个窗口的抽象接口。
然后开始创建控件,每一个控件的皮肤也被抽象成了一个接口。譬如说按钮的皮肤,基本上就是一个被动接受信息的接口,他会不断地接受从控件来的命令,譬如更改字体啊,更改文字啊,更改颜色,更改位置啊啊。其次按钮放到不同的容器里面的时候,皮肤接口也会接到通知,这个信息用来把控件告知的位置换算成以窗口的一个角作为原点的坐标系。但是皮肤自己要绘制,其实还是要知道UI库用的是OpenGL。于是窗口的抽象接口还是留了一个后门,会暴露自己的OpenGL相关的信息。因此这种抽象是不够彻底的,说到底只是一次隔离,控件不需要关心渲染器,但是皮肤不仅要关心渲染器还要关心控件。
GacUI现在的INativeController