GacUI基本概念(一)

vczh發表於2019-05-10

本來是想從如何建立GacUI的VC++工程開始寫的,不過最近網友普遍反映,怎麼建工程都能從 Tutorial 裡面看明白,就打算說到GacUI的XML資源怎麼編譯的時候在順帶講一下。今天主要說的是GacUI的體系架構。

在構造一個GacUI工程的時候, CppXmlMVVM 是兩個推薦的Hello World參考專案。雖然還有一些其他的方法,但那並不是使用GacUI的正確方法,那些Demo只是為了新增知識做出來的。

GacUI 類庫參考

見:http://www.gaclib.net/Document.html#~/ 。這個網站host在Windows Azure上。如果要訪問host在github io上面的映象網站,可以使用 http://vczh-libraries.github.io/Document.html#~/

這個網頁裡面的所有內容其實都在程式碼標頭檔案的註釋裡,然後我寫了一個工具把他們都抽了出來,做成了這個網頁。至於為什麼不用現成的工具,其實我一開始是直接使用VC++的XML註釋功能的。VC++在編譯的時候會幫我做完所有的事情,而且在開發的時候還會把註釋寫進intellisense裡面,特別容易使用。

但是在這個過程中,我痛苦地發現,VC++要直接生成chm的話,必須使用託管專案。顯然GacUI並不是託管專案,而且他要求託管專案的原因,僅僅是為了讀取exe/dll裡面的後設資料。那麼Native C++專案的後設資料,當然就只能去pdb找了。於是我使用了VC++自帶的一個處理pdb讀寫的COM庫,把這個事情給做出來了。

後來我又痛苦地發現,VC++的XML文件,只給託管專案提供對模板的支援。我把註釋寫在模板類上,VC++在編譯出最後的XML註釋彙總檔案的時候,寫在模板上面的直接沒有了!於是我只好放棄使用VC++提供的工具鏈,轉而自己使用C#做了一個C++標頭檔案的parser,從而解決了所有的問題。如果大家感興趣的話,這個文件生成工具可以在 這裡 找到。

不過我寫的文件生成工具,只是產生了一系列的帶後設資料的XML註釋檔案。實際上這個網頁是另外寫的。網頁會在訪問者點選了一個類的超連結之後,去後臺獲取相應的XML,然後cache在客戶端記憶體裡。所以你點選第二次的話不需要重新載入——直到你關閉了這個網頁。

為什麼整個文件只有一個網頁呢?因為那個時候我正在學習如何正確使用Javascript和CSS,就順便練了練手。另一個原因是,我想把網站host在github io上,但是github io又不能後臺跑程式,所以只好痛苦的使用Javascript把ASP.NET MVC裡面我喜歡的部分做了出來,全部邏輯跑在瀏覽器裡。

GacUI 原始檔

可以在兩個地方獲得GacUI的原始檔。

使用Release

第一個是Release。Release的正確下載方法是到 Release repo的Release頁面 獲取程式碼。目前Linux版本正在開發,OSX基本已經完工了,不過他們還沒有整合到同一個Release repo下面。所以需要到 XGaciGac 兩個repo下載程式碼。

Windows版本的Release會包含以下幾個檔案,他們都是成對的.h和.cpp檔案:

  • Vlpp:跨平臺的C++基礎庫。

  • Workflow:一個可以靠反射訪問C++類的指令碼引擎,物件模型使用引用計數,跟C++的互操作性無比的好。

  • GacUI:GacUI的主要部分。

  • GacUIReflection:GacUI所有可以被指令碼訪問的類的後設資料,用於反射。如果不連結這個檔案的話,那麼在初始化的時候,構造後設資料的過程將被跳過。使用者不需要寫額外的程式碼來明確這個過程,只需要選擇連結或者不連結這個檔案就可以了。

  • GacUIWindows:GacUI在Windows平臺下面的Window Provider和Renderer。

目前你需要使用所有的檔案,因為GacUI會把XML資源直接編譯成Workflow指令碼引擎的位元組碼,嵌入到生成後的二進位制資原始檔裡。在構造一個視窗的時候,實際上就是在跑指令碼。儘管Workflow指令碼遠沒有C++快,但是一個視窗再複雜也複雜不到哪裡去,所以載入的時間難以察覺。

