Unity UI Toolkit Manual文檔閱讀記錄


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)

二者的區別如下:

  1. 目的不同,內置的UI Tooklit旨在加強Unity Editor的編輯,很多Unity Editor的自帶功能都是用的內置的UI Toolkit,而Unity Package里的版本添加了很多特性,用於制作runtime下的UI
  2. 二者使用方式是相同的,都是在UnityEditor.Elements和UnityEngine.Elements的命名空間下使用

該選擇UI Toolkit兩個版本的哪一個
如果相關UI只會在Editor下使用的話,那么使用內置的UI Toolkit,如果該UI需要既能在Editor,也能在Runtime下使用的話,那么使用對應的Package的版本,而且對應的版本也能安裝最新的

安裝 UI Toolkit package
打開Unity Editor的Package Manager:

  1. Click Add (+)
  2. From the menu, choose Add package from git URL…
  3. In the text field, type com.unity.ui
  4. 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
坦白說,這一段文檔官方文檔居然沒有配合具體的代碼展示,感覺官方寫的東西就是一坨屎,下面會基於這坨垃圾玩意兒,進行解釋,然后加上自己的解釋和樣例去幫助理解。

  1. 創建類的基本定義
    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而已,如下圖所示:
在這里插入圖片描述

  1. 創建相關的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> { }
	...
};
  1. 創建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>

總結來說,就是使用TemplateInstance關鍵字,可以在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" or src="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-indexfocusable兩個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類才可以:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM