Runtime DataBinding(使用UI Builder)

以下將示範如何使用UI BuilderScriptableObject來建立DataBinding

  1. 在Unity編輯器中,建立一個Script
  2. 建立一個名為ExampleObjectScriptableObject(檔案會是 ExampleObject.cs),它包含了
    • 一個string vector3Label,
    • 一個Vector3 vector3Value
    • 一個float sumOfVector3Properties,這個sumOfVector3Properties是一個readonly的屬性其值來自於vector3Label中的xyz之和,
    • 一個float dangerLevel是一個 0 ~ 1之間的數值。之後在UI Builder中更改使用Value To Progress設定,讓它可以根據ConverterGroup顯示對應的值
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      using Unity.Properties;
      using UnityEditor;
      using UnityEngine;
      using UnityEngine.UIElements;

      [CreateAssetMenu]
      public class ExampleObject : ScriptableObject
      {
      [InitializeOnLoadMethod]
      public static void RegisterConverters()
      {

      // Create local Converters
      var group = new ConverterGroup("Value To Progress");

      // Converter groups can have multiple converters. This example converts a float to both a color and a string.
      group.AddConverter((ref float v) => new StyleColor(Color.Lerp(Color.red, Color.green, v)));
      group.AddConverter((ref float value) =>
      {
      return value switch
      {
      >= 0 and < 1.0f / 3.0f => "Danger",
      >= 1.0f / 3.0f and < 2.0f / 3.0f => "Neutral",
      _ => "Good"
      };
      });

      // Register the converter group in InitializeOnLoadMethod to make it accessible from the UI Builder.
      ConverterGroups.RegisterConverterGroup(group);
      }

      [Header("Bind to multiple properties")]
      public string vector3Label;
      public Vector3 vector3Value;

      [CreateProperty]
      public float sumOfVector3Properties => vector3Value.x + vector3Value.y + vector3Value.z;

      [Header("Binding using a converter group")]
      [Range(0, 1)] public float dangerLevel;
      }
  3. 在Unity編輯器中,選擇 Create > Example Object ,建立一個Example Object並命名為Object1 (檔案會是 Object1.asset)
  4. 建立一個UXML,命名為ExampleObject(檔案會是ExampleObject.uxml)
  5. 點擊剛剛建立的ExampleObject.uxml,開啟UI Builder
  6. 在Hierarchy panel中,加入一個VisualElement
  7. 在剛剛建立的VisualElement中加入Vector3FieldFloatFieldLabel
  8. 最後Hierarchy panel畫面為
  9. 選擇最上層的VisualElement UI Element,在右邊找到Data Source,選擇之前建立的Object1.asset,這樣做,會將它的Child Elements都預設綁定Object1
  10. 選擇Vector3Field UI Element,在右邊找到Label,對Label按一下滑鼠右鍵,點選Add binding...,在Data Source Path中找到並選擇vector3Label,Binding Mode選擇To Target
  11. 接下來,對Vector3FieldValue按一下滑鼠右鍵,點選Add binding...,在Data Source Path中找到並選擇vector3Value,Binding Mode選擇To Target
  12. 選擇Float UI Element,在右邊找到Value按一下滑鼠右鍵,點選Add binding...,在Data Source Path中找到並選擇sumOfVector3Properties,Binding Mode選擇To Target
  13. 選擇Label UI Element,
    • 在右邊找到Text,對Text按一下滑鼠右鍵,點選Add binding...,在Data Source Path中找到並選擇dangerLevel,Binding Mode選擇To Target
    • 接著打開Advanced Settings 找到 Converters > To target property (UI) 選擇 Value To Progress
    • 接下來,在右邊找到Color,對Color按一下滑鼠右鍵,點選Add binding...,在Data Source Path勾選Show only compatible之後才會出現可以選擇的相容屬性,選擇dangerLevel,Binding Mode選擇To Target
    • 接著打開Advanced Settings 找到 Converters > To target property (UI) 選擇 Value To Progress
  14. 至此,這個UI與 ExampleObject的綁定就算完成了,UXML可能會為
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <engine:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:engine="UnityEngine.UIElements" xmlns:editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
    <engine:VisualElement data-source="project://database/Assets/UI/Custom/Object1.asset?fileID=11400000&amp;guid=a0e72bbf894f04543b805067235fe91c&amp;type=2#Object1" style="flex-grow: 1;">
    <engine:Vector3Field name="Vector3Field" focusable="false">
    <Bindings>
    <engine:DataBinding property="label" binding-mode="ToTarget" data-source-path="vector3Label" />
    <engine:DataBinding property="value" binding-mode="ToTarget" data-source-path="vector3Value" />
    </Bindings>
    </engine:Vector3Field>
    <engine:FloatField label="Float Field" name="FloatField">
    <Bindings>
    <engine:DataBinding property="value" binding-mode="ToTarget" data-source-path="sumOfVector3Properties" />
    <engine:DataBinding property="text" binding-mode="ToTarget" data-source-path="dangerLevel" source-to-ui-converters="Value To Progress" />
    <engine:DataBinding property="text" binding-mode="ToTarget" data-source-path="dangerLevel" source-to-ui-converters="Value To Progress" />
    </Bindings>
    </engine:FloatField>
    <engine:Label text="Label" name="Label" enable-rich-text="false" parse-escape-sequences="true">
    <Bindings>
    <engine:DataBinding property="value" binding-mode="ToTarget" data-source-path="sumOfVector3Properties" />
    <engine:DataBinding property="text" binding-mode="ToTarget" data-source-path="dangerLevel" source-to-ui-converters="Value To Progress" />
    <engine:DataBinding property="text" binding-mode="ToTarget" data-source-path="dangerLevel" source-to-ui-converters="Value To Progress" />
    </Bindings>
    </engine:Label>
    </engine:VisualElement>
    </engine:UXML>
  15. 可以調整Object1來觀察UI的變化
  16. 結果

