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就实现了自绘控件。
2.2. 与原生控件的兼容
由于Chrome的控件还有一部分控件是直接使用的系统原生的控件,所以就会牵涉到自绘控件和原生控件如何在View控件树兼容的问题。
一个很自然的解决方法就是建立一个继承自View的原生控件基类,而具体的控件和逻辑则放入他的子类。这个基类就是NativeControl,通过继承他,来将原生控件纳入Views的层次结构中。在Chrome的代码中,Tree就是这样来实现的。
但是Chrome认为这样做存在问题,于是Chrome对其结构进行了改进,以求更好的支持跨平台和代码复用。
所以现在更多的控件的实现是建立一个Wrapper封装NativeControl,Wrapper的实现则继承自NativeControlWin,以便更加方便的控制控件,或者使用View进行替换,如Combobox。
3. 使用范例
好了,扯了这么多,我们来看一下如何使用这个皮肤引擎吧。
3.1. 建立一个工程
由于Chrome UI库和其他工程关联太紧,所以我们如果要建立一个测试工程其实并没有那么容易,我们可以在view_example_exe这个工程上直接进行修改,或者利用gclient生成一个测试工程。
- 打开src/ui/views/views.gyp,将views_examples_exe的描述段复制一份,粘贴在# target_name: views_examples_lib之后。
- 修改其工程名为你想要的工程名,如:view_test。
- 删除其source区域下除了.rc文件以外的所有源代码。
- 在dependencies中加入一项:views。
- 打开配置好的cygwin或者命令行,进入chromium源代码根目录,也就是存放.gclient文件的目录,输入gclient runhooks。
- 重新打开src/ui/views/views.sln,我们就可以在(views)目录下,看到view_test的工程了。
3.2. 准备工程
为了能让这个UI工程运行起来,我们需要写一些准备的代码:
1 | # |
之后我们把测试代码都加载test()这个函数中就可以了,另外这里之后的代码可能会泄漏的问题,这里我们先暂时不去理会他,后面会单独聊。
3.3. 实现一个简单的窗口
建立好了工程之后,我们就可以添加代码了,先让我们来建立一个最简单的窗口:一个空白的Widget。
1 | void test() |
短短几行我们就创建了一个最基本的窗口了,但是这个窗口实在是。。。有点难看啊。。
3.4. 添加一个按钮吧
既然难看,我们就来添加一些小控件到里面吧,先加一个小按钮吧。
首先,让我们来回想一下Chrome UI的元素结构,还记得这幅图么:
所以为了增加按钮,我们需要创建一个按钮的控件,并且为Widget的ClientView生成一个ContentsView来保存我们的这个按钮。
首先我们先添加几个头文件:
1 | # |
另外我们添加了一个TestWidgetDelegate的类,用于设置Widget样式并且提供ContentsView。另外我们还给它添加了一个FillLayout,让按钮与窗口保持一样的大小。
1 | class TestWidgetDelegate : public WidgetDelegateView, public ButtonListener |
另外生成Widget的代码也要做少许的改动:
1 | void test() |
编译运行,可以看到一个按钮已经出现啦~
3.5. 添加一个原生控件
好,我们已经可以添加一个自绘的控件了,现在让我们来试着添加一个系统原生的控件吧。
由于Chrome是在View的基础上封装的原生控件,所以添加原生控件也并非难事。比如我们现在来添加一个Tab栏,我们只需要添加一个头文件,再稍稍修改一下TestWidgetDelegate的构造函数就可以了。
添加头文件:
1 | # |
修改TestWidgetDelegate的构造函数:
1 | TestWidgetDelegate() { |
这里需要注意的一点是:Chrome很多原生控件的真实实现类都是在View层次关系发生改变的时候创建的,所以在Tab等原生控件创建完成之后,需要马上将其加入View中,不然后续调用其接口就会发生崩溃。
让我们来看看最终的效果:
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,供大家参考。
原创文章,转载请标明出处:Soul Orbit本文链接地址:Chrome学习笔记(三):UI组件,皮肤引擎 —— 控件库