はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】UI Toolkitにて入れ子構造となっているデータをTreeViewで表現する方法(ファイルとディレクトリのような入れ子構造)

はじめに

今回はUI Toolkitにて以下の画像のような要素が入れ子になっているようなTreeViewを作成してみる記事になります。

動作させている様子

TreeViewの公式ドキュメントの説明
docs.unity3d.com

docs.unity3d.com

概要

TreeViewは木構造なデータを表示することができるVisualElementです。

A TreeView is a vertically scrollable area that links to, and displays, a list of items organized in a tree.

A TreeView is a ScrollView with additional logic to display a tree of vertically-arranged VisualElements. Each VisualElement in the tree is bound to a corresponding element in a data-source list. The data-source list can contain elements of any type.

// DeepL翻訳
TreeViewは、ツリー状に構成されたアイテムのリストにリンクして表示する、縦方向にスクロール可能な領域です。

TreeViewは、垂直方向に配置されたVisualElementのツリーを表示するためのロジックが追加されたScrollViewです。ツリー内の各VisualElementは、データソースリスト内の対応する要素にバインドされています。データ・ソース・リストには、任意のタイプの要素を含めることができます。

Unity - Scripting API: TreeView

やり方

UXMLを記述する(UI Builder利用)

まずはUXMLTreeViewを記述します。EditorフォルダにSampleTreeView.uxmlというUXMLファイルを作成し、UI BuilderによりTreeViewを作成していきます。

Projectビューで右クリックをし、Create > UI Toolkit > UI Documentを選択してUXMLファイルを生成してください。名前はSampleTreeView.uxmlにしました。

UXMLファイル作成

Projectビュー上でダブルクリックをすると、UI Builderが立ち上がってくれます。そしたら左下のLibraryStandardにあるContainers/Tree ViewをHierarchy上にドラッグ&ドロップしてみてください。

Hierarychyにtree viewを配置

正しく操作ができていればSampleTreeView.uxmlには以下の内容が記述されているはずです。

<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:TreeView />
</ui:UXML>

C#スクリプトでの記述

SampleTreeView.csファイルを作成し、以下のように記述します。

public class SampleTreeView : EditorWindow
{
    private static readonly List<Entry> Entries = new()
    {
        new Directory("Directory 1")
        {
            entries = new List<Entry>
            {
                new File("File 1"),
                new File("File 2"),
                new Directory("Directory 2")
                {
                    entries = new List<Entry>
                    {
                        new File("File3")
                    }
                }
            }
        },
        new File("File4")
    };
    
    // SampleTreeView.uxmlをインスペクターより指定する
    [SerializeField] private VisualTreeAsset uxml;
    
    // TreeViewの各要素のデータを格納しておくリスト
    // Doc: https://docs.unity3d.com/ScriptReference/UIElements.TreeViewItemData_1.html
    private readonly List<TreeViewItemData<Entry>> _rootItems = new();

    // EditorWindowを開く
    [MenuItem("Sample/TreeView")]
    private static void OpenWindow()
    {
        GetWindow<SampleTreeView>("Sample Tree View");
    }

    // イベント関数 ScriptableObject.Reset
    // Doc: https://docs.unity3d.com/ja/2023.1/ScriptReference/ScriptableObject.Reset.html
    private void Reset()
    {
        // EntriesからList<TreeViewItemData<Entry>>に変換をする
        var id = 0;
        
        foreach (var entry in Entries)
        {
            _rootItems.Add(entry.CreateTreeViewItemData(ref id));   
        }
    }
    
    private void CreateGUI()
    {
        // rootVisualElementをrootにしてVisualElementの木を構築する
        uxml.CloneTree(rootVisualElement);

        // TreeViewを検索する
        var treeView = rootVisualElement.Q<TreeView>();
        
        // TreeViewにデータを入力する
        treeView.SetRootItems(_rootItems);
        
        // TreeViewの各要素の初期化処理を設定する
        // NOTE: 自作を指定したい場合は 「[SerializeField] VisualTreeAsset」と「VisualTreeAsset.CloneTree」 などで対応する
        treeView.makeItem = () => new Label();
        
        // 初期化されたノードをデータにバインドするための設定
        treeView.bindItem = (item, index) =>
        {
            item.Q<Label>().text = treeView.GetItemDataForIndex<Entry>(index).GetName();
        };
    }

    // 以下バインドするためのデータ構造
    [Serializable]
    public abstract class Entry
    {
        public abstract string GetName();

        public abstract TreeViewItemData<Entry> CreateTreeViewItemData(ref int id);
    }

    [Serializable]
    public class Directory  : Entry
    {
        public string name;
        public List<Entry> entries;
        
        public Directory(string name)
        {
            this.name = name;
        }
        
        public override string GetName() => name;

        public override TreeViewItemData<Entry> CreateTreeViewItemData(ref int id)
        {
            var children = new List<TreeViewItemData<Entry>>(entries.Count);
            foreach (var entry in entries)
            {
                children.Add(entry.CreateTreeViewItemData(ref id));
            }
            
            return new TreeViewItemData<Entry>(id++, this, children);
        }
    }

    [Serializable]
    public class File : Entry
    {
        public string name;
        
        public File(string name)
        {
            this.name = name;
        }
        
        public override string GetName() => name;
        
        public override TreeViewItemData<Entry> CreateTreeViewItemData(ref int id)
        {
            return new TreeViewItemData<Entry>(id++, this);
        }
    }
}


TreeViewItemData<T>を経由してデータをバインドするのは最初は慣れが必要そうな気がします。結構UI ToolkitのListViewと扱い方が似ていますね。
Unity - Scripting API: TreeViewItemData<T0>


最後に[SerializeField]したフィールドをInspectorビューから設定します。ProjectビューからSampleTreeView.csを選択して、InspectorビューからSampleTreeView.uxmlを設定してあげます。

SampleTreeView.uxmlを設定する

動作確認する

メニューバーのSample > TreeViewを選択すると、以下のような画面が立ち上がるはずです。

動作させている様子