Chrome学习笔记(二):UI组件,皮肤引擎 —— 基础设施篇
Chrome的UI是很奇妙的,因为看起来能很好的跨平台,而且可以很好的兼容各个平台的特性,比如在Mac下最小化和关闭按钮在左侧,还兼容全屏的特性,在Linux上,也能加载GTK的外框,外加现在Chrome在推的Aura,更是直接接管了桌面合成器。。。这一切让人不得不想去弄清楚Chrome到底是怎么来实现这么强大的UI呢?有一句话我非常喜欢:“源码面前,了无秘密”,读了几天的源代码,也总结些东西,以免后面忘记。
对使用比较感兴趣的朋友也可以先看看如何使用这套皮肤引擎,再来回头看实现。
1. 基本概念
由于Chrome不满Windows没有自带好用的皮肤引擎,所以在一顿折腾之后,就自己设计了一套平台无关的皮肤引擎:Views。它是一个典型的DirectUI,关于它,Chromium的网站上有三篇文章对其的设计进行了阐述:Views framework,views Windowing system,NativeControls。这三篇文章虽然是在09年的时候写的,但是后面的设计基本没有太大的改动,所以还是比较有用的,感兴趣的童鞋可以先看看。
在Chrome皮肤引擎里面两个非常重要的概念:Widget和View。
Widget对应着一个原生的窗口,而View对应着窗口里面的一个控件,如容器,Button,Tab等等。这样在Widget和View之上,Chrome搭建起了自己跨平台的皮肤引擎。
关于跨平台,这里可能大家需要注意的一点是:这个皮肤引擎并不会封装的非常的完善,这里从Chromium的文档上对于Views framework的定义可以看出来:Our UI layout layer used on Windows/Chrome OS,能在Windows和ChromeOS上用用就可以了。而且Chrome团队也在文档中坦言,支持跨平台会遇到很多问题,如不好处理特殊的窗口消息等等。
2. 基础库:base
基本上每个程序库都有着自己的基础库,Chrome的皮肤引擎也不例外,在src/ui/base这个目录下放着的就是它的基础库。
由于已经有了Chrome本身的基础库,和一些第三方的组件的支撑,这个基础库下面主要放的就只是一些和UI相关的基本定义和基础的功能实现了。
以下是他所包含的目录和其对应的功能:
- accelerators:快捷键的处理。
- animation:动画效果的抽象,这里并不管绘制,只是负责计算动画效果的进度。
- clipboard:平台无关的剪贴板操作。
- cocoa:Mac上和cocoa相关的代码。
- dragdrop:和拖拽相关的代码,并在内部封装了平台无关的统一数据传输接口。
- glib:一些Linux上用的代码。
- gtk:Linux上使用的和gtk相关的封装,简化事件处理什么的。
- ime:和输入法相关的代码。
- keycodes:平台无关的键盘的KeyCode的封装。
- l10n:本地化工具函数。
- models:定义了一些控件的数据接口。
- range:用于表示范围的基本类型。
- strings:用于本地化的字符串表。
- touch:触屏相关的代码?
- wayland:Linux和wayland相关的代码。
- win:Windows下才会用到的代码,里面包含Windows原生窗口的封装,IME的处理等等。
- x:Linux下和X11相关的代码。
3. 窗口封装:Widget
一个Widget对应着一个真实的窗口,在Windows下它就对应一个HWND。
其相关的代码在src/ui/views/widget目录下。
为了将平台相关的窗口细节隐藏在Widget内部,Chromium为平台相关的窗口抽取出了一个接口:NativeWidgetPrivate,用以封装平台相关的代码,而在里面将平台相关的消息转化为平台无关的消息,再通过NativeWidgetDelegate回调出来。而NativeWidgetDelegate除了接收回调以外,他还有很多用以指定原生窗口风格的回调函数,供NativeWidgetPrivate创建时调用。
这些处理过后的消息,就可以被统一来处理了。这里Widget本身作为NativeWidgetDelegate接收并处理这些消息或者分发给所有的控件,也就是马上要提到的View,由这些控件来触发真实的逻辑。
这里我们可以发现一件事情:那就是为什么没有看到mac平台下的NativeWidget呢?答案估计你也猜到了:那就是。。。。mac下面用cocoa重写了一套,貌似压根就没有实现widget。=.=|||
4. 界面元素:View
Chrome的开发者说:Windows既然没有自带好用的界面库,那我们就自己搞!所以在对原生窗口抽象完毕之后,接下来的工作就是搭建自己的界面了。这个就是View。
其相关的代码主要分布在src/ui/views和src/ui/views/window目录下。
4.1. Windows原生窗口的特征
我们先回忆一下,在开发原生的Windows程序时的窗口结构:
程序一般都有一个主窗口,主窗口下面有子窗口或者各种控件,他们形成一个树形的关系,这些我们在Spy++里面可以很好的观察到。
另外一个窗口的内容实际上分成两个部分:
- 非客户区:一个窗口只有一个非客户区,这部分包括标题栏,关闭按钮等等
- 客户区:这部分包括很多内容,按钮,工具条等等我们用到的控件
通过这些窗口和控件,我们搭建起了程序的主界面。
我们应该能想到,chrome要干的也是这件事情,所以现在我们不用看chrome的源代码,也能将他里面的代码猜个大概。
4.2. Chrome的实现
现在让我们来看Chrome是如何实现的。
为了方便理解各个不同的类的职责,我们首先来看看最后的层级关系,对照着这个关系来看代码。在Chrome的代码里面有一副很GEEK的字符图很形象的表示了这个关系:
在Chrome里面,各种不同的View成树形的关系组织在一起,他们的根节点就是Widget,Widget接收到系统原生的消息,并通过RootView将消息分发给下层的View,这里Widget和RootView是一一对应的。
下层的View主要分为三种:
- 用于表示整个窗体非客户区的NonClientView,负责NCHitTest和设置窗口边框大小。他也是其他两种View的父,原因很简单:他管着整个窗体的边框,所以其他的View必须是他的子。
- 用于表示非客户区的内容的NonClientFrameView,负责绘制非客户区里面的元素,如标题栏,关闭按钮等等。
- 用于表示客户区和其内容的ClientView,负责生成各种窗口元素。
另外Chromium还提供了几种不同的默认的非客户区方便编程:
- NativeFrameView,用于生成默认的窗口。
- DialogClientView,用于生成对话框的窗口。
- CustomFrameView,用于自绘窗口边框。
通过这样的一个关系,chrome将所有的界面元素都管理了起来。
5. 绘制封装:gfx
在封装好了界面元素之后,如何实现跨平台统一的绘制呢?这就是gfx要做的事情。
其相关代码主要分布在src/ui/gfx目录下。
gfx里面其实封装了不少和界面绘制相关的内容,其中最重要的就是Canvas。
为了实现跨平台的界面绘制,Chrome定义了一个Canvas的接口,来进行绘制的操作。我们在View的接口中可以看到一个View::Paint的函数,这个函数就是主要来控制绘图的。我们拿Windows来举例,窗口绘制的回调逻辑主要分如下这么几步:
- 在原生窗口收到了WM_PAINT消息之后,NativeWidget会对其进行处理,将其转化为Chrome内部的事件,并利用系统原生的绘图方式生成Canvas,回调给Widget进行分发。
- Widget在Widget::OnNativeWidgetPaint中将消息分发给其对应的RootView,由其分发给自己和各个子View。
- 每个View在自己的View::OnPaint函数中进行重绘。
为了实现Canvas,Chrome使用Skia作为其2D图形渲染库,来接管所有图形的绘制。而绘制文字的部分,则在不同平台上使用其原生的Api来实现,如在Windows上,则使用Api DrawText进行绘制。
另外在gfx里面还有一个很重要的类,叫做NativeTheme,这个类中保存这当前系统的主题设置,甚至还可以用它来直接画系统默认的一些风格样式。比如:Windows窗口中右下角的表示窗口可以拖拽的小三角。在Windows下,NativeThemeWin优先会使用uxtheme.dll提供的Api进行风格绘图,如果没有这个dll,chrome会使用自定义的风格进行绘制。
6. 布局策略:LayoutManager
写过界面的人都知道,皮肤布局是一件很繁琐的事情,每个元素如何排布,可能都有其各自的策略,而且每个窗口所包含的元素也不尽相同,所以chrome中可以为每一个View创建了一个专门用于控制布局的LayoutManager。这其实是一个典型的策略模式,将复杂且多变的布局封装起来。
其相关代码主要分布在src/ui/views/layout目录下。
在layout目录中,可以发现Chrome还提供几种不同的布局策略:
- FillLayout,用于将第一个子View保持和当前View一样大的策略。
- GridLayout,将子View排布成表格状。
- BoxLayout,排布成一个贴一个的格子。
现在我们可以猜到,RootView肯定使用的是FillLayout,从而让NonClientView永远保持和其本身一样大。
当然一个View也可以没有LayoutManager ,这样除非你重载View的Layout函数,或者使用其他的方法来主动布局,不然里面的元素就不会布局了。
7. 焦点管理:FocusManager
一旦所有界面元素都自己来管理了,那么很明显,这些元素的焦点也就需要自己来管理了。关于焦点的相关代码主要分布在src/ui/views/focus目录下。
7.1. 焦点问题的类型
在看焦点管理的时候,我们需要先意识到一个问题,焦点虽然说起来简单,谁接收鼠标事件谁就是窗口的焦点,但是对于Chrome这种DirectUI的皮肤引擎来说,焦点分为两种类型:
- 原生窗口的焦点:原生的窗口在被点击的时候会被赋予焦点,皮肤引擎必须能够很好的响应这些事件。
- 窗体中元素的焦点: 对于窗口中的所有元素,由于他们都不包含句柄,所以的焦点和键盘消息需要Chrome自己来实现转发。
为了实现上面两种类型的焦点,Chrome建立了一个专门用于管理焦点的类:FocusManager。Chrome会为每一个Widget建立一个对应的FocusManager。利用他来处理这两种焦点问题。
7.2. 和焦点有关的消息的分发
和焦点有关的消息分发流程主要包含这么几步:
- 当一个原生窗口在有焦点的状态时,系统会将发生的键盘和部分鼠标输入交给这个窗口来处理。
- 在窗口收到消息时,NativeWidget会首先处理这些消息并将其转化为KeyEvent,交给Widget来处理。(NativeWidgetWin::OnKeyEvent)
- Widget将此消息转交给他所对应的RootView由他来分发消息。(Widget::OnKeyEvent)
- RootView从当前Widget所对应的FocusManager中获取出当前的焦点窗口分发消息。(RootView::OnKeyEvent)
- 焦点窗口处理消息。
7.3. 焦点变化的处理
在Widget接收到原生窗口的焦点变化的时候(Widget::OnNativeFocus),他会回调WidgetFocusManager来广播焦点变化的事件,但是从皮肤引擎的代码里面来看,默认的,没有类会关心这个事件。
对于窗体元素的焦点,如果某个元素获取了焦点,那么这个元素对应的View会调用当前View所在Widget的FocusManager::SetFocusedView方法,将自己设为焦点。此时FocusManager也会将这个消息广播给其他关心焦点变化的事件的监听者。但是在View里面只有DialogClientView看上去比较关心这个事件。
7.4. 焦点与控件显示的关系
我们发现,这些焦点变化的消息居然没有人关心?那么焦点是怎么影响控件显示的呢?
这里会涉及到两个不同的,但是容易混淆的概念:Focus和Active。这里有一个较为简单的区分这两个概念的方法,当然不一定完全对:
- Focus:可以是非顶层窗口,主要影响键盘鼠标等消息的接收。
- Activate:必须是顶层窗口,影响窗口绘制。
当我们点击非Chrome窗口导致Chrome窗体发生颜色变化,这个主要是由于Active消息对应的处理。
当地址栏在可以输入时会出现一个边框,这个是由Focus来控制的。这个控制Chrome其实实现很简单,通过判断FocusManager中的FocusedView是不是自己来进行不同种类的绘图。
8. Chrome皮肤引擎总结
到此为止Chrome皮肤引擎的基础设施算是基本写完了。总的来说,这一套皮肤引擎算是一个比较容易理解的跨平台的DirectUI设计了。
在这一整套基础设施上,Chrome开始搭建起自己的一套控件库,再在这些内容的基础上搭建起自己的主界面。这些后续再继续写。
原创文章,转载请标明出处:Soul Orbit本文链接地址:Chrome学习笔记(二):UI组件,皮肤引擎 —— 基础设施篇