Version: 2021.3
言語: 日本語
ListView を使ってリストへバインドする
カスタムコントロールをバインドする

ListView を使わないでリストへバインドする

Version: 2021.2

ListView を使わなくても、リストにバインドすることができます。そのためには、各要素をシリアル化されたオブジェクトの配列の項目にバインドし、配列のサイズの値を追跡します。配列のサイズは、元に戻す操作やリセットの操作など、特定の状況下で変化する可能性があります。

この例では、ListView を使わないでリストへバインドする方法を紹介します。

例の概要

この例は、TexturePreviewElements のリストを作成し、そのリストを Texture2D オブジェクトの基礎となるリストにバインドします。

この例で作成するすべてのファイルは、GitHub リポジトリ にあります。

要件

This guide is for developers familiar with the Unity Editor, UI Toolkit, and C# scripting. You are recommended to have a basic understanding of the following:

リストを含むオブジェクトを作成する

リストを含む C# クラスを作成します。このリストがバインディングのターゲットです。

  1. Unity で任意のテンプレートでプロジェクトを作成します。

  2. Project ウィンドウに、bind-to-list-without-ListView という名前のフォルダーを作成し、ファイルを保存します。

  3. TexturePackAsset.cs という名の C# スクリプトを作成し、そのコンテンツを以下に置き換えます。

    using System.Collections.Generic;
    using UnityEngine;
    
    namespace UIToolkitExamples
    {
        [CreateAssetMenu(menuName = "UIToolkitExamples/TexturePackAsset")]
        public class TexturePackAsset : ScriptableObject
        {
            public List<Texture2D> textures;
    
            public void Reset()
            {
                textures = new() { null, null, null, null };
            }
        }
    }
    

カスタムコントロールの作成とスタイル設定

2D テクスチャアセットへの参照を表すカスタムコントロールを C# で作成し、USS でスタイルを設定します。

  1. フォルダーに Editor という名前のフォルダーを作成します。
  2. Editor フォルダーに、TexturePreviewElement.cs という C# スクリプトを作成します。
  3. TexturePreviewElement.cs の内容を以下に置き換えます。
using System;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using Object = UnityEngine.Object;

namespace UIToolkitExamples
{
    public class TexturePreviewElement : BindableElement, INotifyValueChanged<Object>
    {
        public new class UxmlTraits : BindableElement.UxmlTraits { }
        public new class UxmlFactory : UxmlFactory<TexturePreviewElement, UxmlTraits> { }

        public static readonly string ussClassName = "texture-preview-element";

        Image m_Preview;
        ObjectField m_ObjectField;
        Texture2D m_Value;

        public TexturePreviewElement()
        {
            AddToClassList(ussClassName);

            // Create a preview image.
            m_Preview = new Image();
            Add(m_Preview);

            // Create an ObjectField, set its object type, and register a callback when its value changes.
            m_ObjectField = new ObjectField();
            m_ObjectField.objectType = typeof(Texture2D);
            m_ObjectField.RegisterValueChangedCallback(OnObjectFieldValueChanged);
            Add(m_ObjectField);

            styleSheets.Add(Resources.Load<StyleSheet>("texture_preview_element"));
        }
        
        void OnObjectFieldValueChanged(ChangeEvent<Object> evt)
        {
            value = evt.newValue;
        }

        public void SetValueWithoutNotify(Object newValue)
        {
            if (newValue == null || newValue is Texture2D)
            {
                // Update the preview Image and update the ObjectField.
                m_Value = newValue as Texture2D;
                m_Preview.image = m_Value;
                // Notice that this line calls the ObjectField's SetValueWithoutNotify() method instead of just setting
                // m_ObjectField.value. This is very important; you don't want m_ObjectField to send a ChangeEvent.
                m_ObjectField.SetValueWithoutNotify(m_Value);
            }
            else throw new ArgumentException($"Expected object of type {typeof(Texture2D)}");
        }

        public Object value
        {
            get => m_Value;
            // The setter is called when the user changes the value of the ObjectField, which calls
            // OnObjectFieldValueChanged(), which calls this.
            set
            {
                if (value == this.value)
                    return;

                var previous = this.value;
                SetValueWithoutNotify(value);

                using (var evt = ChangeEvent<Object>.GetPooled(previous, value))
                {
                    evt.target = this;
                    SendEvent(evt);
                }
            }
        }
    }
}
  1. Editor フォルダーに、Resources という名前のフォルダーを作成します。

  2. Resources フォルダーに texture_preview_element.uss という名の StyleSheet を作成し、そのコンテンツを以下に置き換えます。

    .texture-preview-element {
        width: 200px;
        height: 200px;
    }
    
    .texture-preview-element > .unity-image {
        flex-grow: 1;
    }
    

