Chrome学习笔记(二):UI组件,皮肤引擎 —— 基础设施篇

Chrome的UI是很奇妙的,因为看起来能很好的跨平台,而且可以很好的兼容各个平台的特性,比如在Mac下最小化和关闭按钮在左侧,还兼容全屏的特性,在Linux上,也能加载GTK的外框,外加现在Chrome在推的Aura,更是直接接管了桌面合成器。。。这一切让人不得不想去弄清楚Chrome到底是怎么来实现这么强大的UI呢?有一句话我非常喜欢:“源码面前,了无秘密”,读了几天的源代码,也总结些东西,以免后面忘记。

对使用比较感兴趣的朋友也可以先看看如何使用这套皮肤引擎,再来回头看实现。

1. 基本概念

由于Chrome不满Windows没有自带好用的皮肤引擎,所以在一顿折腾之后,就自己设计了一套平台无关的皮肤引擎:Views。它是一个典型的DirectUI,关于它,Chromium的网站上有三篇文章对其的设计进行了阐述:Views frameworkviews Windowing systemNativeControls。这三篇文章虽然是在09年的时候写的,但是后面的设计基本没有太大的改动,所以还是比较有用的,感兴趣的童鞋可以先看看。

在Chrome皮肤引擎里面两个非常重要的概念:WidgetView

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,由这些控件来触发真实的逻辑。

chrome-native-widget

这里我们可以发现一件事情:那就是为什么没有看到mac平台下的NativeWidget呢?答案估计你也猜到了:那就是。。。。mac下面用cocoa重写了一套,貌似压根就没有实现widget。=.=|||

4. 界面元素:View

Chrome的开发者说:Windows既然没有自带好用的界面库,那我们就自己搞!所以在对原生窗口抽象完毕之后,接下来的工作就是搭建自己的界面了。这个就是View

其相关的代码主要分布在src/ui/viewssrc/ui/views/window目录下。

4.1. Windows原生窗口的特征

我们先回忆一下,在开发原生的Windows程序时的窗口结构:

程序一般都有一个主窗口,主窗口下面有子窗口或者各种控件,他们形成一个树形的关系,这些我们在Spy++里面可以很好的观察到。

另外一个窗口的内容实际上分成两个部分:

  • 非客户区:一个窗口只有一个非客户区,这部分包括标题栏,关闭按钮等等
  • 客户区:这部分包括很多内容,按钮,工具条等等我们用到的控件

通过这些窗口和控件,我们搭建起了程序的主界面。

我们应该能想到,chrome要干的也是这件事情,所以现在我们不用看chrome的源代码,也能将他里面的代码猜个大概。

4.2. Chrome的实现

现在让我们来看Chrome是如何实现的。

为了方便理解各个不同的类的职责,我们首先来看看最后的层级关系,对照着这个关系来看代码。在Chrome的代码里面有一副很GEEK的字符图很形象的表示了这个关系:

chrome-view-hierarchy

在Chrome里面,各种不同的View成树形的关系组织在一起,他们的根节点就是Widget,Widget接收到系统原生的消息,并通过RootView将消息分发给下层的View,这里Widget和RootView是一一对应的。

下层的View主要分为三种:

  • 用于表示整个窗体非客户区的NonClientView,负责NCHitTest和设置窗口边框大小。他也是其他两种View的父,原因很简单:他管着整个窗体的边框,所以其他的View必须是他的子。
  • 用于表示非客户区的内容的NonClientFrameView,负责绘制非客户区里面的元素,如标题栏,关闭按钮等等。
  • 用于表示客户区和其内容的ClientView,负责生成各种窗口元素。

另外Chromium还提供了几种不同的默认的非客户区方便编程:

通过这样的一个关系,chrome将所有的界面元素都管理了起来。

5. 绘制封装:gfx

在封装好了界面元素之后,如何实现跨平台统一的绘制呢?这就是gfx要做的事情。

其相关代码主要分布在src/ui/gfx目录下。

gfx里面其实封装了不少和界面绘制相关的内容,其中最重要的就是Canvas。

为了实现跨平台的界面绘制,Chrome定义了一个Canvas的接口,来进行绘制的操作。我们在View的接口中可以看到一个View::Paint的函数,这个函数就是主要来控制绘图的。我们拿Windows来举例,窗口绘制的回调逻辑主要分如下这么几步:

  1. 在原生窗口收到了WM_PAINT消息之后,NativeWidget会对其进行处理,将其转化为Chrome内部的事件,并利用系统原生的绘图方式生成Canvas,回调给Widget进行分发。
  2. Widget在Widget::OnNativeWidgetPaint中将消息分发给其对应的RootView,由其分发给自己和各个子View。
  3. 每个View在自己的View::OnPaint函数中进行重绘。

