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

原创文章,转载请注明:转载自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的朋友们肯定对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-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,供大家参考。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值