Chrome学习笔记(三):UI组件,皮肤引擎

  • Post author:
  • Post category:其他



原创文章,转载请注明:

转载自

Soul Apogee



本文链接地址:


Chrome学习笔记(三):UI组件,皮肤引擎 —— 控件库

这篇文章是接着上篇文章继续聊的,Chrome的代码实在太多,每一个东西单拿出来都可以说很很多,

单就一个breakpad都说了两篇

。恩,不过也许是我太啰嗦了。




1. UI控件库(Control)简介

我们知道Chrome做这一套皮肤引擎是为了替换掉Windows原生的控制UI的方式,所以这个皮肤引擎上怎么能没有控件呢?所以在建立好各种基础的UI元素和默认处理之后,Chrome在上面开始封装各种基础的控件,比如button等等。

其相关代码主要分布在

src/ui/views/control

目录下。

为了进一步的方便开发,Chrome的UI控件库中包括了很多基础的控件,这些控件现在包括如下几种:


  • button

    :基本的按钮控件和其常用的变种,类似于CButton。

  • combobox

    :下拉列表和原生的下拉列表,类似于CComboBox。

  • menu

    :菜单。

  • scrollbar

    :滚动条。

  • tabbed_pane

    :封装了自绘的和系统原生的Tab分页控件,类似于CTabCtrl。

  • table

    :封装列表控件,类似于CListCtrl。

  • textfield

    :封装输入控件,类似于CEdit。

  • tree

    :树形控件,类似于CTreeCtrl。

  • 其他

    :Label,进度条,分栏等等等等。

这些控件中有一些并不一定是全部自绘的,而是使用系统原生的控件,比如tabbed_pane,tree和table。按照Chrome的文档来看,

Chrome团队应该并不喜欢使用系统原生的控件

,所以从长远来看,这些代码应该是中间代码,毕竟很好的实现一个这样的控件还是比较复杂的,所以Chrome就暂时使用着原生的控件。


另外还有一种我们在控件库中找不到,但是却十分重要的控件:容器。


Chrome的皮肤引擎有一个特点:万物皆容器。所有的控件都继承于一个同一个基类:View,所以所有的控件都可以有子元素。在Chrome里面,你可以建立一个其他什么都不做的View,只用它来排布他的子元素。用过

GTK

的朋友们肯定对

GtkHBox



GtkVBox

这个类有一定的印象,

这两个类对辅助控件的布局是很有帮助的

。在Chrome里面,你也可以使用类似的用法来辅助控件的布局,而且在

UI里面还提供了几种基础的布局方法

来帮助大家开发。




2. 实现方式

提供的控件确实比较全面,那么为了更好的帮助我们理解和使用这些控件,在使用这些控件之前,先让我们来看一下Chrome的UI控件的实现方法。


2.1. 自绘控件实现

我们知道自绘控件的关键是三个方面:绘制、数据提供和事件回调。所以Chrome在代码里面也就是针对着这样三个方面来实现他的封装。

真是熟悉的三个方面啊,想必很多朋友已经能对Chrome控件的实现方式猜个大概了,如果还对于

Chrome UI绘制机制

有一定了解,那么代码估计自己也能写出个大概了。

没错,就是

MVC模型

  • 使用

    Canvas

    来实现绘制的接口,在控件的OnPaint回调中进行自绘。
  • 采用MVC的设计思想,对于复杂的控件,如

    Tree



    Table

    等等,提取出Model接口和Controller接口,分别用于管理数据和处理事件回调并控制控件行为。

我们拿Tree来举例,Chrome将一个Tree分为三个部分:

TreeView



TreeModel



TreeViewController

  • TreeView主要用于绘制。现在TreeView已经被系统原生控件接管,但是在Chrome代码里面,我们依然能找到自绘的

    TreeView

  • TreeModel主要用于管理数据。
  • TreeViewController主要用于处理事件回调,控制控件行为,如:控制树中某一项能不能被编辑。


chrome-ui-control-tree:



chrome-ui-control-tree

这样Chrome就实现了自绘控件。


2.2. 与原生控件的兼容

由于Chrome的控件还有一部分控件是直接使用的系统原生的控件,所以就会牵涉到自绘控件和原生控件如何在View控件树兼容的问题。

一个很自然的解决方法就是

建立一个继承自View的原生控件基类

