UnityのUI ToolkitでScrollView
とList
をバインドする方法についてまとめました。
Unity2021.3.16f1
やりたいこと
まず、ListView
を使うと、以下の記事のようにBinding Pathを使ってListとUI要素をバインドできます。
一方、ScrollView
にはListView
のようなBinding Pathが存在せず、パスによるバインドができません。
本記事ではこのScrollView
を使った場合のList
とのバインド方法についてまとめます。
上述の記事と同様、以下のScriptableObject
のカスタムInspectorを作ることを目標とします。
using System; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu] public sealed class Example : ScriptableObject { public List<Item> items = new(); public void Reset() { items = new List<Item> { new() { enabled = true, name = "Item1" }, new() { enabled = false, name = "Item2" }, new() { enabled = true, name = "Item3" } }; } [Serializable] public struct Item { public bool enabled; public string name; } }
ScrollViewを作る
まずはScrollView
を持つUXMLファイルを作ります。
UI Builderで、container
という名前のScrollView
をとadd-button
という名前のButton
を配置しておきます。
UI Builderにおける設定は下図の通りです。
UXMLは以下の通りです。
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xmlns="UnityEngine.UIElements" ue="UnityEditor.UIElements" editor-extension-mode="False"> <ui:ScrollView name="container" /> <ui:Button text="Add" display-tooltip-when-elided="true" name="add-button" /> </ui:UXML>
これをeditor_layout.uxmlとして保存しておきます。
ScrollViewの要素を作る
次にListView
の各要素となるUIのUXMLファイルを作ります。
下図のようにToggle
とTextField
を横一列に並べておきます。
UXMLは以下の通りです。
Binding Pathにはそれぞれのバインド対象の変数名であるenabled
とname
を入力しています。
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False"> <ui:VisualElement style="flex-direction: row; flex-shrink: 1;"> <ui:Toggle /> <ui:TextField picking-mode="Ignore" value="filler text" text="filler text" style="flex-grow: 1; margin-left: 8px; margin-right: 8px;" /> </ui:VisualElement> </ui:UXML>
これをitem_layout.uxmlとして保存しておきます。
CustomEditorを作る
最後にInspector表示用のスクリプトを以下のように作成します。
まずスクリプトの全貌を掲載します。
using UnityEditor; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; [CustomEditor(typeof(Example))] public sealed class ExampleEditor : Editor { [SerializeField] private VisualTreeAsset itemLayout; [SerializeField] private VisualTreeAsset editorLayout; public override VisualElement CreateInspectorGUI() { var root = editorLayout.CloneTree(); var container = root.Q<ScrollView>("container"); // リストのSerializedPropertyを監視して配列サイズが変わったらバインド処理をする var arr = serializedObject.FindProperty("items.Array"); arr.Next(true); // 配列サイズを取得するためにNextを呼ぶ root.TrackPropertyValue(arr, prop => BindList(container)); // ボタンを押したら要素を追加する root.Q<Button>("add-button").RegisterCallback<ClickEvent>(OnClick); // 初期化 BindList(container); return root; } private void BindList(VisualElement container) { var property = serializedObject.FindProperty("items.Array"); var endProperty = property.GetEndProperty(); // 最初の子を展開する property.NextVisible(true); var childIndex = 0; while (property.NextVisible(false)) { // 最後の要素の到達したら終了 if (SerializedProperty.EqualContents(property, endProperty)) break; // 配列サイズプロパティはスキップ if (property.propertyType == SerializedPropertyType.ArraySize) continue; // 現在のインデックスに既につくられた要素があれば使い回すためにそれを取得、なければ新規作成して追加 VisualElement element; if (childIndex < container.childCount) { element = container[childIndex]; } else { element = itemLayout.CloneTree(); container.Add(element); } // バインド var toggle = element.Q<Toggle>(); var textField = element.Q<TextField>(); var enabledProperty = property.FindPropertyRelative("enabled"); var nameProperty = property.FindPropertyRelative("name"); toggle.Unbind(); textField.Unbind(); toggle.BindProperty(enabledProperty); textField.BindProperty(nameProperty); childIndex++; } // 余分な要素を削除 while (childIndex < container.childCount) container.RemoveAt(container.childCount - 1); } private void OnClick(ClickEvent evt) { var property = serializedObject.FindProperty("items"); property.arraySize += 1; serializedObject.ApplyModifiedProperties(); } }
基本的に説明はコメントとして記述しています。
ポイントとしては、ScrollView
はListView
と違ってバインドする機能がないため、BindProperty
を使って手動でバインドをおこなっています。
BindProperty
の使い方については以下の記事を参照してください。
また、リストに変更が加わるたびにバインドをし直す必要があるため、リストの要素数をTrackPropertyValue
を使ってトラッキングしています。
こちらについては以下の記事で解説しています。
動作確認
Projectウィンドウで先ほどスクリプトを選択し、Inspectorから以下のように設定します。
次にCreate > Example
からScriptableObject
を作成します。
作成したらそれを選択してInspectorを表示すると、正常に表示されていることが確認できます。