为了实现Canvas,Chrome使用Skia作为其2D图形渲染库,来接管所有图形的绘制。而绘制文字的部分,则在不同平台上使用其原生的Api来实现,如在Windows上,则使用Api DrawText进行绘制。

chrome-ui-canvas

另外在gfx里面还有一个很重要的类,叫做NativeTheme,这个类中保存这当前系统的主题设置,甚至还可以用它来直接画系统默认的一些风格样式。比如:Windows窗口中右下角的表示窗口可以拖拽的小三角。在Windows下,NativeThemeWin优先会使用uxtheme.dll提供的Api进行风格绘图,如果没有这个dll,chrome会使用自定义的风格进行绘制。

chrome-native-theme

6. 布局策略:LayoutManager

写过界面的人都知道,皮肤布局是一件很繁琐的事情,每个元素如何排布,可能都有其各自的策略,而且每个窗口所包含的元素也不尽相同,所以chrome中可以为每一个View创建了一个专门用于控制布局的LayoutManager。这其实是一个典型的策略模式,将复杂且多变的布局封装起来。

其相关代码主要分布在src/ui/views/layout目录下。

在layout目录中,可以发现Chrome还提供几种不同的布局策略:

  • FillLayout,用于将第一个子View保持和当前View一样大的策略。
  • GridLayout,将子View排布成表格状。
  • BoxLayout,排布成一个贴一个的格子。
    chrome-ui-layout

现在我们可以猜到,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. 和焦点有关的消息的分发

和焦点有关的消息分发流程主要包含这么几步:

  1. 当一个原生窗口在有焦点的状态时,系统会将发生的键盘和部分鼠标输入交给这个窗口来处理。
  2. 在窗口收到消息时,NativeWidget会首先处理这些消息并将其转化为KeyEvent,交给Widget来处理。(NativeWidgetWin::OnKeyEvent
  3. Widget将此消息转交给他所对应的RootView由他来分发消息。(Widget::OnKeyEvent
  4. RootView从当前Widget所对应的FocusManager中获取出当前的焦点窗口分发消息。(RootView::OnKeyEvent
  5. 焦点窗口处理消息。

7.3. 焦点变化的处理

在Widget接收到原生窗口的焦点变化的时候(Widget::OnNativeFocus),他会回调WidgetFocusManager来广播焦点变化的事件,但是从皮肤引擎的代码里面来看,默认的,没有类会关心这个事件。

对于窗体元素的焦点,如果某个元素获取了焦点,那么这个元素对应的View会调用当前View所在Widget的FocusManager::SetFocusedView方法,将自己设为焦点。此时FocusManager也会将这个消息广播给其他关心焦点变化的事件的监听者。但是在View里面只有DialogClientView看上去比较关心这个事件。

7.4. 焦点与控件显示的关系

我们发现,这些焦点变化的消息居然没有人关心?那么焦点是怎么影响控件显示的呢?

这里会涉及到两个不同的,但是容易混淆的概念:FocusActive。这里有一个较为简单的区分这两个概念的方法,当然不一定完全对:

  • Focus:可以是非顶层窗口,主要影响键盘鼠标等消息的接收。
  • Activate:必须是顶层窗口,影响窗口绘制。

当我们点击非Chrome窗口导致Chrome窗体发生颜色变化,这个主要是由于Active消息对应的处理。

当地址栏在可以输入时会出现一个边框,这个是由Focus来控制的。这个控制Chrome其实实现很简单,通过判断FocusManager中的FocusedView是不是自己来进行不同种类的绘图。

8. Chrome皮肤引擎总结

到此为止Chrome皮肤引擎的基础设施算是基本写完了。总的来说,这一套皮肤引擎算是一个比较容易理解的跨平台的DirectUI设计了。

在这一整套基础设施上,Chrome开始搭建起自己的一套控件库,再在这些内容的基础上搭建起自己的主界面。这些后续再继续写。

同系列文章:
原创文章,转载请标明出处:Soul Orbit
本文链接地址:Chrome学习笔记(二):UI组件,皮肤引擎 —— 基础设施篇