Reference: https://docs.unity3d.com/2023.2/Documentation/Manual/UIE-get-started-runtime-binding.html

暴露自訂control給UXML

  1. 自訂control需要繼承VisualElement並且這個自訂control需要有一個預設constructor
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class StatusBar : VisualElement
    {
    // 需要有一個預設constructor
    public StatusBar()
    {
    m_Status = String.Empty;
    }

    string m_Status;
    public string status { get; set; }
    }
  2. 為了讓你自訂的control可以在UXML中使用,你需要定義一個factory class,如果沒有特殊的需求,可以直接繼承UxmlFactory<T>
    1
    2
    3
    4
    5
    6
    7
    class StatusBar : VisualElement
    {
    // 建議直接將factory class放在自訂的control中
    public new class UxmlFactory : UxmlFactory<StatusBar> {}

    // ...
    }
  3. UxmlTraits:定義在UXML中可以使用的特徵(UXML traits),
    • UxmlTraits的目的有
      • factory建立新物件時使用它們。
      • 在schema產生時,會分析它們以取得關於該Element的資訊,之後將這些資訊轉換為XML schema directive。
    • 以下範例
      • 宣告了一個m_Status,用來定義一個XML attribute status
      • uxmlChildElementsDescription回傳一個空IEnumerable,用來表明這個自訂的StatusBar沒有child。
      • Init()中,XML parser從property bag讀出的值設定給StatusBar.status
      • UxmlTraits class定義在StatusBar class中,讓Init()可以存取StatusBar的私有變數。
      • UxmlTraits繼承了VisualElement.UxmlTraits,它也擁有VisualElement.UxmlTraits的屬性
      • Init()會呼叫base.Init()以初始化base class的屬性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class StatusBar : VisualElement
{
public new class UxmlFactory : UxmlFactory<StatusBar, UxmlTraits> {}

public new class UxmlTraits : VisualElement.UxmlTraits
{
UxmlStringAttributeDescription m_Status = new UxmlStringAttributeDescription { name = "status" };

public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
get { yield break; }
}

public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
((StatusBar)ve).status = m_Status.GetValueFromBag(bag, cc);
}
}

// ...
}
- 注意:在UI Builder中創作時,UI Builder可能會多次呼叫`UxmlTraits.Init()`來同步UXML檔案中的值,建議使用`GetValueFromBag`而不是`TryGetValueFromBag`以確保當UI Builder取消設定(unset)element的值時,這個element不會殘留先前的值。
  1. 若想要有child的話,需要override uxmlChildElementsDescription
    1
    2
    3
    4
    5
    6
    7
    public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
    {
    get
    {
    yield return new UxmlChildElementDescription(typeof(VisualElement));
    }
    }
  2. 可以將你自訂的control放到namespace中來分類它們
    1
    2
    [assembly: UxmlNamespacePrefix("My.First.Namespace", "first")]
    [assembly: UxmlNamespacePrefix("My.Second.Namespace", "second")]

Reference:https://docs.unity3d.com/Manual/UIE-expose-custom-control-to-uxml.html#define-a-factory

自訂control

