UI Toolkit介紹
UI Toolkit是Unity最新的UI系統,它主要被設計用來優化不同平台的性能,此項技術是基於標准的web技術開發的(standard web technologies),既可以使用UI Toolkit來拓展Unity Editor,也可以在打包出來的游戲和應用里使用Runtime的UI(但需要安裝UI Toolkit Package)
UI Toolkit包括以下內容:
- 一個保留模式的UI系統(A retained-mode UI system),擁有創建UI的核心特性和功能
- UI資源類型,這些類型啟發於標准的web格式 (Inspired by standard web formats),比如HTML、XML和CSS。使用這些資源文件可以構造出整個UI界面
- 用於學習UI Toolkit的工具和資源,這些工具和資源還可以用於創建和Debug你的interfaces
Unity想推薦UI Toolkit成為新項目的UI系統,但是它跟傳統的uGUI和IMGUI相比,還是少了一些功能,后面會再提到。
UI Toolkit是一系列用於創建UI的資源、函數、特性和工具的集合,它可以被用過來創建常規的UI,也可以用來拓展Unity Editor、制作Runtime的Debug工具和創建Runtime的游戲UI。
UI Toolkit受standard web technologies啟發得到,很多核心的概念是類似的。
UI Toolkit分為以下三類:
- UI System: 包含了核心features and functionality
- UI Assets: 受標准web格式啟發得到的文件類型,可以被用來structure and style UI
- Tools and resources: Create and debug your interfaces, 還可以用於幫助學習UI Toolkit
UI System
UI Toolkit的核心是一個retained-mode UI system based on recognized web technologie。它支持stylesheets,和dynamic and contextual event handling.
UI System有以下內容:
- Visual tree:定義了所有UI Toolkit創建的UI(Defines every user interface you build with the UI Toolkit),A visual tree即是一個object graph,graph由輕量級node組成,這些node存儲了所有在窗口或panel里的UI元素。
- Controls:提供了標准的UI Control庫,比如buttons、popups、list views和color pickers,可以直接原樣使用它們、自定義(customize)它們或創建自己的controls。
- Data binding system:可以把相關的property link到Control上,從而通過UI改變它們的值
- Layout Engine:一個基於CSS的Flexbox模型的Layout系統,它可以基於layout和styling properties來放置UI元素
- Event System:事件交互,包括:input、touch and pointer interactions(應該是觸碰操作吧?),drag和drop操作等。系統包括了:a dispatcher,a handler,a synthesizer和一大堆event類型
- UI Renderer:直接在Unity的graphics device layer上創建的渲染系統
- UI Toolkit Runtime Support(via the UI Toolkit package):包含了用於runtime的相關組件,不過UI Toolkit package is currently in preview.
UI Assets
UI Assets也就是UI Toolkit里用到的資源文件,UI Toolkit提供了兩種資源文件來幫助構建UI,與web應用類似:
- UXML documents,文件后綴是.uxml
- USS,文件后綴是.uss
UXML全稱為Unity eXtensible Markup Lauguage,是受HTML和XML啟發得到的一種markup(標記)語言,用於定義UI結構和可復用的UI模板,Unity推薦使用UXML來創建UI,而不是在C#腳本里進行
USS全稱為Unity Style Sheets:可以對UI使用可視的style和behaviours,與web的CSS類似,跟上面相同,Unity推薦用USS文件來定義style,而不是直接在C#腳本里對style這個property進行修改
UI Tools and resources
提供了以下工具和資源:
- UIDebugger:類似web瀏覽器的debug窗口,可以看到對應的UXML結構和USS對應的style相關的hierarchy的信息,在Window->UI Toolkit -> Debugger下
- UI Builder(package):幫助用可視化的方式創建UI資源文件,比如uss和hxml documents,需要安裝對應package
- UI Samples:Window->UI Toolkit -> Samples下可看到很多關於UI Control的代碼示例
Accessing UI Toolkit
UI Toolkit有兩種獲取方法,或者說有兩個版本:
- 直接在Unity Editor里獲取,也就是Unity提供的引擎編輯器里自帶的內置版本
- 從Unity Package里獲取(com.unity.ui)
二者的區別如下:
- 目的不同,內置的UI Tooklit旨在加強Unity Editor的編輯,很多Unity Editor的自帶功能都是用的內置的UI Toolkit,而Unity Package里的版本添加了很多特性,用於制作runtime下的UI
- 二者使用方式是相同的,都是在UnityEditor.Elements和UnityEngine.Elements的命名空間下使用
該選擇UI Toolkit兩個版本的哪一個
如果相關UI只會在Editor下使用的話,那么使用內置的UI Toolkit,如果該UI需要既能在Editor,也能在Runtime下使用的話,那么使用對應的Package的版本,而且對應的版本也能安裝最新的
安裝 UI Toolkit package
打開Unity Editor的Package Manager:
- Click Add (+)
- From the menu, choose Add package from git URL…
- In the text field, type com.unity.ui
- Click Add
The Visual Tree
UI Toolkit里UI的最基本構建單元被稱為Visual Element,這些elements會被排序,形成一個有層次結構的樹,稱為Visual Tree,下圖是一個例子:
Visual elements
VisualElement類是所有出現在Visual Tree里節點的基類,它定義了通用的properties,比如style、layout data和event handles。可以使用
stylesheet來自定義Visual Element的形狀,也可以使用event callback來自定義Visual Element的行為
VisualElement的派生類可以再添加behaviour和功能,比如UI Controls,下面的這些都是基於Visual Element派生出來的:
- Button
- Toggles
- Text Input fields
后面還會介紹更多的內置的Controls
Panels
panel是Visual Tree的父object,對於一個Visual Tree,它需要連接到panel上才能被渲染出來,所有的Panels都從屬於Window,比如EditorWindow,Panel除了處理Visual Tree的渲染外,還會處理相關的focus control和event dispatching。
每一個在Visual Tree里的Visual Element都會記錄該Panel的引用,VisualElement對象里叫panel的property可以用於檢測Element是否與Panel相連,若panel為null說明不相連
Draw Order
Visual Tree里默認是按深度遍歷的順序繪制Element的,如果想要改順序,可以使用以下函數:
VisualElement e;
// 注意,下面的front和back都是視覺上的繪制關系,front意味着重疊部分不會被遮擋
// 會把該元素移到它原本的parent的children列表的最后面,所以該元素最后畫,所以在top
e.BringToFront();
// 同上,正好反過來
e.SendToBack();
// 在parent的childrenn列表里,把e放到sbling的前面,即先畫e再畫sibling,所以e在底層
e.PlaceBehind(UIElements.VisualElement sibling);
// 同上,正好反過來
e.PlaceInFront(UIElements.VisualElement sibling);
Coordinate and position systems
UI Toolkit有一個強大的layout系統,根據每一個Visual Element里名為style的property,就能自動計算出每個Element的位置和size,后面還會詳細提到Layout Engine.
UI Toolkit有兩種坐標(coordinates):
- Relative:基於element被計算好的position的相對坐標(Coordinates relative to the element’s calculated position.),也就是說,element的位置等於其parent的位置加上coordinates對應的offset,在這種情況下,子element的位置會影響父element的位置(因為Layout系統需要合理的安排區間,來擺放所有的element)
- Absolute:基於parent element的絕對坐標(Coordinates relative to the parent element). 這種方式下,element的位置不再由layout系統自動計算,而是直接會被設置position。同一個element下的子elements之間的位置不會受互相的影響,也就是說,element與其parent的位置關系是確定不變的(有點Anchor的意思)
設置一個Element的Coordinates的方法如下所示:
var newElement = new VisualElement();
newElement.style.position = Position.Relative;
newElement.style.left = 15;
newElement.style.top = 35;
在實際計算pos的時候,layout system會為每個element計算位置和size,再把前面的relative或absolute的coordinate offset加進去,最后的結果計算出來,存到element.layout里(類型是Rect)
The layout.position is expressed in points, relative to the coordinate space of its parent.
VisualElement類還有一個繼承的Property,叫做ITransform,修改它可以添加額外的Local的position和rotation的變化,相關的變化不會顯示在layout屬性里,ITransform默認是Identity.
VisualElement.worldBounds代表Element在窗口空間的最終坐標bounds,它既考慮了layout,也考慮了ITransform,This position includes the height of the header of the window.
下面介紹一個例子,使用內置的UI Toolkit來創建Editor下的窗口。首先可以創建一個腳本,腳本內容如下:
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
public class PositioningTestWindow : EditorWindow
{
[MenuItem("Window/UI Toolkit/Positioning Test Window")]
public static void ShowExample()
{
var wnd = GetWindow<PositioningTestWindow>();
wnd.titleContent = new GUIContent("Positioning Test Window");
}
public void CreateGUI()
{
// 創建兩個數據一模一樣的Element, 注意這里沒有指定位置,因為位置是Layout系統自己算的
for (int i = 0; i < 2; i++)
{
// 創建兩個Element, 為一個正方形, 背景是灰色
var temp = new VisualElement();
temp.style.width = 70;
temp.style.height = 70;
// marginBottom代表當Layout系統計算布局時, 此Element下方會預留20個像素的距離
temp.style.marginBottom = 20;
temp.style.backgroundColor = Color.gray;
rootVisualElement.Add(temp);
}
}
}
點擊對應的menu操作,就能出現窗口,如下圖所示:
繼續補充CreateGUI代碼,現在畫一個Label,而且更改它的style里的pos,代碼如下:
public void CreateGUI()
{
// 創建兩個數據一模一樣的Element, 注意這里沒有指定位置,因為位置是Layout系統自己算的
...//原本的不變
// 創建一個Label, Label是VisualElement的派生類
var relative = new Label("Relative\nPos\n25, 0");
// relative.style.position = Position.Relative;// 默認的就是Relative的方式, 所以不用刻意去寫
relative.style.width = 70;
relative.style.height = 70;
relative.style.left = 25;
relative.style.marginBottom = 20;
relative.style.backgroundColor = Color.red;
rootVisualElement.Add(relative);
}
現在的結果變成了下圖所示的樣子,可以看到,原本Label應該是跟之前的一樣,往下20個像素繪制的,但是這里有style.left = 25,所以在原本的基礎上,加上offset(25, 0),得到最后右移的位置:
展示完了Relative的方式,下面再看看Absolute的例子,代碼也是類似:
public void CreateGUI()
{
...// 畫原本三個Element的代碼不變
// 又畫兩個相同的方塊進行對比
for (int i = 0; i < 2; i++)
{
var temp = new VisualElement();
temp.style.width = 70;
temp.style.height = 70;
temp.style.marginBottom = 20;
temp.style.backgroundColor = Color.gray;
rootVisualElement.Add(temp);
}
// 繪制Absolute類型的方塊:Absolute Positioning
var absolutePositionElement = new Label("Absolute\nPos\n25, 25");
// 類型是Absolute, 基准點是parent element, 其parent element就是窗口里的rootVisualElement
absolutePositionElement.style.position = Position.Absolute;
absolutePositionElement.style.top = 25; // 設置上方間距
absolutePositionElement.style.left = 25; // 設置左邊間距
absolutePositionElement.style.width = 70;
absolutePositionElement.style.height = 70;
absolutePositionElement.style.backgroundColor = Color.black;
rootVisualElement.Add(absolutePositionElement);
}
最后的效果如下圖所示,黑色的方塊:
注意,在EidtorWindow類里,有一個Property叫做public VisualElement rootVisualElement { get; }
,可以用於取得窗口的Visual Tree的root visual element。
Transformation between coordinate systems
VisualElement.layout.position和VisualElement.transform兩個參數,決定了local coordinate system 和 the parent coordinate system直接的轉換,靜態類VisualElementExtensions為這些轉換提供了一些方法:
- WorldToLocal:把一個Vector2或Rect,從Panel Space轉換到element local space
- LocalToWorld:同上,方向正好相反
- ChangeCoordinatesTo:把Vector2或Rect從一個Element的local space轉換到另外一個Element的local space
The Layout Engine
Layout Engine可以基於Visual Elements的layout和style屬性自動計算UI布局,它是基於Github上的開源項目Yoga開發的(Yoga implements a subset of Flexbox: a HTML/CSS layout system)。
要學習Yoga和Flexbox,還需要到文檔上提供的鏈接里去看,這里就不掛鏈接了。
Layout System默認有以下特點:
- 一個container會豎直分布其children(container具體定義是什么?)
- 一個container rectangle的position會包含其chidren的rectangles,此特點可以被其他的layout屬性影響
- 帶有text的Visual Element,會在計算size時使用它字體的size,此特點可以被其他的layout屬性影響
使用layout engine的一些方法:
- 使用width和height來指定element的size
- 通過flexGrow屬性實現flexible size(in USS:
flex-grow: <value>;
) ,當element的大小由其兄弟element決定時, flexGrow 屬性的值用作權重。 - 通過將flexDirection屬性設置為row,可以把layout從豎直變為水平分布
- 如果想要在已有的element的位置上做偏移,使用relative positioning
- 如果想讓一個element像一個anchor一樣,保持其與parent的位置關系,使用absolute positioning,不會影響其他的element和parent的布局
The UXML format
UXML是一種文本文件,它定義了UI的邏輯結構,本章會介紹UXML的語法、還要如何寫入、讀取和定義UXML模板等,還包含了一些自定義新的UI Element的方法,以及使用UQuery的方法。
In UXML 可以:
- 在XML里定義UI的structure
- 在USS styleshhets里定義UI layout
而與這些相關的資源加載部分,就留給開發者自己去做了,比如導入資產、壓縮數據什么的。
如何理解USS和UXML文件
這里強調一下初次看到這的時候我不理解的問題,UI的structure和UI layout有何區別?
其實Structure代表了節點的組織關系,就是Hierarchy里的父子關系,而UI Layout則代表了每個UI節點的具體的style等參數,如下圖所示,HTML文件記錄是Structure,CSS文件里記錄的是每個節點的繪制信息,這樣一看應該就很清楚了:
類比到UI Toolkit里,UXML文件用於描述整體節點之間的Structure,也就是對應的父子連接關系,而每個節點都有自己的USS文件,用於描述那個節點的尺寸等UI信息。
自定義Visual Element
Unity的原文檔連接在這里:https://docs.unity3d.com/2020.1/Documentation/Manual/UIE-UXML.html
坦白說,這一段文檔官方文檔居然沒有配合具體的代碼展示,感覺官方寫的東西就是一坨屎,下面會基於這坨垃圾玩意兒,進行解釋,然后加上自己的解釋和樣例去幫助理解。
- 創建類的基本定義
UI Toolkit是一個可拓展的工具包,可以基於Visual Element自定義UI Element,相關的代碼如下:
// 需要繼承於VisualElement
class StatusBar : VisualElement
{
// 必須要實現一個默認構造函數
public StatusBar()
{
}
public string status { get; set; }
}
然后我試了試,創建了個EditorWindow窗口,代碼如下:
public class MyEditorWindow :EditorWindow
{
[MenuItem("Window/Open My Window")]
public static void OpenWindow()
{
var window = GetWindow<MyEditorWindow>();
StatusBar statusBar = new StatusBar();
statusBar.status = "Hello World";
statusBar.style.width = 50;
statusBar.style.height = 50;
window.rootVisualElement.Add(statusBar);
}
}
然后打開EditorWindow,發現沒有任何顯示,但是我打開UIElements Debugger發現是有東西的,只是沒有顯示String和UI而已,如下圖所示:
- 創建相關的factory類
雖然這個類被創建了,但是目前好像new出來,設置width和height之后,並沒有在Window中有任何顯示。
這是因為,還沒有讀取對應的UXML,來決定該element的結構。為了讀取UXML文件,需要創建一個對應的factory類,這個類可以繼承於UxmlFactory<T>
,一般推薦在Element類內定義,代碼如下:
class StatusBar : VisualElement
{
// 在定義了這個類之后, 就可以在UXML文件里寫StatusBar元素了,
// 不過我還不熟悉這個new class的寫法
public new class UxmlFactory : UxmlFactory<StatusBar> { }
...
};
- 創建Element的Attribute
這個Attribute的概念源自於XML,具體的可以看后面的附錄。
這里需要創建一個UxmlTraits的對象,來實現相關的Attribute的創建:
class StatusBar : VisualElement
{
public new class UxmlFactory : UxmlFactory<StatusBar, UxmlTraits> {}
// 取的類名不變
public new class UxmlTraits : VisualElement.UxmlTraits
{
// 創建一個StringAttribute對象, StatusBar只有一個Attribute, 名字叫status
UxmlStringAttributeDescription m_Status = new UxmlStringAttributeDescription { name = "status" };
// 定義UxmlChildElementDescription函數
// 函數返回空的IEnumerable,表示StatusBar的沒有任何child element, 也不接受任何children
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
get { yield break; }
}
// 會從XML parser里讀取到對應的bag, 然后賦值給m_status
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
// calls base.Init() to initialize the base class properties.
base.Init(ve, bag, cc);
// 把此類定義在StatusBar內部, 可以直接獲取私有成員status
((StatusBar)ve).status = m_Status.GetValueFromBag(bag, cc);
}
}
public StatusBar()
{
m_Status = String.Empty;
}
string m_Status;
public string status { get; set; }
}
UxmlTraits類有兩個作用:
- 會被Factory對象用於初始化新創建的對象
- 在schema generation過程中,可以從中獲得element的信息,用於轉換成XML schema directives
上面的Trait類里定義了UxmlStringAttributeDescription
對象代表String的Attribute,一共有以下類型:
前面的uxmlChildElementsDescription函數里,寫的代碼是不支持任何Children的,如果想支持任何Children,可以這么寫:
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
get
{
yield return new UxmlChildElementDescription(typeof(VisualElement));
}
}
UxmlFactory和UxmlTraits實例
這一塊內容Unity的文檔居然沒有給例子,真是辣雞,這里舉個例子。
- UxmlFactory類, 用於在UXML里識別此類, 並在里面創建此類對應的Tag
- UxmlTraits類用於在UXML文件里添加自定義的Attributes, 它們都可以在UI Builder里看到
舉個例子,在定義這么一個類以后:
class TwoPaneSplitView : VisualElement
{
// 定義UxmlFactory類, 用於在UXML里識別此類, 並在里面創建此類對應的Tag
public new class UxmlFactory : UxmlFactory<TwoPaneSplitView, UxmlTraits> {}
// UxmlTraits類用於在UXML文件里添加自定義的Attributes, 它們都可以在UI Builder里看到
public new class UxmlTraits : VisualElement.UxmlTraits{}
}
只有在里面加上了UxmlFactory,才可以在Uxml里這么寫:
<BuilderAttributesTestElement/>// 目前沒有加任何Attribute
Defining a namespace prefix
在完成上面的代碼后,就可以在UXML文件里使用對應的Element了,如果是在Namespace里面自定義Element,還需要做額外的處理。
需要定義一個namspace prefix, Namespace prefixes其實就是在UXML的root element上面聲明的attributes,它會replace the full namespace name when scoping elements.
寫法如下:
// This can be done at the root level (outside any namespace) of any C# file of the assembly.
[assembly: UxmlNamespacePrefix("My.First.Namespace", "first")]
[assembly: UxmlNamespacePrefix("My.Second.Namespace", "second")]
schema generation系統會做這些事情:
- 檢查所有的attributes,使用它們創建schema,也就是XML文件里面的組織結構
- 為每一個新創建的UXML文件,在里面的
<UXML>
這個element上添加namespace prefix的定義 - includes the schema file location for the namespace in its
xsi:schemaLocation
attribute.
接下來,需要更新項目里的UXML schema,選擇Assets > Update UXML Schema,保證text editor可以辨別出來新的element。
The defined prefix is available in the newly created UXML by selecting Create > UI Toolkit > Editor Window in the Project/Assets/Editor folder.
Advanced usage
Customizing a UXML name
可以通過override繼承於UxmlFactory類的Property,代碼如下:
public class FactoryWithCustomName : UxmlFactory<..., ...>
{
// 暫時還不知道具體會展示在哪里
public override string uxmlName
{
get { return "UniqueName"; }
}
public override string uxmlQualifiedName
{
get { return uxmlNamespace + "." + uxmlName; }
}
}
Selecting a factory for an element
默認情況下,IUxmlFactory
會創建一個element,然后選擇根據它的名字來選擇對應的element,主要是為了讓它在UXML文件里能夠被識別出來
Writing UXML Templates
其實就是用XML語言寫的表示UI邏輯結構的uxml文件,舉個例子:
<-- 第一行是XML declaration, it is optional, 只可以出現在第一行, 前面不允許有空格-->
<-- version的attribute必須要寫, encoding可以不寫, 如果寫了, 就必須說清楚文件的字符encoding -->
<?xml version="1.0" encoding="utf-8"?>
<-- UXML 代表document root, 包含了用於namespace prefix definitions和schema的源文件位置的attributes -->
<UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <-- 下面這句話有點像是using UnityEngine.UIElements, 表示后面的Label什么的都是這個ns下的, 這里的ns是作為默認的ns -->
xmlns="UnityEngine.UIElements"
xsi:noNamespaceSchemaLocation="../UIElementsSchema/UIElements.xsd"
xsi:schemaLocation="UnityEngine.UIElements ../UIElementsSchema/UnityEngine.UIElements.xsd">
<-- 這下面的Label、Box、Button等都是Visual Element -->
<-- 前面的Label代表繼承於VisualElement的類名, 而后面的text叫做Element的Attributes--->
<Label text="Select something to remove from your suitcase:"/>
<Box>
<Toggle name="boots" label="Boots" value="false" />
<Toggle name="helmet" label="Helmet" value="false" />
<Toggle name="cloak" label="Cloak of invisibility" value="false"/>
</Box>
<Box>
<Button name="cancel" text="Cancel" />
<Button name="ok" text="OK" />
</Box>
</UXML>
補充幾點:
xmlns:engine="UnityEngine.UIElements"
,這種寫法,相當於是typedef,之后可以寫<engine:Button />
,等同於<UnityEngine.UIElements:Button />
- 如果在自己的namespace下自定義了UI Element,那么需要在
<UXML>
的tag里包含對應的 namespace definition and schema file location,同時還要包含Unity原本的namespaces
VisualElement通用的Attribute
一共有如下:
- name: Element的名字,應該是獨一無二的
- picking-mode:Position或者Ignore,用於鼠標事件
- focus-index: (OBSOLETE) Use tabIndex and focusable.
- tabindex:一個int,決定當前element的tabbing位置?
- focusable:a boolean indicating whether the element is focusable.
- class:a space-separated list of identifiers that characterize the element. Use classes to assign visual styles to elements. You can also use classes to select a set of elements in UQuery.
- tooltip:一個string
- view-data-key:一個string,定義了序列化element的key
創建UXML template asset
When you create a new UXML template asset by selecting Asset > Create > UI Toolkit > Editor Window, the Editor automatically defines namespaces for you.
Adding styles to UXML
UXML文件可以引用USS文件,需要在任何element的聲明下面使用<Style>
這個element,舉個例子:
<engine:UXML ...>
<engine:VisualElement class="root">
<-- 意思所有的VisualElement都在調用這個style.uss作為布局? -->
<Style src="styles.uss" />
</engine:VisualElement>
</engine:UXML>
此時的USS文化和UXML需要在相同文件夾下,具體的style.uss文件內容如下:
#root {
width: 200px;
height: 200px;
background-color: red;
}
也可以不要uss文件,直接UXML里一行代碼設置style:
<engine:UXML ...>
<engine:VisualElement style="width: 200px; height: 200px; background-color: red;" />
</engine:UXML>
Reusing UXML files
UXML文件也可以作為類似prefab的東西進行復用,舉個例子,這里有個當作人像的UXML文件,它的UI里有一個圖形和人名:
<engine:UXML ...>
<engine:VisualElement class="portrait">
<engine:Image name="portaitImage" style="--unity-image: url(\"a.png\")"/>
<engine:Label name="nameLabel" text="Name"/>
<engine:Label name="levelLabel" text="42"/>
</engine:VisualElement>
</engine:UXML>
在其他的UXML文件里,就可以把這個人像的UXML作為模板使用了:
<engine:UXML ...>
<-- 類名叫Template, 路徑src為...., Element的名字為Portrait, 感覺這里是創建了一個模板的類 -->
<engine:Template src="/Assets/Portrait.uxml" name="Portrait"/>
<engine:VisualElement name="players">
<-- Instance代表模板的示例, 后面template后面是類名, 然后根據name創建具體的Instance -->
<engine:Instance template="Portrait" name="player1"/>
<engine:Instance template="Portrait" name="player2"/>
</engine:VisualElement>
</engine:UXML>
總結來說,就是使用Template
和Instance
關鍵字,可以在UXML里使用別的UXML里創建的class
Overriding UXML attributes
即使基於UXML Template創建了Instance,還是可以override其elements里默認的Attribute的值。
具體操作如下,要寫一行xml語句指名下面的內容:
- 對應的想要override的Element的名字(The element-name attribute of the element whose attributes you want to override)
- 對應的想要override的Attribute的名字(The name of the attribute to override)
- override的值(The new attribute value)
舉個例子,看下面這段代碼:
<-- 由於override的是Instance不是Template, 所以可以輸入多個參數,比如這里輸入 兩個參數:一個是類名,一個是Element的名字,滿足這兩個條件的Element, 其text的attribute都會被Override -->
<AttributeOverrides element-name="player-name-label" text="Alice" />
再舉一個例子,假設有不同的玩家,他們都要展示相同的Template,但是每個人具體的數值不同:
<-- 指明namespace -->
<UXML xmlns="UnityEngine.UIElements">
<-- 其實是UnityEngine.UIElements.Label -->
<-- 創建兩個Label, 名字分別為player-name-label和player-score-label -->
<Label name="player-name-label" text="default name" />
<Label name="player-score-label" text="default score" />
</UXML>
在創建完模板后,可以創建其Instance,然后override它的attributes,其實就是語法上的學習,沒什么難度:
<-- 添加兩個namespace的include -->
<UXML xmlns="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
<-- 聲明使用的模板和路徑 -->
<Template src="MyTemplate.uxml" name="MyTemplate" />
<-- 基於名為MyTemplate模板創建Instance -->
<Instance name="player1" template="MyTemplate">
<-- Override兩個element的text對應的attribute -->
<AttributeOverrides element-name="player-name-label" text="Alice" />
<AttributeOverrides element-name="player-score-label" text="2" />
</Instance>
<Instance name="player2" template="MyTemplate">
<AttributeOverrides element-name="player-name-label" text="Bob" />
<AttributeOverrides element-name="player-score-label" text="1" />
</Instance>
</UXML>
Overriding multiple attributes
上面的例子都只override了一個attribute,用同樣的方法還可以ovverride多個attribute:
<-- ovverride text和tooltip兩個attribute -->
<AttributeOverrides element-name="player-name-label" text="Alice" tooltip="Tooltip 1" />
Nesting attribute overrides
When you override attributes in nested templates, the deepest override takes precedence.
UXML里引用其他的文件
UXML文件可以引用別的UXML文件和USS文件
其中,<Template>
和Style
兩種Element可以接受src
或者path
的attribute,二者有些許差別。
src
存的是相對路徑,要么是相對於Project Root路徑,要么是相對於所在的UXML文件的路徑。舉個例子,我的UXML文件在Assets\Editor\UXML下,USS文件在Assets\Editor\USS下:
- 如果要從UXML里讀取別的USS文件,那么src為
src="../USS/styles.uss"
,如果要讀取別的UXML文件,那么src="template.uxml"
- 使用Project Root的路徑
src="/Assets/Editor/USS/styles.uss"
orsrc="project:/Assets/Editor/UXML/template.uxml"
.
path
path只支持在Resources或者Editor的Resouces下的文件夾的文件:
- 如果在普通的Resources文件夾下,不需要file的拓展,比如
path="template"
代表Assets/Resources/template.uxml
。 - 如果是在Editor Default Resources文件夾下,需要帶文件的拓展名,比如
path="template.uxml"
代表Assets/Editor Default Resources/template.uxml.
****
C#讀取UXML文件
很簡單,記錄下寫法:
// 寫法一
var template = EditorGUIUtility.Load("path/to/file.uxml") as VisualTreeAsset;
// 這里的parentElement, 可以是EditorWindow下的rootVisualElement
template.CloneTree(parentElement, slots);
// 寫法二
var template = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("path/to/file.uxml");
template.CloneTree(parentElement, slots);
實際使用的時候大概是這樣:
public class MyWindow : EditorWindow {
[MenuItem ("Window/My Window")]
public static void ShowWindow () {
EditorWindow w = EditorWindow.GetWindow(typeof(MyWindow));
VisualTreeAsset uiAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/MyWindow.uxml");
VisualElement ui = uiAsset.CloneTree(null);
w.rootVisualElement.Add(ui);
}
void OnGUI () {
// Nothing to do here, unless you need to also handle IMGUI stuff.
}
}
UQuery
UQuery是Unity實現的自己版本的JQuery/Linq,可以使用UQuery獲取VisualElement的子節點Tree里特定的Element,示例代碼如下:
// 查找第一個叫foo的Button
root.Query<Button>("foo").First();
// 對每個叫foo的Button做...
root.Query("foo").Children<Button>().ForEach(//do stuff);
UXML elements reference
總結了UnityEngine.UIElements和UnityEditor.UIElements命名空間下可以用 的UXML Element:
基本的Element
就兩種:
- Visual Element:
- Bindable Element:可以綁定到一個SerializedProperty上的Element,相當於把UI對象和Property綁定到一起,它有一個binding-path的Attribute,表示綁定的Property的Path
兩個Base Element都在UnityEngine.UIElements下,而其實BindableElement也是VisualElement:
public class BindableElement : VisualElement, IBindable
Utilities
提供的常用的UI Element有:
- Box:可以有任意個數的Child Element,Attribute和Visual Element相同,無非是UI上,Content周圍多了個方框
- TextElement:VisualElement多一個Text的Attribute,不可以有Child Element
- Label:Attribute和Visual Element相同,不可以有Child Element
- Image:Attribute和Visual Element相同,不可以有Child Element
- IMGUIContainer:繼承於Visual Element,不可以有Child Element,用於繪制ImGUI的東西,添加了
focus-index
和focusable
兩個Attribute - Foldout:可以有任意個數的Child Element,有個Toggle可以開啟或者隱藏其Conten,應該本質是BinndableElement
這些Element都是在UnityEngine.UIElements下
Templates
一共三種:
- Template:
- Instance:
- TemplateContainer:
太多了,自己看吧。。。。
https://docs.unity3d.com/2021.2/Documentation/Manual/UIE-ElementRef.html
Unity style sheets (USS)
每個Visual Element都有一個style屬性,可以使用USS文件來定義它的UI,規則如下:
- 后綴為.uss
- 只支持style rules(?)
- Style rules由一個Selector和一個declaration block組成
- The selector identifies which visual element the style rule affects.
- The declaration block, enclosed by curly braces, contains one or more style declarations. Each style declaration is comprised of a property and a value. Each style declaration ends with a semi-colon.
- style property是一個literal,when parsed, must match the target property name.
Style Rule
我理解的就是語法規則,如下所示:
selector {
property1:value;
property2:value;
}
Attaching USS to visual elements
- uss添加到visual element之后,還會應用到其所有的子elements上
- 使用
AssetDatabase.Load()
或Resources.Load()
加載文件,使用VisualElement.styleSheets.Add()
添加stylesheet
**Style matching with rules** StyleSheet可以直接添加到一個Visual Tree上,它會自動去匹配: ```css /* 自動匹配叫做Button的Visual Element */ Button { width: 200px; } ```
USS Selector
USS Selector負責根據uss文件里的內容名字,找到對應匹配的Style Rule,在我理解,Selector本質就是一些語法,通過不同的語法,可以實現uss里的Style Rule能應用到指定的Visual Element上
常見的寫法:
#name{}
Button{}
.classlist{}
附錄
刪除Visual Element的寫法
// 刪除一整個數組的UI Element
for (int i = 0; i < modelAreasUI.Count; i++)
{
modelAreasUI[i].parent.Remove(modelAreasUI[i]);
}
modelAreasUI.Clear();
XML Elements vs. Attributes
XML的Element可以擁有Attribute,二者是從屬關系,比如下面的
<person gender="female">
里的person是Element,而gender是Attribute
再看兩個例子:
<!-- 第一個例子 -->
<person gender="female">
<firstname>Anna</firstname>
<lastname>Smith</lastname>
</person>
<!-- 第二個例子 -->
<person>
<gender>female</gender>
<firstname>Anna</firstname>
<lastname>Smith</lastname>
</person>
第一個例子里,gender是Attribute,第二個例子里,gender是element
什么是XML Schema
參考來源:https://www.w3schools.com/xml/schema_intro.asp
https://www.differencebetween.com/difference-between-xml-and-vs-xsd/
schema翻譯過來是模式、概要和議程。在計算機術語里,schema經常用於描述不同類型的數據的structure,最通用的就是數據和XML的schemas。
An XML Schema describes the structure of an XML document. The XML Schema language is also referred to as XML Schema Definition (XSD). 如下所示是一個XSD的例子:
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="note">
<xs:complexType>
<xs:sequence>
<xs:element name="to" type="xs:string"/>
<xs:element name="from" type="xs:string"/>
<xs:element name="heading" type="xs:string"/>
<xs:element name="body" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
核心在於,xml schema旨在定義XML文檔本身的結構和內容,xml和xml schema的區別,也可以認為是XML和XSD的區別。在我理解,比如說xml里的node的節點關系,element可以添加attribute這些,應該都是schema來設置的。
flex-grow屬性是干嘛的
參考鏈接:https://css-tricks.com/snippets/css/a-guide-to-flexbox/
在VisualElement里有一個Property:
StyleFloat flexGrow: Specifies how much the item will grow relative to the rest of the flexible items inside the same container.
本質上flexGrow是一個float值,這個概念源於Flexbox Layout
,用於為那些尺寸不確定、或者說是動態的Box進行布局的分配,其核心在於,在一個固定尺寸的Container里,如何靈活的變化里面的Box的尺寸,讓他們能布局在Container里
The Flexbox Layout (Flexible Box) module (a W3C Candidate Recommendation as of October 2017) aims at providing a more efficient way to lay out, align and distribute space among items in a container, even when their size is unknown and/or dynamic (thus the word “flex”).
如下圖所示,是Flexbox的相關概念:
具體有以下概念:
- main axis:該軸的方向決定了flex item的擺放方向,具體是水平擺放還是豎直擺放,取決於
flex-direction
屬性 - main-start | main-end:代表flex items沿着main axis的擺放區間
- main size:flex container沿着main axis的尺寸
- cross開頭的相關的屬性與main的差不多
uss或者說css相關的layout的代碼,根據作用的對象,可以分為兩種,由於Visual Element,往往是Parent作為所有Children的容器,所以這里分為:
- 作用在父節點,也就是容器上的屬性
- 作用在子節點上的屬性
作用在父節點,也就是容器上的屬性
display
如下所示,可以定義一個允許子節點靈活變化的容器:
/* 可以選擇flex或者inline-flex */
.container {
display: flex; /* or inline-flex */
}
flex-direction
決定了main-axis的方向,也就是容器里的元素排列的方向,一共四種:左到右、右到左、上到下、下到上
.container {
flex-direction: row | row-reverse | column | column-reverse;
}
如下圖所示:
flex-wrap
正常情況下,flex container里的flex items會盡量放到一行(或一列),這里可以通過flex-wrap設置,允許它在需要的時候放到多行
.container {
flex-wrap: nowrap | wrap | wrap-reverse;
}
- nowrap(default):默認下,所有的flex items都在一行
- wrap: 多行,從上到下
- wrap-reverse:多行,從下到上
flex-flow
它是flex-direction和flex-wrap的總體簡稱,默認的就是row nowrap:
/* main axis沿豎直方向, 而且有wrap */
.container {
flex-flow: column wrap;
}
justify-content
This defines the alignment along the main axis. 還有一些定義,可以定義main axis上的flex items對齊的一些方法,如下圖所示:
代碼如下:
.container {
justify-content: flex-start | flex-end | center | space-between | space-around | space-evenly | start | end | left | right ... + safe | unsafe;
}
align-items
This defines the default behavior for how flex items are laid out along the cross axis on the current line. 前面決定的是flex items沿着main axis的對齊,這里指的是flex items沿着cross axis的對齊,如下圖所示,main-aixs是橫向的,cross axis是縱向的:
寫法如下:
.container {
align-items: stretch | flex-start | flex-end | center | baseline | first baseline | last baseline | start | end | self-start | self-end + ... safe | unsafe;
}
align-content
感覺跟align-items很像,如下圖所示:
代碼如下:
.container {
align-content: flex-start | flex-end | center | space-between | space-around | space-evenly | stretch | start | end | baseline | first baseline | last baseline + ... safe | unsafe;
}
子節點自身的屬性
前面提到的flex屬性都是針對flex container的,用於調整里面的元素的layout,下面介紹用於container里面具體的item的property
order
flex item有個屬性叫order,用於確定其排序,如下圖所示:
.item {
order: 5; /* default is 0 */
}
flex-grow
This defines the ability for a flex item to grow if necessary. 其實就是在它所有的兄弟里面,它試圖占有的權重值,如下圖所示,權重為2的,長度也是2倍,如果所有的flex item的flex-grow都是1,那么他們的長度還會是一樣的:
.item {
flex-grow: 4; /* default 0 */
}
flex-shrink
如果有必要的話,一個flex item會收縮
.item {
flex-shrink: 3; /* default 1 */
}
flex-basis
代表元素被分配尺寸之前的默認尺寸,代碼如下:
.item {
flex-basis: | auto; /* default auto */
}
- auto:會基於flex-grow計算額外的空間
除了auto,還有: - content:基於item的content計算size
- 0:the extra space around content isn’t factored in.
flex
flex-grow(子節點擴大權重)、flex-shrink(允許收縮的程度)和flex-basis(基本默認尺寸)這三個屬性的總體簡稱,代碼如下:
/*It is recommended that you use this shorthand property rather than set the individual properties. The shorthand sets the other values intelligently.*/
.item {
flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
}
align-self
自定義一個元素的alignment:
.item {
align-self: auto | flex-start | flex-end | center | baseline | stretch;
}
UI Element的相關layout信息的總結
// 獲得Visual Element的實際尺寸
element.resolvedStyle.width //(recommended)
element.resolvedStyle.height //(recommennded)
element.worldBound //(relative to the EditorWindow)
element.transform
element.layout
但是要注意一點,這些參數都不會在第一幀創建對應的element之后馬上生效,而是需要等待Unity計算每個元素的size和position之后,才可以生效。
如果想要在該值可用后的第一時間讀取該值,可以在該元素上登記GeometryChangeEvent回調函數
VisualTreeAsset
在代碼里看到了這個類,主要是API方面,類的定義如下:
// 此類的實例表示一個Visual Element的Tree, 這個Tree是從UXML文件里讀取出來的
// 在UXML文件里, 每一個Node(xml概念里的Node)都代表一個VisualElementAsset
public class VisualTreeAsset : ScriptableObject
{
public VisualTreeAsset();
...
}
其實這個類就是幫助從UXML文件里,得到對應的Visual Element的,代碼如下所示:
VisualTreeAsset template = EditorGUIUtility.Load("Assets/TrainningDataViewer.uxml") as VisualTreeAsset;
VisualElement root = template.CloneTree();
Unity自帶的Manipulator
如下圖所示,在UnityC#的源碼里去引用得到的:
分為兩種,一類是在UnityEditor下用到,這里提到的Inserter、SelectionDropper、ShortcutHandler和ContentZoomer都是在GraphView的Namespace里提供的,而MouseManipulator是Unity UI Elements命名空間下的。
繼承MouseManipulator的有:
其中,ElementResizer、ClickSelector、ContentDragger、Dragger、EdgeConnector、EdgeManipulator、FreehandSelector和RectangleSelector都是在GraphView的命名空間下的
UI Element如何創建Enum Field
其實在UI Samples里都有介紹,代碼如下:
// 在uxml里加入Enum Field(也可以在代碼里加入)
<uie:EnumField label="MyEnum" value="2D" name="MyEnum"/>
// 在C#腳本里
enum MyEnum
{
One,
Two
}
var enumField = rootVisualElement.Q<EnumField>("MyEnum");
enumField .Init(MyEnum.One);// 初始值
enumField .value = MyEnum.Two;// 再設別的值
UI Element的PopuoField的使用
參考鏈接:https://docs.unity3d.com/Packages/com.unity.ui@1.0/api/UnityEditor.UIElements.PopupField-1.html
構造函數的接口:
public PopupField(string label, List<T> choices, T defaultValue, Func<T, string> formatSelectedValueCallback = null, Func<T, string> formatListItemCallback = null)
實際使用:
List<string> s = new List<string>();
s.Add("321");
s.Add("11");
var ClipsField = new PopupField<string>("Choose Clips", s, "11");
Add(ClipsField);// 加到一個Visual Element里
效果如下圖所示,跟EnumField有點像:
可以通過下面的方式直接進行選擇:
// 相當於點選第21個choice
ClipsField.index = 20;
UI Element接受Keyboard Event
參考:https://forum.unity.com/threads/any-good-way-to-find-out-if-a-keyboard-key-is-pressed-with-the-mouse-over-a-visualelement.1063190/
相關的event可以寫在MouseManipulator類里,不過使用之前,一定要注意,這里的Keyboard Event只對focused element起作用,所以要保證:
focusable = true;
然后還要保證接受的Element處於focused狀態
UI Element在鼠標hover的時候改變顏色
這種判斷UI Element的UI狀態的,Unity里叫做Pseudo-classes,有這么幾種:
舉個例子:
-- 連續用兩個: 來表示兩個狀態的與 Toggle:checked:hover
{
background-color: yellow;
}
如下圖所示:
再比如我自己創建的繼承於Button的類:
AnimClipButton:hover
{
background-color: rgba(99, 99, 99, 255);
}
// 注意普通的 background-color也要寫在這里,不要用腳本控制,不然腳本控制顏色的優先級永遠高於stylesheets
// 也不要直接更改Button類的background-color,可能跟Unity對Button自身的Stylesheets起沖突
AnimClipButton
{
background-color: rgba(56, 56, 56, 255);
}
樣子是這樣:
我還嘗試在自己繼承的Button類上面加,focus的代碼,這么寫:
// cs文件里
myBtn.focusable = true
// uss文件里
MyButton:focus
{
background-color: rgba(99, 99, 99, 255);
}
但是沒有效果,可能是只有繼承了Unity的UI Element的Focusable類才可以: