0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity】UIToolkit~バインディング編~ MultiColumnListViewを添えて

Posted at

はじめに

この記事はGCC Advent Calendar 2024の10日目の記事です。

UIToolkitの中で分かりづらい部分も多いように感じたMultiColumnListViewを題材にUIToolkitにおけるバインディングを行う方法をご紹介いたします。

前提

理解している前提で進めさせていただくことを以下にまとめます。

  • UXMLUSSC#VisualElements名前空間を使用したエディター作成の基礎知識

想定読者様を以下にまとめます。

  • UIToolkit(UIElements)を使用した際のバインディング方法を知りたい方
  • Unity6ではないMultiColumnListViewの扱い方の一例を知りたい方

目指すもの

今回目指すものはSerializableSampleEnemyクラスをバインディングしたMultiColmunListViewです。

具体的には以下の画像のようになります。

image.png

そして、今回バインディングするSampleEnemyクラスは以下の通りです。

SampleEnemy.cs
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;
    }
}

作成手順

  1. EditorWindowList<SampleEnemy>なメンバーを定義する
  2. EditorWindowMultiColumnListViewを追加する
  3. MultiColumnListViewの設定を行う
  4. MultiColumnListViewに1のメンバーをバインディングする

以上の手順で作成していきます。

1. EditorWindowList<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を起動すると以下のような結果が表示されるかと思います。

image.png

ここまで出来たら1は完了です。

2. EditorWindowMultiColumnListViewを追加する

ここでタイトルにもある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);
        }

...

以下のような結果になるかと思います。

image.png

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);
        }
        
...

このようにすることで以下のような結果が得られます。

▼アイテム追加なし
image.png
▼アイテム追加あり
image.png

ここから紐づけることでパラメーターも表示できるようになります。

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);
        }

...

このコードから以下の結果が得られます。

image.png

パラメーターのFoldOutを開くと高さが変更され内部フィールドの設定項目が確認できます。

image.png

さいごに

ここまで、お読みいただきありがとうございます。

お疲れさまでした。

MultiColumnListViewColumnを入れ替えたり、Columnの横幅を変更したりできます。
柔軟なEditor拡張の手段の一つとしてこの機会に覚えていただけましたらと思います。

UI Toolkitについてのご意見を数人にお聞かせいただいたところ「UXMLって何?」という返答が多かったので次はUI Toolkitの初歩について書かせていただこうと考えています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?