建立自訂的Control需要建立一個control C# class,然後將它暴露給UXML

  1. 建立一個繼承VisualElement的Class

  2. 若是要建立可綁定(Binding)的自訂Control,可以繼承自BaseField基底類別,而非 BindableElement。繼承BaseField有以下好處

    • 有實作一個泛型的INotifyValueChanged
    • 預設focusable
    • 提供一個水平佈局(horizontal layout),左側為label element,右側為input element
  3. VisualElement不會綁定到GameObject的生命週期函數,也就是說它不會收到以下callbacks

    • Awake()
    • OnEnable()
    • OnDisable()
    • OnDestroy()
  4. 可以在自訂control中的constructor做初始化的動作,但是如果你想要將初始化的動作延遲到這個自訂的control被加到UI之後,可以把初始化的動作加到AttachToPanelEvent callback

  5. 使用DetachFromPanelEvent callback可以偵測你自訂的control是否已經從UI上移除

    1
    2
    3
    4
    5
    6
    7
    8
    public CustomControl()
    {
    var myCustomElement = rootVisualElement.Q(className: "my-custom-element");
    myCustomElement.RegisterCallback<AttachToPanelEvent>(e =>
    { /* 此處放置當這個elemenet被加入到UI上要做的事 */ });
    myCustomElement.RegisterCallback<DetachFromPanelEvent>(e =>
    { /* 此處放置當這個elemenet從UI上移除後要做的事 */ });
    }
  6. UI Toolkit會分派這兩個事件(AttachToPanelEvent與DetachFromPanelEvent)給所有element

  7. 要讓UXMLUI Builder使用你自訂的control,需要定義一個繼承UxmlFactory<T>factory class將你自訂的control暴露給他們。

  8. 綁定資料需要實作INotifyValueChanged並根據需要監聽ChangeEvent。繼承BindableElement或是實作IBindable

  9. 你可以創建USS custom properties以設定自訂控制項的樣式。

Reference: https://docs.unity3d.com/Manual/UIE-create-custom-controls.html

輸入事件(Input events)

當字串透過使用者輸入到Text field時,便會觸發InputEvent,與觸碰螢幕的PointerCaptureOutEvent類似。對於預設的鍵盤輸入,每次按一下鍵(keystroke)都會觸發InputEvent。但是對於間接來源(indirect source)的輸入,將不會觸發InputEvent,例如自動化腳本(automated script)。

專屬於Input Event的屬性

  • previousData: 存放先前的資料
  • newData: 存放新的資料

當資料輸入到繼承了TextInputBaseField的control便會發送InputEventInputEvent不同於ChangeEvent的地方在於,即使輸入到control的資料沒改變,它也會發送InputEvent

例子:下面例子為一個TextField註冊一個InputEvent,每當觸發便會印出輸出的的值
  1. 在Assets > Scripts > Editor 下建立一個 C# Script InputEventExample.cs
  2. 將以下程式碼複製到剛剛建立的C# Script中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.UIElements;

    public class InputEventExample : EditorWindow
    {
    // 這個Attributes會在 Windows -> UI Tollkit 下加入一個 InputEventExample
    [MenuItem("Window/UI Toolkit/InputEventExample")]
    public static void ShowExample()
    {
    var wnd = GetWindow<InputEventExample>();
    wnd.titleContent = new GUIContent("Input Event Example");
    }

    public void CreateGUI()
    {
    // 建立 TextField
    TextField textField = new TextField();
    rootVisualElement.Add(textField);

    // 註冊InputEvent
    textField.RegisterCallback<InputEvent>(OnInput);
    }

    private void OnInput(InputEvent evt)
    {
    // 印出InputEvent中的newData與previousData
    Debug.Log("newData=" + evt.newData + " previousData=" + evt.previousData);
    }
    }
  3. 在Unity編輯器中找到 Window > UI Toolkit > InputEventExample
  4. 顯示結果

Reference: https://docs.unity3d.com/Manual/UIE-Input-Events.html

焦點事件(Focus events)

當你需要把焦點移到某個visual element或移開時,可以使用Focus event。

Focus events分為兩種不同的類型:

  • FocusOutEventFocusInEvent:在焦點發生變化之前,焦點離開事件(FocusOutEvent)和焦點進入事件(FocusInEvent)會沿著事件傳播路徑(propagation path)發送。
  • FocusEventBlurEvent:焦點事件(FocusEvent)和失焦事件(BlurEvent)會在焦點發生變化後立即發送到事件目標(event target)
Event 描述 Trickles down Bubbles up Cancellable
FocusOutEvent 在失去焦點之前,會發送一個FocusOutEvent
FocusInEvent 在element取得焦點之前,會發送一個FocusInEvent
BlurEvent 在失去焦點之後,會發送一個BlurEvent
FocusEvent 在element取得焦點之後,會發送一個FocusEvent

專屬於Focus Event的屬性

  • relatedTarget:在焦點事件中參與的第二個target element。
    • 對於FocusOutEventBlurEvent,這個屬性為聚焦的element。
    • 對於FocusInEventFocusEvent,這個屬性為失去焦點的element。