明年即將推出把GacUI的XML資源直接編譯成C++的選項,到時候可以無限縮小檔案體積,再也不需要帶上GacUIReflection裡面的後設資料了,那麼Workflow和GacUIReflection在大部分情況下就都不需要了。不過如果你的程式想要支援外掛,那自然無法使用這個功能。

閱讀 GacUI 程式碼

上面的每一個檔案都十分的大。我這樣做純粹是為了程式設計師的使用方便。程式設計師只需要根據需求連結不同的檔案就可以了,而不需要把整棵目錄樹都拖進來。不過我自己在開發的時候,顯然不可能直接寫這些檔案的。這些檔案是我在做Release的時候,呼叫我寫的一個命令列工具拼裝出來的。包括上面提到的文件也是。

所以如果需要閱讀GacUI的程式碼的話,應該分別去 VlppWorkflowGacUI 三個repo。

GacUI 體系架構

事實上GacUI的架構是分層的,從底層到頂層分別是:

  • Window Provider

  • Renderers

  • Elements + Compositions

  • Controls + Templates

  • XML Resource Compiler

Window Provider

Window Provider指的是如何操縱作業系統提供的原生的視窗、圖片資源、滑鼠資源、非同步原語和一些其他的東西。畢竟GacUI怎麼做,最頂層的視窗是沒辦法自己做的,最多通過Template來替換掉視窗邊框。把GacUI移植到Linux和OSX的工作,主要就是寫兩個新的Window Provider,然後提供各個平臺上不同渲染器的Renderer。其他的部分都是平臺共享的程式碼。

Renderers

Renderers跟Window Provider是分開的。畢竟同一個作業系統上你可以使用不同的影像技術來繪圖,而不同的作業系統上你可以使用相同的影像技術來繪圖。舉個例子,OpenGL和Cairo就是在很多平臺上可以用的。不過OpenGL在每一個平臺上都是二等公民,所以我並沒有真的使用OpenGL來開發。一個GacUI程式在剛開始的時候,如果是Windows的話就是在WinMain函式裡,需要首先選擇一個Renderer,選擇了之後就不能變了。

目前GacUI在不同的作業系統上使用的繪圖技術如下所示:

  • Windows : GDI, Direct2D 1.0, Direct2D 1.1

  • Linux : Cairo + Pango

  • OSX : CoreGraphics + CoreText

Direct2D的1.0和1.1的版本雖然只有初始化的程式碼有區別,不過這關係到能不能直接跟Direct3D 11.0攪在一起,所以單獨拿出來講了。目前GacUI在Windows上,如果你選擇了使用Direct2D技術:
WinMain.cpp

#include <GacUI.h>
#include <Windows.h>

int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int CmdShow)
{
    return SetupWindowsDirect2DRenderer();
}

那麼在GacUI初始化的時候會優先選擇Direct1.1。如果在程式碼裡面引用了GacUIWindows.h,那麼你還可以得到每個視窗所使用的系統相關的物件,可以讓你在不得不使用非跨平臺技術的時候,提供一個機會。

Elements + Compositions

在GacUI裡面Element和和Composition分別代表基礎的圖元和排版功能。每一個Element執行在具體的平臺的時候,都需要具體的Renderer物件。這些物件是前面兩層合作提供的。

Composition是GacUI的其中一個重要部分。這個部分提供了所有的排版功能。一個Composition物件代表了視窗上的一個長方形的區域。每一個區域可以嵌入一個Element。當一個Composition確定了他的位置的時候,那麼Element會被填充到整個長方形的區域裡面,從而渲染出來。

幾乎所有的Element都是很簡單的幾何圖形,除了渲染文字的 GuiColorizedTextElementGuiDocumentElement 。不過在製作控制元件皮膚(也就是Template的一部分功能)的時候,文字框控制元件由於功能的複雜性,皮膚需要提供一個區域讓控制元件放置這兩個Element,而不是跟普通的控制元件一樣,全權處理所有的渲染物件。

目前Composition支援直接定位、Stack、Table、Flow和一些其他的功能。一個比較特殊的就是,使用 GuiDocumentElementGuiDocumentLabelGuiDocumentViewer 可以在富文字文件的中間嵌入Composition。這個功能是其他的Element所不具備的。因此這兩個控制元件構成了一種新的排版方法。

Element和Composition的具體介紹將在以後的文章中提供。