,而具体的控件和逻辑则放入他的子类。这个基类就是NativeControl,通过继承他,来将原生控件纳入Views的层次结构中。在Chrome的代码中,

Tree

就是这样来实现的。


chrome-ui-native-control:



chrome-ui-native-control

但是Chrome认为这样做存在问题,于是Chrome

对其结构进行了改进

,以求更好的支持跨平台和代码复用。

所以现在更多的控件的实现是建立一个Wrapper封装NativeControl,Wrapper的实现则继承自

NativeControlWin

,以便更加方便的控制控件,或者使用View进行替换,如

Combobox




3. 使用范例

好了,扯了这么多,我们来看一下如何使用这个皮肤引擎吧。




3.1. 建立一个工程

由于Chrome UI库和其他工程关联太紧,所以我们如果要建立一个测试工程其实并没有那么容易,我们可以在view_example_exe这个工程上直接进行修改,或者利用

gclient

生成一个测试工程。

  1. 打开src/ui/views/views.gyp,将views_examples_exe的描述段复制一份,粘贴在# target_name: views_examples_lib之后。
  2. 修改其工程名为你想要的工程名,如:view_test。
  3. 删除其source区域下

    除了.rc文件以外

    的所有源代码。
  4. 在dependencies中加入一项:views。
  5. 打开

    配置好的cygwin或者命令行

    ,进入chromium源代码根目录,也就是存放.gclient文件的目录,输入gclient runhooks。
  6. 重新打开src/ui/views/views.sln,我们就可以在(views)目录下,看到view_test的工程了。


chrome-add-ui-proj:



chrome-add-ui-proj


3.2. 准备工程

为了能让这个UI工程运行起来,我们需要写一些准备的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

#

include

"base/at_exit.h"

#include "base/command_line.h"

#include "base/message_loop.h"

#include "base/i18n/icu_util.h"

#include "ui/base/ui_base_paths.h"

#include "ui/base/resource/resource_bundle.h"

#include "ui/views/widget/widget.h"

#include "ui/views/widget/widget_delegate.h"

using


namespace


views;

void


test()

{




return


;

}

int


main(


int


argc,


char


** argv)

{




// 以下内容必不可少,如果不添加会导致程序无法运行



OleInitialize(NULL);


// Windows上必备,OLE初始化



CommandLine::Init(argc, argv);


// 初始化命令行参数



base::AtExitManager at_exit;


// 为MessageLoop所使用的,用于在退出时清理对象的工具类



ui::RegisterPathProvider();


// 注册UI组件所需要的路径,不然会出现资源找不到的问题



bool


icu_result = icu_util::Initialize();


// 注册ICU,用于国际化



CHECK(icu_result);



ui::ResourceBundle::InitSharedInstanceWithLocale(


"en-US"


);


// 初始化国际化资源包



// 以上内容必不可少,如果不添加会导致程序无法运行



// 初始化消息循环



MessageLoopForUI msg_loop;



test();



msg_loop.Run();



OleUninitialize();



return


0;

}

之后我们把测试代码都加载test()这个函数中就可以了,另外这里之后的代码可能会泄漏的问题,这里我们先暂时不去理会他,后面会单独聊。


3.3. 实现一个简单的窗口

建立好了工程之后,我们就可以添加代码了,先让我们来建立一个最简单的窗口:一个空白的Widget。

1
2
3
4
5

void


test()

{




// 创建窗口并显示



Widget::CreateWindowWithBounds(NULL, gfx::Rect(0, 0, 320, 240))->Show();

}

短短几行我们就创建了一个最基本的窗口了,但是这个窗口实在是。。。有点难看啊。。


chrome-base-ui:



chrome-base-ui


3.4. 添加一个按钮吧

既然难看,我们就来添加一些小控件到里面吧,先加一个小按钮吧。

首先,让我们来回想一下Chrome UI的元素结构,还记得这幅图么:


chrome-view-hierarchy:



chrome-view-hierarchy

所以为了增加按钮,我们需要创建一个按钮的控件,并且为Widget的ClientView生成一个ContentsView来保存我们的这个按钮。

首先我们先添加几个头文件:

1
2

#include "ui/views/controls/button/text_button.h"

#include "ui/views/layout/fill_layout.h"

另外我们添加了一个TestWidgetDelegate的类,用于设置Widget样式并且提供ContentsView。另外我们还给它添加了一个FillLayout,让按钮与窗口保持一样的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

class