Event target relatedTarget
BlurEvent 失去焦點的element 取得焦點的element
FocusEvent 取得焦點的element 失去焦點的element
FocusInEvent 取得焦點的element 失去焦點的element
FocusOutEvent 失去焦點的element 取得焦點的element
例子:使用FocusInEventFocusOutEvent為TextField實現placeholder text功能
  1. 在Assets > Scripts > Editor 下建立一個 C# Script PlaceHolderExample.cs
  2. 將以下程式碼複製到剛剛建立的C# Script中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.UIElements;

    public class PlaceHolderExample : EditorWindow
    {
    // 這個Attributes會在 Windows -> UI Tollkit 下加入一個 PlaceHolderExample
    [MenuItem("Window/UI Toolkit/PlaceHolderExample")]
    public static void ShowExample()
    {
    PlaceHolderExample wnd = GetWindow<PlaceHolderExample>();
    wnd.titleContent = new GUIContent("PlaceHolderExample");
    }

    private bool placeHolderMode = true;
    private const string placeHolderText = "Write here";

    public void CreateGUI()
    {
    // 建立 TextField
    TextField textField = new TextField();
    textField.value = placeHolderText;
    rootVisualElement.Add(textField);

    // 註冊FocusInEvent與FocusOutEvent
    textField.RegisterCallback<FocusInEvent>(OnFocusInTextField);
    textField.RegisterCallback<FocusOutEvent>(OnFocusOutTextField);
    }

    private void OnFocusInTextField(FocusInEvent evt)
    {
    // 如果這個 text field 收到聚焦,表示使用者想要輸入字串,因此要將placeholder文字清空
    if (placeHolderMode)
    {
    var textField = evt.target as TextField;
    textField.value = "";
    }
    }

    private void OnFocusOutTextField(FocusOutEvent evt)
    {
    // 如果這個 text field 失焦,表示使用者已經完成輸入,但如果text field為空字串,則要將placeholder文字填回去。
    var textField = evt.target as TextField;
    placeHolderMode = string.IsNullOrEmpty(textField.value);
    if (placeHolderMode)
    textField.value = placeHolderText;
    }
    }
  3. 在Unity編輯器中找到 Window > UI Toolkit > PlaceHolderExample

Reference: https://docs.unity3d.com/Manual/UIE-Focus-Events.html

點擊事件(Click events)

當一個Visual Element被滑鼠左鍵(或點擊裝置(pointing device)的第一個按鈕)點擊,將觸發一個Click Event。進行點擊操作的Visual Element會觸發Pointer Down EventPointer up Event。此外Click Event也可以檢測到非按鈕Visual Element的點擊,例如Toggle control便是使用了Click Event來處理顯示,隱藏勾號(check mask)並改變control的值。

Event 描述 Trickles down Bubbles up Cancellable
ClickEvent 當滑鼠左鍵點擊時,就會觸發一個Click Event
註冊Click Event
1
2
3
4
5
6
7
8
// 使用RegisterCallback註冊ClickEvent,同時還傳入一個VisualElement參數
// 這個asset是當click event被觸發之後,要關閉的element
btnClose.RegisterCallback<ClickEvent, VisualElement>(Clicked, asset);

private void Clicked(ClickEvent evt, VisualElement root)
{
root.ShowVisualElement(false);
}
例子:以下例子示範如何使用Click Event讓一個visual element在被點擊之後將原有顏色,替換為另一個新的顏色。
  1. 在Assets > Scripts > Editor 下建立一個 C# Script ClickEventExampleWindow.cs
  2. 將以下程式碼複製到剛剛建立的C# Script中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.UIElements;

    public class ClickEventExampleWindow : EditorWindow
    {
    // 這個Attributes會在 Windows -> UI Tollkit 下加入一個 ClickEventExample
    [MenuItem("Window/UI Toolkit/ClickEventExample")]
    public static void ShowExample()
    {
    var wnd = GetWindow<ClickEventExampleWindow>();
    wnd.titleContent = new GUIContent("Click Event Example");
    }

    public void CreateGUI()
    {
    // 建立四個不同顏色的box
    for (int i = 0; i < 4; i++)
    {
    // 為這些Visual Element設定隨機的background顏色
    var newBox = new VisualElement() { style = { flexGrow = 1, backgroundColor = GetRandomColor() } };
    rootVisualElement.Add(newBox);

    // 為這個Element註冊一個ClickEvent callback
    newBox.RegisterCallback<ClickEvent>(OnBoxClicked);
    }
    }

    // 這個callback會改變target的Background color。
    private void OnBoxClicked(ClickEvent evt)
    {
    // 只在taget階段執行這個callback
    if (evt.propagationPhase != PropagationPhase.AtTarget)
    return;

    // 設定新的Background color
    var targetBox = evt.target as VisualElement;
    targetBox.style.backgroundColor = GetRandomColor();
    }

    private Color GetRandomColor()
    {
    return new Color(Random.Range(0, 1f), Random.Range(0, 1f), Random.Range(0, 1f));
    }
    }
  3. 在Unity編輯器中找到 Window > UI Toolkit > ClickEventExample
  4. 觀看結果

Reference: https://docs.unity3d.com/Manual/UIE-Click-Events.html

變更事件(Change events)

當一個Element的值改變時,就會發送一個Change Event,例如使用者切換了(toggles)一個核取方塊(Checkbox),Change Event是一個帶有類型的事件(Typed Event),它含有這個Element先前的值和新值。由於Change Event是在新的值被賦值後才觸發,因此你無法透過取消Change Event的方式去來避免值的改變。