Controls + Templates

Control的結構比較複雜。一個典型的GacUI的Control,包含了用來代表控制元件本身的操作和資料邏輯的Control物件,和包含了如何渲染這個空間的IStyleController或者IStyleProvider物件。IStyleController擁有整個Composition和Element的控制權。如果當一個Control只決定讓皮膚控制一部分的Composition和Element的時候,那麼他會提供IStyleProvider物件。

不過在開發的時候,程式設計師不需要區分IStyleController和IStyleProvider,因為使用XML來編寫皮膚的時候,都是使用Template來編輯。最後每個Template會自己去找一個合適的wrapper物件來把自己wrap成IStyleController或者IStyleProvider然後提供給Control。IStyleController / IStyleProvider 和 Template的區別,就在於一個是Pull模型的,一個是Push模型的。Push模型做data binding特別容易,因此在XML裡面都是通過建立Template物件來修改一個Control是如何渲染的。

每一個Control類都有自己相應的Template類。

對於列表控制元件、譬如 GuiTextListGuiListViewGuiTreeViewGuiVirtualDataGrid 等,除了Template以外,還有ItemTemplate。Template和ItemTemplate是可以分開指定的。Template確立了整個控制元件的外觀,而ItemTemplate確定了每一個列表項的外觀。

如果需要對容器的內容做資料繫結的話,那麼需要使用上述4個控制元件的Bindable版本,分別是它們的子類:GuiBindableTextListGuiBindableListViewGuiBindableTreeViewGuiBindableDataGrid 。在使用這些控制元件的時候,可以通過在XML裡面的Workflow指令碼——其實通常就是

<GuiBindableListView ItemSource-eval="ViewModel.Something"/>

這種簡單的表示式——把一個C++的容器物件繫結到ListView上。每個ListViewItem拿到的容器的每一個物件,可能最終型別是不一樣的。GacUI還提供了一個功能,你可以通過給ListView的ItemTemplate指定一系列的Template物件,通過在XML裡面寫好的這些Template的建構函式的引數的型別,來讓ListView決定到底要使用哪個Template。於是一個異構的列表就這麼輕鬆的造出來了。

Compositions 和 Controls 的生命週期

你們可能會注意到,Control並不在這一層裡面。這是正確的。因為整個視窗就是由Element和Composition共同組成的一張超大的動態向量圖。每一個Control負責管理這顆Composition樹的一些子樹,每一個Control會告訴你他最外層的Composition和用來做容器的Composition分別是什麼,然後把Control放進Composition、把Composition放進、把Control放進Control的這些動作,實際上都是在操作Composition。在實際的程式碼裡面,你的確也是首選獲取Control相應的Composition,然後去操作Composition的。

因此Control和Composition並不是平級的,你可以認為Control對於Composition使用了Builder和Facade模式,讓你更容易的操作GUI。

當然這種做法對整個GacUI物件的生命週期會有一些影響。當你在C++程式碼裡面delete一個Composition的時候,他會把下面的所有Composition子節點一起delete。當你在C++程式碼裡面delete一個Control的時候,他會把下面所有的Control子節點,還有對應的所有Composition一起delete。

所以這個時候就會有一個疑問,那delete一個Composition的時候,如果Composition子節點上有Control怎麼辦?為了解決這個問題,我提供了這樣的兩個函式:SafeDeleteCompositionSafeDeleteControl

另外值得一提的是,如果你直接delete一個Control(通常情況下是你用完了一個 GuiWindow 直接把它刪掉),他會先刪掉整棵Composition樹,然後再刪除Control樹。所以自己開發的Control在解構函式裡面,千萬不能訪問Composition,否則直接GG。

XML Resource Compiler

GacUI目前提供的XML資原始檔,支援讓你構造Window、UserControl、Template、類似CSS那樣的InstanceStyle(主要通過XPath來批量設定XML的屬性,比選擇器好用多了,而且精確控制起來更不費腦)和一些共享的Workflow指令碼。共享的Workflow指令碼可以用來定義一些視窗的邏輯程式碼,還有MVVM模式需要的ViewModel的介面和資料結構。

