1. 程式人生 > >Chrome學習筆記(二):UI元件,面板引擎 —— 基礎設施篇

Chrome學習筆記(二):UI元件,面板引擎 —— 基礎設施篇

本文連結地址: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相關的基本定義和基礎的功能實現了。

以下是他所包含的目錄和其對應的功能:

  • animation:動畫效果的抽象,這裡並不管繪製,只是負責計算動畫效果的進度。
  • cocoa:Mac上和cocoa)相關的程式碼。
  • dragdrop:和拖拽相關的程式碼,並在內部封裝了平臺無關的統一資料傳輸介面。
  • glib:一些Linux上用的程式碼。
  • gtk:Linux上使用的和gtk相關的封裝,簡化事件處理什麼的。
  • ime:和輸入法相關的程式碼。
  • keycodes:平臺無關的鍵盤的KeyCode的封裝。
  • l10n:本地化工具函式。
  • models:定義了一些控制元件的資料介面。
  • range:用於表示範圍的基本型別。
  • strings:用於本地化的字串表。
  • touch:觸屏相關的程式碼?
  • win:Windows下才會用到的程式碼,裡面包含Windows原生視窗的封裝,IME的處理等等。
  • x:Linux下和X11相關的程式碼。

3. 視窗封裝:Widget

一個Widget對應著一個真實的視窗,在Windows下它就對應一個HWND。

為了將平臺相關的視窗細節隱藏在Widget內部,Chromium為平臺相關的視窗抽取出了一個介面:NativeWidgetPrivate,用以封裝平臺相關的程式碼,而在裡面將平臺相關的訊息轉化為平臺無關的訊息,再通過NativeWidgetDelegate回調出來。而NativeWidgetDelegate除了接收回調以外,他還有很多用以指定原生視窗風格的回撥函式,供NativeWidgetPrivate建立時呼叫。

這些處理過後的訊息,就可以被統一來處理了。這裡Widget本身作為NativeWidgetDelegate接收並處理這些訊息或者分發給所有的控制元件,也就是馬上要提到的View,由這些控制元件來觸發真實的邏輯。

chrome-native-widget

這裡我們可以發現一件事情:那就是為什麼沒有看到mac平臺下的NativeWidget呢?答案估計你也猜到了:那就是。。。。mac下面用cocoa重寫了一套,貌似壓根就沒有實現widget。=.=|||

4. 介面元素:View

Chrome的開發者說:Windows既然沒有自帶好用的介面庫,那我們就自己搞!所以在對原生視窗抽象完畢之後,接下來的工作就是搭建自己的介面了。這個就是View

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要做的事情。

gfx裡面其實封裝了不少和介面繪製相關的內容,其中最重要的就是Canvas。

為了實現跨平臺的介面繪製,Chrome定義了一個Canvas的介面,來進行繪製的操作。我們在View的介面中可以看到一個View::Paint的函式,這個函式就是主要來控制繪圖的。我們拿Windows來舉例,視窗繪製的回撥邏輯主要分如下這麼幾步:

  1. 在原生視窗收到了WM_PAINT.aspx)訊息之後,NativeWidget會對其進行處理,將其轉化為Chrome內部的事件,並利用系統原生的繪圖方式生成Canvas,回撥給Widget進行分發。
  2. Widget在Widget::OnNativeWidgetPaint中將訊息分發給其對應的RootView,由其分發給自己和各個子View。

為了實現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。這其實是一個典型的策略模式,將複雜且多變的佈局封裝起來。

在layout目錄中,可以發現Chrome還提供幾種不同的佈局策略:

  • FillLayout,用於將第一個子View保持和當前View一樣大的策略。

現在我們可以猜到,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. Widget將此訊息轉交給他所對應的RootView由他來分發訊息。(Widget::OnKeyEvent
  3. RootView從當前Widget所對應的FocusManager中獲取出當前的焦點視窗分發訊息。(RootView::OnKeyEvent
  4. 焦點視窗處理訊息。

7.3. 焦點變化的處理

在Widget接收到原生視窗的焦點變化的時候(Widget::OnNativeFocus),他會回撥WidgetFocusManager來廣播焦點變化的事件,但是從面板引擎的程式碼裡面來看,預設的,沒有類會關心這個事件。

對於窗體元素的焦點,如果某個元素獲取了焦點,那麼這個元素對應的View會呼叫當前View所在Widget的FocusManager::SetFocusedView方法,將自己設為焦點。此時FocusManager也會將這個訊息廣播給其他關心焦點變化的事件的監聽者。但是在View裡面只有DialogClientView看上去比較關心這個事件。

7.4. 焦點與控制元件顯示的關係

我們發現,這些焦點變化的訊息居然沒有人關心?那麼焦點是怎麼影響控制元件顯示的呢?

這裡會涉及到兩個不同的,但是容易混淆的概念:FocusActive.aspx#active)。這裡有一個較為簡單的區分這兩個概念的方法,當然不一定完全對:

  • Focus:可以是非頂層視窗,主要影響鍵盤滑鼠等訊息的接收。
  • Activate:必須是頂層視窗,影響視窗繪製。

當我們點選非Chrome視窗導致Chrome窗體發生顏色變化,這個主要是由於Active訊息對應的處理。

當地址欄在可以輸入時會出現一個邊框,這個是由Focus來控制的。這個控制Chrome其實實現很簡單,通過判斷FocusManager中的FocusedView是不是自己來進行不同種類的繪圖。

8. Chrome面板引擎總結

到此為止Chrome面板引擎的基礎設施算是基本寫完了。總的來說,這一套面板引擎算是一個比較容易理解的跨平臺的DirectUI設計了。

在這一整套基礎設施上,Chrome開始搭建起自己的一套控制元件庫,再在這些內容的基礎上搭建起自己的主介面。這些後續再繼續寫。