Event 描述 Trickles down Bubbles up Cancellable
ChangeEvent 當一個Element的值改變時,就會發送一個Change Event

專屬於Change Event的屬性

  • previousValue: 存放該Element先前的值 The previous value of the target control.
  • newValue: 存放該Element該要改變的新值

Change Event是一個通知事件(notification event)讓你可以在visual element的值改變時做出反應,例如:當使用者按下音樂核取方塊(Checkbox)那麼遊戲的音樂就應該都被關閉。

Change Event應用到所有有實作INotifyValueChanged<T>的controls其中<T>就是ChangeEvent,此外這個也被在內部使用,透過Data binding的方式來更新實體物件的屬性

避免觸發Change Event

如果你是透過程式碼更改control的值而觸發control的Change Event,你可以透過呼叫INotifyValueChange<T>中的SetValueWithoutNotify來更改control的值,並避免觸發Change Event

註冊Change Event
  1. 呼叫在Visual Element上的RegisterCallback<>()

    • 如果你想要在一個Element中監聽它的子Elements中是否有發生改變的話,可以使用
      1
      2
      3
      4
      5
      6
      7
      8
      // 使用RegisterCallback註冊callback
      rootVisualElement.RegisterCallback<ChangeEvent<bool>>(OnBoolChangedEvent);

      // callback方法,它會監聽bool值是否發生改變
      private void OnBoolChangedEvent(ChangeEvent<bool> evt)
      {
      // Handling code
      }
  2. 呼叫有實作INotifyValueChange的Visual Element上的RegisterValueChangedCallback()

    • 使用RegisterValueChangedCallback會比較方便因為它已經內建(build-in)好要傳值得類型
    • 取消註冊可以使用UnregisterValueChangedCallback
      1
      2
      3
      4
      5
      6
      var newToggle = new Toggle("Test Toggle");
      newToggle.RegisterValueChangedCallback(OnTestToggleChanged);
      private void OnTestToggleChanged(ChangeEvent<bool> evt)
      {
      // Handling code
      }
例子一:註冊兩個ChangeEvent,一個在toggle element上使用RegisterValueChangedCallback,另外一個在root element使用RegisterCallback

以下例子為示範如何使用ChangeEvent

  1. 在Assets > Scripts > Editor 下建立一個 C# Script ChangeEventTestWindow.cs
  2. 將以下程式碼複製到剛剛建立的C# Script中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.UIElements;

    public class ChangeEventTestWindow : EditorWindow
    {
    private Toggle m_MyToggle;

    // 這個Attributes會在 Windows -> UI Tollkit 下加入一個 Change Event Test Window
    [MenuItem("Window/UI Toolkit/Change Event Test Window")]
    public static void ShowExample()
    {
    ChangeEventTestWindow wnd = GetWindow<ChangeEventTestWindow>();
    wnd.titleContent = new GUIContent("Change Event Test Window");
    }

    public void CreateGUI()
    {
    // 建立一個 toggle control
    m_MyToggle = new Toggle("Test Toggle") { name = "My Toggle" };
    rootVisualElement.Add(m_MyToggle);

    // 使用RegisterValueChangedCallback為這個 toggle control註冊OnTestToggleChanged
    m_MyToggle.RegisterValueChangedCallback(OnTestToggleChanged);

    // 使用RegisterCallback在root element註冊OnBoolChangedEvent,它必須要指定泛型
    rootVisualElement.RegisterCallback<ChangeEvent<bool>>(OnBoolChangedEvent);
    }

    private void OnBoolChangedEvent(ChangeEvent<bool> evt)
    {
    Debug.Log($"Toggle changed. Old value: {evt.previousValue}, new value: {evt.newValue}");
    }

    private void OnTestToggleChanged(ChangeEvent<bool> evt)
    {
    Debug.Log($"A bool value changed. Old value: {evt.previousValue}, new value: {evt.newValue}");
    }
    }
  3. 在Unity編輯器中找到 Window > UI Toolkit > Change Events Test Window
  4. 觀看結果
