Soul Orbit

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

原创文章,转载请标明出处:Soul Orbit
本文链接地址: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的朋友们肯定对GtkHBoxGtkVBox这个类有一定的印象,这两个类对辅助控件的布局是很有帮助的。在Chrome里面,你也可以使用类似的用法来辅助控件的布局,而且在UI里面还提供了几种基础的布局方法来帮助大家开发。

2. 实现方式

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

2.1. 自绘控件实现

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

真是熟悉的三个方面啊,想必很多朋友已经能对Chrome控件的实现方式猜个大概了,如果还对于Chrome UI绘制机制有一定了解,那么代码估计自己也能写出个大概了。

没错,就是MVC模型

  • 使用Canvas来实现绘制的接口,在控件的OnPaint回调中进行自绘。
  • 采用MVC的设计思想,对于复杂的控件,如TreeTable等等,提取出Model接口和Controller接口,分别用于管理数据和处理事件回调并控制控件行为。

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

  • TreeView主要用于绘制。现在TreeView已经被系统原生控件接管,但是在Chrome代码里面,我们依然能找到自绘的TreeView
  • TreeModel主要用于管理数据。
  • TreeViewController主要用于处理事件回调,控制控件行为,如:控制树中某一项能不能被编辑。
    chrome-ui-control-tree

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

2.2. 与原生控件的兼容

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

一个很自然的解决方法就是建立一个继承自View的原生控件基类,而具体的控件和逻辑则放入他的子类。这个基类就是NativeControl,通过继承他,来将原生控件纳入Views的层次结构中。在Chrome的代码中,Tree就是这样来实现的。

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

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

3.4. 添加一个按钮吧

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

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

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

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

3.6. 控件的生命周期

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

答案是:不会。这些的对象都会在窗口接收到最后一个消息的时候把所有在View树中的对象都释放掉。在Windows下,也就是在WM_NCDESTROY.aspx)消息中的处理中,主动释放所有的对象的。

所以我们在使用中,只需要保存好这些对象的裸指针,并且在合适的时机将其置空即可。对于置空的时机,Widget和View也有对应的回调,如Widget::DeleteDelegate,或者在析构函数里面来进行。

4. 写在最后

对于Chrome UI控件库,这里只是做了写简要的记录。对于各种控件的使用,在Chrome的代码里面也提供了非常详细的实例程序,大家可以在src/ui/views/examples下找到这些代码。在VS中也提供了相应的工程:views_examples_exe,供大家参考。

同系列文章:
坚持原创技术分享,您的支持将鼓励我继续创作!