カスタムエディターを作成しバインディングを設定する

C# スクリプトでカスタムエディターを作成し、アセットを作成します。

UI の TexturePreviewElements の数が変わるときにテクスチャリストのサイズを変更するには、SetupList() メソッドを呼び出し、シリアル化されたリストのエントリを走査します。

TexturePreviewElement をテクスチャのリストにバインドするには、BindProperty()TexturePackAsset.textures のプロパティ名で呼び出します。

  1. Editor フォルダーに、TexturePackEditor.cs という名の C# スクリプトを作成し、そのコンテンツを以下と置き換えます。

    using UnityEditor;
    using UnityEditor.UIElements;
    using UnityEngine;
    using UnityEngine.UIElements;
    
    namespace UIToolkitExamples
    {
        [CustomEditor(typeof(TexturePackAsset))]
        public class TexturePackEditor : Editor
        {
            [SerializeField]
            VisualTreeAsset m_VisualTreeAsset;
    
            public override VisualElement CreateInspectorGUI()
            {
                var editor = m_VisualTreeAsset.CloneTree();
    
                var container = editor.Q(className: "preview-container");
    
                SetupList(container);
    
                // Watch the array size to handle the list being changed        
                var propertyForSize = serializedObject.FindProperty(nameof(TexturePackAsset.textures) + ".Array");
                propertyForSize.Next(true); // Expand to obtain array size
                editor.TrackPropertyValue(propertyForSize, prop => SetupList(container));
    
                editor.Q<Button>("add-button").RegisterCallback<ClickEvent>(OnClick);
    
                return editor;
            }
    
            void SetupList(VisualElement container)
            {
                var property = serializedObject.FindProperty(nameof(TexturePackAsset.textures) + ".Array");
    
                var endProperty = property.GetEndProperty();
    
                property.NextVisible(true); // Expand the first child.
    
                var childIndex = 0;
    
                // Iterate each property under the array, and populate the container with preview elements
                do
                {
                    // Stop if you reach the end of the array
                    if (SerializedProperty.EqualContents(property, endProperty))
                        break;
    
                    // Skip the array size property
                    if (property.propertyType == SerializedPropertyType.ArraySize)
                        continue;
    
                    TexturePreviewElement element;
    
                    // Find an existing element or create one
                    if (childIndex < container.childCount)
                    {
                        element = (TexturePreviewElement)container[childIndex];
                    }
                    else
                    {
                        element = new TexturePreviewElement();
                        container.Add(element);
                    }
    
                    element.BindProperty(property);
    
                    ++childIndex;
                }
                while (property.NextVisible(false));   // Never expand children.
    
                // Remove excess elements if the array is now smaller
                while (childIndex < container.childCount)
                {
                    container.RemoveAt(container.childCount - 1);
                }
            }
    
            void OnClick(ClickEvent evt)
            {
                var property = serializedObject.FindProperty(nameof(TexturePackAsset.textures));
                property.arraySize += 1;
                serializedObject.ApplyModifiedProperties();
            }
        }
    }
    
  2. texture_pack_editor.uxml という名の UI ドキュメントを作成し、そのコンテンツを以下に置き換えます。

    <ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xmlns="UnityEngine.UIElements" example="UIToolkitExamples" editor-extension-mode="True">
        <ui:ScrollView>
            <ui:VisualElement class="preview-container" style="flex-wrap: wrap; flex-direction: row; justify-content: space-around;" />
        </ui:ScrollView>
        <ui:Button name="add-button" text="Add" />
    </ui:UXML>
    
  3. Project ウィンドウで TexturePackEditor.cs を選択します。

  4. Inspector で texture_pack_editor.uxmlビジュアルツリーアセット にドラッグします。

バインディングのテスト

  1. メニューから、Assets > Create > UIToolkitExamples > TexturePackAsset を選択してください。これで、 New Texture Pack Asset という名前のアセットが作成されます。
  2. Project ウィンドウで New Texture Pack Asset を選択します。これにより、Inspector に 4 つの TexturePreviewElement 要素が表示されます。
  3. これらの要素に 2D イメージアセットを割り当てるか、 Add (追加) ボタンを使用して新しい要素を追加します。Inspector UI で変更を加えると、TexturePackAsset.textures オブジェクトのプロパティが変更されます。

****ヒント**: いくつかのテクスチャをインポートして、リストの異なるエントリに割り当てるには、この無料の Playground プラグイン を利用してください。

その他の参考資料

ListView を使ってリストへバインドする
カスタムコントロールをバインドする