例子二:使用程式碼觸發事件的話,可以透過element上的SetValueWithoutNotify避免觸發Change Event
  • 將以下程式碼覆蓋到ChangeEventTestWindow.cs Script中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.UIElements;

    public class ChangeEventTestWindow : EditorWindow
    {
    private Toggle m_MyToggle;

    [MenuItem("Window/UI Toolkit/Change Event Test Window")]
    public static void ShowExample()
    {
    GetWindow<ChangeEventTestWindow>().titleContent = new GUIContent("Change Event Test Window");
    }

    public void CreateGUI()
    {
    // 建立一個toggle並為其註冊callback
    m_MyToggle = new Toggle("Test Toggle") { name = "My Toggle" };
    m_MyToggle.RegisterValueChangedCallback((evt) => { Debug.Log("Change Event received"); });
    rootVisualElement.Add(m_MyToggle);

    // 建立一個button來更改toggle的值
    Button button01 = new Button() { text = "Toggle" };
    button01.clicked += () =>
    {
    // 這個會觸發Change Event
    m_MyToggle.value = !m_MyToggle.value;
    };
    rootVisualElement.Add(button01);

    // 建立另外一個button,但是使用SetValueWithoutNotify更改toggle的值,
    Button button02 = new Button() { text = "Toggle without notification" };
    button02.clicked += () =>
    {
    // 使用SetValueWithoutNotify()不會觸發Change Event
    m_MyToggle.SetValueWithoutNotify(!m_MyToggle.value);
    };
    rootVisualElement.Add(button02);
    }
    }
  • 觀看結果

Reference: https://docs.unity3d.com/Manual/UIE-Change-Events.html

事件捕捉(Capture events)

事件捕捉(Capture events)通知你滑鼠捕捉狀態(mouse capture state)的變化,在UI Toolkit中的事件有兩捕捉種類型:

  1. 滑鼠捕捉事件(Mouse capture events)
  2. 指標捕捉事件(Pointer capture events)

當一個element捕捉了滑鼠或指標,它是唯一接收來自點擊裝置(Pointing device,如滑鼠)事件的element,直到該裝置釋放或失去捕捉為止。例如:假設你使用滑鼠點擊了一個文字框(text box),這個文字框會捕捉這個滑鼠,此時滑鼠仍然可以移動,但是它不會觸發文字框之外的事件;只要這個文字框正在捕捉你的滑鼠,那麼它就不會觸發其他事件。當你在文字框之外按下滑鼠上的按鈕時,文字框會釋放其滑鼠捕捉。

捕捉滑鼠(Mouse capture)

滑鼠捕捉事件(Mouse capture events)是實體滑鼠或是模擬滑鼠的虛擬滑鼠上的事件。捕捉到滑鼠時,也會產生一個滑鼠指標的PointerCaptureEvent。當一個element釋放捕捉滑鼠時,會觸發相應的MouseCaptureOutEvent

注意:不會發生同時有兩個elements同時捕捉滑鼠的情況,如果另外一個Visual Element觸發了MouseCaptureEvent那麼原先捕捉滑鼠的Element就會釋滑鼠並收到一個MouseCaptureOutEvent

捕捉指標(Pointer capture)

在UI Toolkit中,指標事件優先於滑鼠事件。如果指標的類型是滑鼠的話,在捕捉到指標事件時也會觸發滑鼠事件。

Event 描述 Trickles down Bubbles up Cancellable
MouseCaptureEvent 當某個Element被滑鼠捕捉時會發送這個事件,此時target為這個Element
MouseCaptureOutEvent 當某個Element捕捉的滑鼠被釋放或是某些其他原因是放時,會發送這個事件,此時target為這個失去滑鼠捕捉的Element
PointerCaptureEvent 當某個Element捕捉指標時會發送這個事件,此時target為這個Element
PointerCaptureOutEvent 當某個Element捕捉的指標被釋放時,會發送這個事件,此時target為這個失去指標捕捉的Element
例子

以下例子為示範捕捉與釋放的行為

  1. Assets > Scripts > Editor 下建立一個 C# Script CaptureEventsTestWindow.cs

  2. 將以下程式碼複製到剛剛建立的C# Script中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.UIElements;

    public class CaptureEventsTestWindow : EditorWindow
    {
    // 這個Attributes會在 Windows -> UI Tollkit 下加入一個 Capture Events Test Window
    [MenuItem("Window/UI Toolkit/Capture Events Test Window")]
    public static void ShowExample()
    {
    var wnd = GetWindow<CaptureEventsTestWindow>();
    wnd.titleContent = new GUIContent("Capture Events Test Window");
    }

    private bool m_IsCapturing = false;

    public void CreateGUI()
    {
    for (int i = 0; i < 4; i++)
    {
    // 建立一個 Label 的Visual Element
    Label clickableLabel = new Label($"Label {i} - Click Me!");
    // 為這個Label Element註冊 MouseDownEvent
    clickableLabel.RegisterCallback<MouseDownEvent>((evt) => {
    // 這個匿名Callback會在Console中印出 Clicked on label *** 的字串
    Debug.Log($"Clicked on label '{(evt.target as Label).text}'");
    });
    rootVisualElement.Add(clickableLabel);
    }

    // 建立一個 Label 的Visual Element
    Label capturingLabel = new Label("Click here to capture mouse");
    // 為這個Label Element註冊 MouseDownEvent
    capturingLabel.RegisterCallback<MouseDownEvent>((evt) =>
    {
    if (!m_IsCapturing)
    {
    capturingLabel.text = "Click here to release mouse";
    // 捕捉滑鼠
    MouseCaptureController.CaptureMouse(capturingLabel);
    m_IsCapturing = true;
    }
    else
    {
    capturingLabel.text = "Click here to capture mouse";
    // 釋放捕捉
    MouseCaptureController.ReleaseMouse(capturingLabel);
    m_IsCapturing = false;
    }
    });
    rootVisualElement.Add(capturingLabel);

    // 註冊捕捉滑鼠事件
    rootVisualElement.RegisterCallback<MouseCaptureEvent>((evt) =>
    {
    Debug.Log("Mouse captured");
    });

    // 註冊滑鼠釋放捕捉事件
    rootVisualElement.RegisterCallback<MouseCaptureOutEvent>((evt) =>
    {
    Debug.Log("Mouse captured released");
    });
    }
    }
  3. 在Unity編輯器中找到 Window > UI Toolkit > Capture Events Test Window

  4. 點擊Test Window上面的Label來觀看結果