TestWidgetDelegate :


public


WidgetDelegateView,


public


ButtonListener

{


public


:



TestWidgetDelegate() {




// 设置窗口背景,让其不为黑色



set_background(Background::CreateStandardPanelBackground());



// 添加子按钮



Button *button =


new


TextButton(


this


, L


"test"


);



button->set_tag(1);


// Button的tag是用于区分按钮的,在回调时,通过这个tag来区分不同的按钮



AddChildView(button);



// 设置子按钮保持和窗口一样大小的布局方式



SetLayoutManager(


new


FillLayout);



}



virtual


~TestWidgetDelegate() {}



// 可以变化大小



virtual


bool


CanResize()


const


{


return


true


; }



// 可以最大化



virtual


bool


CanMaximize()


const


{


return


true


; }



// 初始化焦点



virtual


View* GetInitiallyFocusedView() OVERRIDE {


return


this


; }



// 提供ContentsView



virtual


View* GetContentsView() OVERRIDE {


return


this


; }



// 窗口关闭是退出消息循环



virtual


void


WindowClosing() OVERRIDE { MessageLoopForUI::current()->Quit(); }



// 如果按钮发生点击,则回调此事件



virtual


void


ButtonPressed(Button* sender,


const


views::Event& event) {




if


(sender->tag() == 1) {


// 回调时,通过这个tag来区分不同的按钮



// .....



}



}

};

另外生成Widget的代码也要做少许的改动:

1
2
3
4
5

void


test()

{




// 创建窗口并显示



Widget::CreateWindowWithBounds(


new


TestWidgetDelegate, gfx::Rect(0, 0, 320, 240))->Show();

}

编译运行,可以看到一个按钮已经出现啦~


chrome-base-ui-with-button:



chrome-base-ui-with-button


3.5. 添加一个原生控件

好,我们已经可以添加一个自绘的控件了,现在让我们来试着添加一个系统原生的控件吧。

由于Chrome是在View的基础上封装的原生控件,所以添加原生控件也并非难事。比如我们现在来添加一个Tab栏,我们只需要添加一个头文件,再稍稍修改一下TestWidgetDelegate的构造函数就可以了。

添加头文件:

1

#include "ui/views/controls/tabbed_pane/tabbed_pane.h"

修改TestWidgetDelegate的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

TestWidgetDelegate() {




// 设置窗口背景,让其不为黑色



set_background(Background::CreateStandardPanelBackground());



// 添加Tab栏



TabbedPane *tabbedpane =


new


TabbedPane();



AddChildView(tabbedpane);


// 此处创建完成需要立刻添加到View中,因为其后端实现是在此时被创建的,如果不添加,调用AddTab接口会发生崩溃。



// 添加子按钮



Button *button =


new


TextButton(


this


, L


"test"


);



button->set_tag(1);


// Button的tag是用于区分按钮的,在回调时,通过这个tag来区分不同的按钮



tabbedpane->AddTab(L


"Tab1"


, button);



// 设置子按钮保持和窗口一样大小的布局方式



SetLayoutManager(


new


FillLayout);

}

这里需要注意的一点是:Chrome很多原生控件的真实实现类都是在View层次关系发生改变的时候创建的,所以在Tab等原生控件创建完成之后,需要马上将其加入View中,不然后续调用其接口就会发生崩溃。

让我们来看看最终的效果:


chrome-base-ui-with-tab:



chrome-base-ui-with-tab


3.6. 控件的生命周期

Chrome控件的生命周期是比较晦涩的,在上面的代码,我们可以看见我们new出来了很多对象,但是从未调用过delete,那中间会有内存泄漏么?


答案是:不会。

这些的对象都会在窗口接收到最后一个消息的时候把所有在View树中的对象都释放掉。在Windows下,也就是在

WM_NCDESTROY

消息中的处理中,主动释放所有的对象的。

所以我们在使用中,只需要保存好这些对象的裸指针,并且在合适的时机将其置空即可。对于置空的时机,Widget和View也有对应的回调,如

Widget::DeleteDelegate

,或者在析构函数里面来进行。


4. 写在最后

对于Chrome UI控件库,这里只是做了写简要的记录。对于各种控件的使用,在Chrome的代码里面也提供了非常详细的实例程序,大家可以在

src/ui/views/examples

下找到这些代码。在VS中也提供了相应的工程:views_examples_exe,供大家参考。