はじめに
この記事はGCC Advent Calendar 2024の10日目の記事です。
UIToolkitの中で分かりづらい部分も多いように感じたMultiColumnListViewを題材にUIToolkitにおけるバインディングを行う方法をご紹介いたします。
前提
理解している前提で進めさせていただくことを以下にまとめます。
-
UXML
・USS
・C#VisualElements名前空間
を使用したエディター作成の基礎知識
想定読者様を以下にまとめます。
-
UIToolkit(UIElements)
を使用した際のバインディング方法を知りたい方 -
Unity6ではないMultiColumnListView
の扱い方の一例を知りたい方
目指すもの
今回目指すものはSerializable
なSampleEnemy
クラスをバインディングしたMultiColmunListViewです。
具体的には以下の画像のようになります。
そして、今回バインディングするSampleEnemy
クラスは以下の通りです。
using System;
using UnityEngine;
namespace GarbageWay.Editor
{
[Serializable]
public struct EnemyParameter
{
public int HP;
public int AttackPoint;
}
[Serializable]
public class SampleEnemy
{
[SerializeField]
private string _name = "NEW_ENEMY";
[SerializeField]
private EnemyParameter _parameter;
}
}
作成手順
-
EditorWindow
にList<SampleEnemy>
なメンバーを定義する -
EditorWindow
にMultiColumnListView
を追加する -
MultiColumnListView
の設定を行う -
MultiColumnListView
に1のメンバーをバインディングする
以上の手順で作成していきます。
1. EditorWindow
にList<SampleEnemy>
なメンバーを定義する
EditorWindow
を作成するコードを書き、そこにList<SampleEnemy>
を追加します。
#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace GarbageWay.Editor
{
/// <summary>
/// MultiColumnListViewのサンプルコードです。
/// </summary>
public class MultiColumnListViewSampleWindow : EditorWindow
{
[SerializeField]
private List<SampleEnemy> _enemies = new();
[MenuItem("Window/CustomWindow/MultiColumnListViewSampleWindow")]
private static void Window()
{
var window = GetWindow<MultiColumnListViewSampleWindow>();
window.Show();
}
}
}
#endif
UnityのツールバーからWindow→CustomWindow→MultiColumnListViewSampleWindow
を起動すると以下のような結果が表示されるかと思います。
ここまで出来たら1は完了です。
2. EditorWindow
にMultiColumnListView
を追加する
ここでタイトルにもあるMultiColumnListView
が登場します。
以下のようなコードを追加することでMultiColumnListViewを追加することができます。
...
private static void Window()
{
var window = GetWindow<MultiColumnListViewSampleWindow>();
window.Show();
}
private void OnEnable()
{
// VisualElementを描画するためパネルのrootを取得
var root = rootVisualElement;
var listView = new MultiColumnListView();
{
// わかりやすいように幅をWindow幅と同様に
listView.style.width = Length.Percent(100);
// リストを表示するため表示に使用する元を設定
listView.itemsSource = _enemies;
}
// Sampleなのでビジュアルツリー的正しさは考慮しない
root.Add(listView);
}
...
以下のような結果になるかと思います。
3. MultiColumnListView
の設定を行う
ここからコード量が増えます。
ほとんどは描画設定を行っているので、要点だけに存在するコメントを参考に見ていただくと見やすいかと思います。
...
private void OnEnable()
{
// VisualElementを描画するためパネルのrootを取得
var root = rootVisualElement;
var listView = new MultiColumnListView();
{
// わかりやすいように幅をWindow幅と同様に
listView.style.width = Length.Percent(100);
// リストを表示するため表示に使用する元を設定
listView.itemsSource = _enemies;
listView.showAddRemoveFooter = true;
listView.showBorder = true;
listView.reorderable = true;
listView.reorderMode = ListViewReorderMode.Animated;
// 複数選択を可能にするために必要
listView.selectionType = SelectionType.Multiple;
// リストに所属するアイテムの高さに合わせてリストの高さを変更するために必要
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
// 表示するフィールドの列を追加するためにColumnを作成
var nameColumn = new Column()
{
// 列の名前を表示を指定
// わかりやすくするため日本語で設定
title = "敵の名前",
// リストがAddされたときに作成するビジュアル要素を指定
// 今回は名前入力のためTextFieldを作成するように指定
makeCell = () => new TextField()
{
multiline = true
},
// 他の列の中で同じ行の最も高いものに合わせる
stretchable = true
};
var parameterColumn = new Column()
{
title = "敵パラメーター",
// パラメーター入力はPropertyFieldで作成するように指定
makeCell = () => new PropertyField()
{
label = "パラメーター"
},
stretchable = true
};
// 列を追加
listView.columns.Add(nameColumn);
listView.columns.Add(parameterColumn);
}
// Sampleなのでビジュアルツリー的正しさは考慮しない
root.Add(listView);
}
...
このようにすることで以下のような結果が得られます。
ここから紐づけることでパラメーターも表示できるようになります。
4. MultiColumnListView
に1のメンバーをバインディングする
ようやく本題のバインディングです。
MultiColumnListView
は複雑なバインディングを使用する関係上、この記事で書かせていただいたバインディングを覚えればほとんどのバインディングができるようになるかと思います。
以下ソースコードです。
...
private void OnEnable()
{
// VisualElementを描画するためパネルのrootを取得
var root = rootVisualElement;
// バインディングパスを使用できるようにするため
// rootにこのEditorWindowのSerializedObjectをバインディング
var serializedObject = new SerializedObject(this);
root.Bind(serializedObject);
var listView = new MultiColumnListView();
{
// わかりやすいように幅をWindow幅と同様に
listView.style.width = Length.Percent(100);
// リストを表示するため表示に使用する元を設定
listView.itemsSource = _enemies;
listView.showAddRemoveFooter = true;
listView.showBorder = true;
listView.reorderable = true;
listView.reorderMode = ListViewReorderMode.Animated;
// 複数選択を可能にするために必要
listView.selectionType = SelectionType.Multiple;
// リストに所属するアイテムの高さに合わせてリストの高さを変更するために必要
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
// 設定されているSerializedObject(上記でrootに登録したこのEditorWindow)
// の相対的なシリアライズされているフィールド名を入力することでバインディング
// 今回は_enemiesをバインディングするためstringで変数名"_enemies"を指定
listView.bindingPath = "_enemies";
// 変更のたびにあらかじめSerializedObjectを更新しないと
// 新たな要素が反映されておらず、範囲外アクセスエラーが発生します
listView.itemsAdded += collection => serializedObject.Update();
// 表示するフィールドの列を追加するためにColumnを作成
var nameColumn = new Column()
{
// 列の名前を表示を指定
// わかりやすくするため日本語で設定
title = "敵の名前",
// リストがAddされたときに作成するビジュアル要素を指定
// 今回は名前入力のためTextFieldを作成するように指定
makeCell = () =>
{
var textField = new TextField()
{
// バインディングによりフィールド名が表示されるため消しておく
label = "",
multiline = true
};
// styleの入れ子になっているので文字の折り返しは外側で設定
textField.style.whiteSpace = WhiteSpace.Normal;
return textField;
},
// ここでビジュアル要素のバインディング
bindCell = (VisualElement elem, int index) =>
{
if (elem is TextField textField)
{
// バインディングするためにSerializedPropertyを取得
SerializedProperty serializedProperty = serializedObject
.FindProperty("_enemies") // 敵のリストを取得
.GetArrayElementAtIndex(index) // 敵のリストの特定IndexのSerializedProperty(SampleEnemy)を取得
.FindPropertyRelative("_name"); // SampleEnemyのバインディングパスに相当する"_name"を指定して名前フィールドを取得
// SerializedPropertyを使用したバインディングにはBindPropertyを使用する必要がある
textField.BindProperty(serializedProperty);
}
serializedObject.ApplyModifiedProperties(); // 変更を適用
},
// 他の列の中で同じ行の最も高いものに合わせる
stretchable = true
};
var parameterColumn = new Column()
{
title = "敵パラメーター",
// パラメーター入力はPropertyFieldで作成するように指定
makeCell = () => new PropertyField()
{
label = "パラメーター"
},
bindCell = (VisualElement elem, int index) =>
{
if (elem is PropertyField propertyField)
{
// パラメーターをバインディングするためにSerializedPropertyを取得
SerializedProperty serializedProperty = serializedObject.FindProperty("_enemies").GetArrayElementAtIndex(index).FindPropertyRelative("_parameter");
propertyField.BindProperty(serializedProperty);
}
serializedObject.ApplyModifiedProperties(); // 変更を適用
},
stretchable = true
};
// 列を追加
listView.columns.Add(nameColumn);
listView.columns.Add(parameterColumn);
}
// Sampleなのでビジュアルツリー的正しさは考慮しない
root.Add(listView);
}
...
このコードから以下の結果が得られます。
パラメーターのFoldOut
を開くと高さが変更され内部フィールドの設定項目が確認できます。
さいごに
ここまで、お読みいただきありがとうございます。
お疲れさまでした。
MultiColumnListView
はColumn
を入れ替えたり、Column
の横幅を変更したりできます。
柔軟なEditor拡張の手段の一つとしてこの機会に覚えていただけましたらと思います。
UI Toolkit
についてのご意見を数人にお聞かせいただいたところ「UXMLって何?」という返答が多かったので次はUI Toolkit
の初歩について書かせていただこうと考えています。