Reference: https://docs.unity3d.com/Manual/UIE-Change-Events.html

合成並發送事件(Synthesize and send events)

事件系統使用一個事件池(pool of events),以避免重複分配事件物件(event object)。要合成並發送自己的事件需要:

  1. 建立一個UnityEngine.Event用來持有初始化的資料
  2. 從事件池中獲取一個事件物件。
  3. 填寫事件的屬性(event properties)。
  4. 將事件封裝在using區塊中,以確保它被返回到事件池。
  5. 將事件傳遞給panel.visualTree.SendEvent()

下面的例子中會發送一個KeyDownEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void SynthesizeAndSendKeyDownEvent(IPanel panel, KeyCode code,
char character = '\0', EventModifiers modifiers = EventModifiers.None)
{
// 建立一個UnityEngine.Event用來持有初始化的資料
var evt = new Event() {
type = EventType.KeyDownEvent,
keyCode = code,
character = character,
modifiers = modifiers
};

// 將事件封裝在`using`區塊中,以確保它被返回到事件池
using (KeyDownEvent keyDownEvent = KeyDownEvent.GetPooled(evt))
{
panel.visualTree.SendEvent(keyDownEvent);
}
}

注意:

  1. 不能發送不是來自OS(operating system)的事件,
  2. 不能發送不在UnityEngine.Event types中的事件
  3. 某些事件是由UI Toolkit作為對內部狀態變化的反應而發送的,不應由外部過程發送。例如,如果您發送PointerCaptureEvent,visual elements會假定該事件的底層條件已滿足,並且不會為它們設置指針捕獲(pointer capture)。這可能會破壞visual elements的內部配置(configurations)並導致未定義的行為。

Event reference

Reference: https://docs.unity3d.com/Manual/UIE-Events-Synthesizing.html

Manipulators

Manipulators是處理使用者與UI Element交互的State machine,它被用來儲存,註冊,取消註冊event callbacks

建立與使用Manipulators

你不需要自己撰寫管理callback的class,你只需要要繼承UI Toolkit提供的manipulators class就可以使用它們來管理callback與event,步驟如下:

  1. 建立一個class,這個class封裝了針對特定使用者互動所需的事件處理邏輯,並繼承了UI Toolkit提供的manipulators class。
  2. 在這個class中,實作方法(method)來回應相關的互動,例如滑鼠點擊或拖曳。這些方法捕獲並處理必要的資訊以執行這個互動行為(behavior)。
  3. 當你完成設計這個class之後,你就可以實體化它並將其附加到目標(target) UI Element上。這個附加使這個Manipulator class可以攔截和管理指定的事件,並協調使用者互動,同時與你的UI程式碼保持清晰的分離。
  4. 在Visual Element上使用AddManipulator將建立的Manipulator加入到這個Element
  5. 在Visual Element上使用RemoveManipulator為這個Element移除指定的的Manipulator

以下是UI Toolkit提供的Manipulator class
Manipulator 繼承自 描述
Manipulator 所有manipulators的基類(Base class)
KeyboardNavigationManipulator Manipulator 將特定設備輸入事件(device-specific input event)轉換為可以使用鍵盤進行的較高階導航操作(higher-level navigation operations)
MouseManipulator Manipulator 處理滑鼠輸入,擁有一個啟用過濾器(ManipulatorActivationFilter)列表
ContextualMenuManipulator MouseManipulator 當使用者按下滑鼠右鍵或是menu鍵時,顯示內容選單(contextual menu)
PointerManipulator MouseManipulator 處理指標(pointer)輸入,擁有一個啟用過濾器(ManipulatorActivationFilter)列表
Clickable PointerManipulator 追蹤element上的滑鼠事件並辨認是否發生點擊事件,也就是說在同一個element中是否有發生指標按下放開

例子