當你準備好一個XML資源的時候,Release裡面提供的GacGen.exe會幫你把XML資源編譯成一個二進位制的資原始檔,還有一系列的C++程式碼。生成的C++程式碼模擬了C#的partial class的能力,讓你可以像Windows Forms一樣,準備控制元件的事件處理,還有在視窗初始化的時候做一些任務等等。而且當你的XML需要更新的時候,GacGen.exe重新生成的C++程式碼會跟你修改後的那部分自動合併。

使用Workflow指令碼寫的ViewModel相關的介面和資料結構,也會被一併生成C++程式碼。在構造一個帶有MVVM模式的視窗的時候,你只需要繼承一下ViewModel介面,然後把這個類的例項當做視窗的引數填進去就好了。所有生成的程式碼都是強型別的,如果你物件給錯了,會直接無法編譯。特別安全。

目前GacUI把所有的用來構造視窗的那部分XML,在編譯之後都轉成了Workflow指令碼的位元組碼,寫進了二進位制資原始檔裡面(這項功能將包含在即將到來的下一個Release裡面)。視窗在初始化的時候,會去資原始檔裡面找到相應的指令碼來執行,從而按照要求建立控制元件和data binding。

在後續的開發過程中,我還將為XML資源提供Visual State、Animation、State Machine和多語言字串資源等重要部件。明年還計劃讓Workflow指令碼可以被編譯成C++,不僅可以大幅度的提高編寫出來的GacUI程式的效能,還可以通過讓你再也不需要連結GacUIReflection,讓你的二進位制檔案的尺寸縮小到1/8。

當然了,還是會有一小部分情況是無法讓你完全放棄在二進位制檔案裡面帶後設資料的,舉個例子,如果你編寫出來的程式需要支援帶GUI的外掛,那麼為了載入那些已經被編譯成二進位制資源的、在釋出了之後使用者自行製作的GacUI視窗,那你還是要保留反射的功能。不過這種需求在廣大的GUI程式裡面還是比較罕見的。

值得一提的是,GacUI的data binding的功能十分強大,你可以使用任何滿足語法要求的Workflow指令碼表示式(基本上就像C#一樣豐富)來從ViewModel和控制元件之間做資料繫結。舉個簡單的例子,你完全可以寫三個文字框,然後讓第三個文字框永遠等於前兩個文字框的數字之和,並且在輸入錯誤的情況下報錯:

<SinglelineTextBox ref.Name="textBox1" Text="0"/>
<SinglelineTextBox ref.Name="textBox2" Text="0"/>
<SinglelineTextBox Text-bind="(cast int textBox1.Text) + (cast int textBox2.Text) ?? `<ERROR>`" />

這個例子要用WPF或者其他GUI框架來寫就很蛋疼。那麼我在編譯XML資源的時候是怎麼處理這個表示式的呢?其實這主要使用了語言愛好者們非常熟悉但是總是搞不明白的CPS變換(跟各種語言的玄乎的coroutine在編譯的時候其實使用了基本相同的手法),然後把這種pull的程式碼轉變成push的程式碼,這樣就可以在textBox1的TextChanged發生的時候,跟換存起來的其他沒有變化的屬性的計算後的值(如cast int textBox2.Text)一起,做最少的計算,最後寫到第三個控制元件的Text屬性裡面。

你還可以在這個表示式裡面引用你在資原始檔裡面提供的Workflow指令碼,或者乾脆引用你自己用C++寫的類和庫函式,來幫助你做一些不屬於ViewModel但是卻十分蛋疼的、GUI相關的功能。舉個例子,寫一個統計學生成績的程式,你可能需要給學生分優良中差。顯然如何描述一個等級,使用中文和英文的方法就不一樣。然而這並不是ViewModel的功能,ViewModel應該只負責計算等級,然後GUI再根據使用者使用的系統所提供的語言資訊來決定到底要如何顯示。

這部分你就可以從ViewModel中間分離開,獨立的寫成Workflow指令碼、XML資源或者C++程式碼,從而在定義視窗的XML裡面使用。這樣整個架構分層清晰、測試起來容易、而且需求變更的時候還特別好改。

尾聲

這篇文章主要介紹了在使用GacUI的過程中需要了解的一些關於GacUI的體系架構的知識。裡面的每一個知識點都會陸續在接下來的文章裡面詳細描述。除此之外,我還會偶爾寫一些文章來介紹GacUI的、外部不可見的、跟實現緊密相關的內部架構,以及需要用到的一些編譯原理、設計模式等知識,敬請關注。

相關文章