以下範例將示範

  • 如何一個繼承PointerManipulator,來處理滑鼠輸入,
  • 並使用activators list屬性來設定可以啟用這個manipulator的條件,
    • 例如:當使用者點擊滑鼠左鍵時,啟用這個manipulator
      • 要做到這個,你只要實體化一個ManipulatorActivationFilter,將其button屬性設為MouseButton.LeftMouse並加入到activators list即可
  • 使用target屬性來存取附加這個manipulator的element,
  • Override RegisterCallbacksOnTargetUnregisterCallbacksFromTarget方法用以註冊和取消註冊event callbacks。
建立一個可以讓你拖動element的manipulator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
using UnityEngine;
using UnityEngine.UIElements;

// 繼承PointerManipulator
public class ExampleDragger : PointerManipulator
{
private Vector3 m_Start;
protected bool m_Active;
private int m_PointerId;
private Vector2 m_StartSize;

public ExampleDragger()
{
m_PointerId = -1;
// 實體化一個`ManipulatorActivationFilter`,將其button屬性設為`MouseButton.LeftMouse`並加入到`activators` list
activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse });
m_Active = false;
}

// Override RegisterCallbacksOnTarget方法用以註冊event callbacks
protected override void RegisterCallbacksOnTarget()
{
target.RegisterCallback<PointerDownEvent>(OnPointerDown);
target.RegisterCallback<PointerMoveEvent>(OnPointerMove);
target.RegisterCallback<PointerUpEvent>(OnPointerUp);
}

// Override UnregisterCallbacksFromTarget方法用以取消註冊event callbacks
protected override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<PointerDownEvent>(OnPointerDown);
target.UnregisterCallback<PointerMoveEvent>(OnPointerMove);
target.UnregisterCallback<PointerUpEvent>(OnPointerUp);
}

protected void OnPointerDown(PointerDownEvent e)
{
if (m_Active)
{
e.StopImmediatePropagation();
return;
}

if (CanStartManipulation(e))
{
m_Start = e.localPosition;
m_PointerId = e.pointerId;

m_Active = true;
target.CapturePointer(m_PointerId);
e.StopPropagation();
}
}

protected void OnPointerMove(PointerMoveEvent e)
{
if (!m_Active || !target.HasPointerCapture(m_PointerId))
return;

Vector2 diff = e.localPosition - m_Start;

target.style.top = target.layout.y + diff.y;
target.style.left = target.layout.x + diff.x;

e.StopPropagation();
}

protected void OnPointerUp(PointerUpEvent e)
{
if (!m_Active || !target.HasPointerCapture(m_PointerId) || !CanStopManipulation(e))
return;

m_Active = false;
target.ReleaseMouse();
e.StopPropagation();
}
}
建立一個可以當拖動時,改變element大小的manipulator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
using UnityEngine;
using UnityEngine.UIElements;

public class ExampleResizer : PointerManipulator
{
private Vector3 m_Start;
protected bool m_Active;
private int m_PointerId;
private Vector2 m_StartSize;
public ExampleResizer()
{
m_PointerId = -1;
activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse });
m_Active = false;
}

protected override void RegisterCallbacksOnTarget()
{
target.RegisterCallback<PointerDownEvent>(OnPointerDown);
target.RegisterCallback<PointerMoveEvent>(OnPointerMove);
target.RegisterCallback<PointerUpEvent>(OnPointerUp);
}

protected override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<PointerDownEvent>(OnPointerDown);
target.UnregisterCallback<PointerMoveEvent>(OnPointerMove);
target.UnregisterCallback<PointerUpEvent>(OnPointerUp);
}

protected void OnPointerDown(PointerDownEvent e)
{
if (m_Active)
{
e.StopImmediatePropagation();
return;
}

if (CanStartManipulation(e))
{
m_Start = e.localPosition;
m_StartSize = target.layout.size;
m_PointerId = e.pointerId;
m_Active = true;
target.CapturePointer(m_PointerId);
e.StopPropagation();
}
}

protected void OnPointerMove(PointerMoveEvent e)
{
if (!m_Active || !target.HasPointerCapture(m_PointerId))
return;

Vector2 diff = e.localPosition - m_Start;

target.style.height = m_StartSize.y + diff.y;
target.style.width = m_StartSize.x + diff.x;

e.StopPropagation();
}

protected void OnPointerUp(PointerUpEvent e)
{
if (!m_Active || !target.HasPointerCapture(m_PointerId) || !CanStopManipulation(e))
return;

m_Active = false;
target.ReleasePointer(m_PointerId);
m_PointerId = -1;
e.StopPropagation();
}
}
加入manipulator
1
2
3
4
var myElement = new VisualElement();

// 加入manipulator到這個VisualElement
myElement.AddManipulator(new ExampleDragger());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var box = new VisualElement()
{
style =
{
left = 100,
top = 100,
width = 100,
height = 100,
backgroundColor = Color.red
},
pickingMode = PickingMode.Position,
};

box.AddManipulator(new ExampleResizer());
移除manipulator
1
2
// 為這個VisualElement移除manipulator
myElement.RemoveManipulator<ExampleDragger>();