Version: Unity 6.0 (6000.0)
言語 : 日本語
IMGUI によるカスタムエディターの作成
Animation

IMGUI でツリービューを作成

ノート: UI Toolkit を使用して Unity エディター を拡張することを強くお勧めします。UI Toolkit は IMGUI より柔軟かつスケーラブルな最新のソリューションを提供します。

このページの説明は、ユーザーが IMGUI (Immediate Mode GUI) の概念についてすでに基本的な知識をもっていることを前提としています。IMGUI と Editor Windows のカスタマイズについては、エディター拡張 および IMGUI Unity ブログ を参照してください。

ツリービューは IMGUI 制御の 1 つであり、階層的なデータを展開、または折り畳んで表示することを可能にします。ツリービューを使用すると、高度にカスタマイズ可能なエディターウィンドウ用のリストビューと複数の列を持つテーブルを作成できます。これらは、他の IMGUI 制御やコンポーネントと一緒に使用できます。

ツリービューの API 関数について詳しくは、Unity のスクリプティング API に関するリファレンスの TreeView を参照してください。

MultiColumnHeader と SearchField を持つツリービューの例
MultiColumnHeader と SearchField を持つツリービューの例

ツリービューは 樹木データモデル ではないことに注意してください。ツリービューは任意の木構造のデータを使用して作成できます。C# の木構造や Transform 階層のような Unity ベースの木構造を使用できます。

ツリービューのレンダリングは、行と呼ばれる開いた状態の項目リストを決めることによって処理されます。各行は 1 つの TreeViewItem を表します。各 TreeViewItem には親と子の情報が含まれ、ツリービューでキーとマウスによる入力でナビゲーションを処理するために使用されます。

ツリービューには TreeViewItem という 1 つのルートがあり、非表示でエディターには表示されません。この項目は他のすべての項目のルートです。

重要なクラスとメソッド

ツリービュー自体以外で最も重要なクラスは、TreeViewItemTreeViewState です。

TreeViewState (TreeViewState) には、選択状態、展開状態、ナビゲーション状態、スクロール状態など、エディターのツリービューフィールドを操作するときに変更される状態の情報が含まれます。TreeViewState だけが シリアライズ可能 です。ツリービュー自体はシリアライズ可能ではありません。ツリービューは、構築時またはリロード時に、それを表すデータから再構築されます。スクリプトのリロード時や再生モードの開始時にユーザーによって変更された状態が失われないように、EditorWindow から派生したクラスのフィールドとして TreeViewState を追加します (この方法の詳細については、エディター拡張 に関するドキュメントを参照してください)。TreeViewState フィールドを含むクラスの例については、以下の 例 1: SimpleTreeView (基本的なツリービュー) を参照してください。

TreeViewItem (TreeViewItem) には、個々の ツリービューの項目に関するデータが含まれており、エディターでツリー構造の表示を構築するために使用されます。各 TreeViewItem は、一意の整数 ID (ツリービューのすべての項目内で一意) で構築する必要があります。ID は、ツリーの項目の選択状態、展開状態、ナビゲーション状態を調べるために使用されます。樹木が Unity オブジェクトを表す場合は、各オブジェクトの GetInstanceIDTreeViewItem の ID として使用します。この ID はスクリプトをリロードするときやエディターで再生モードに入るときに、ユーザーが変更した状態 (展開された項目など) を保持するために TreeViewState で使用されます。

すべての TreeViewItems には depth プロパティがあり、見た目のインデントを示します。詳しくは、後述の TreeView の初期化 の例を参照してください。

BuildRoot (BuildRoot) は、ツリービューを作成するために実装する必要がある TreeView クラスの単一の抽象メソッドです。このメソッドを使用して、樹木のルート項目の作成をハンドルします。これは、樹木で リロード が呼び出されるたびに呼び出されます。小さなデータセットを使用する単純な樹木の場合は、BuildRoot のルート項目の下に TreeViewItems の樹木全体を作成します。非常に大きな樹木の場合、リロードのたびに樹木全体を作成するのは最適な方法ではありません。この場合、ルートを作成してから BuildRows メソッドをオーバーライドし、現在の行の項目のみを作成します。BuildRoot の使用例については、以下の 例 1: SimpleTreeView (基本的なツリービュー) を参照してください。

BuildRows (BuildRows) は仮想メソッドで、デフォルトの実装では BuildRoot で作成された完全なツリーに基づく行リストを構築します。BuildRoot でルートだけが作成された場合は、展開した行を処理するためにこのメソッドをオーバーライドする必要があります。詳細については、下の TreeView の初期化 を参照してください。

この図は、ツリービューの生存期間中の BuildRoot および BuildRows イベントメソッドの順序と繰り返しをまとめたものです。BuildRoot メソッドは、Reload の呼び出しのたびに 1 回呼び出されることに注意してください。BuildRows は、Reload 時 (BuildRootの直後) と TreeViewItem の展開または折り畳みのたびに 1 回呼び出されるため、呼び出し頻度が高くなります。

ツリービューの初期化

ツリービューは、Reload メソッドが TreeView オブジェクトから呼び出されるたびに初期化されます。

ツリービューを設定するには、以下の 2 つの方法があります。

  1. 完全なツリーを作成 - 木構造のデータのすべての項目の TreeViewItem を作成します。これはデフォルトで、より少ないコードで設定できます。TreeView オブジェクトから BuildRoot が呼び出されると、完全なツリーが構築されます。

  2. 展開された項目だけを作成 - BuildRows をオーバーライドして、表示する行を手動でコントロールする必要があります。BuildRoot はルートである TreeViewItem を作成するためだけに使用されます。この方法は、大きなデータセットやしばしば変更されるデータに対し、もっとも良くスケールします。

小さなデータセットやあまりデータの変更がないものに対しては最初の方法を使用します。完全なツリーを作成するよりも展開された項目のみを作成するほうが時間がかからないため、大きなデータセットやしばしばデータが変更されるものに対しては 2 番目の方法を使います。

TreeViewItems を設定するには 3 つの方法があります。

  • 初めから親、子、深度を初期化して TreeViewItem を作成します。

  • 親と子を持つ TreeViewItem を作成し、次に SetupDepthsFromParentsAndChildren を使用して深度を設定します。

  • 深度情報だけで TreeViewItem を作成し、次に SetupDepthsFromParentsAndChildren を使用して親と子の参照を設定します。

以下の例のプロジェクトとソースコードは、TreeViewExamples.zip からダウンロードできます。

例 1: SimpleTreeView (基本的なツリービュー)

ツリービューを作成するには、TreeView クラスを拡張し、BuildRoot 抽象メソッドを実装します。以下の例では、基本的なツリービューを作成します。

class SimpleTreeView : TreeView
{
    public SimpleTreeView(TreeViewState treeViewState)
        : base(treeViewState)
    {
        Reload();
    }
        
    protected override TreeViewItem BuildRoot ()
    {
        // BuildRoot is called every time Reload is called to ensure that TreeViewItems 
        // are created from data. Here we create a fixed set of items. In a real world example,
        // a data model should be passed into the TreeView and the items created from the model.

        // This section illustrates that IDs should be unique. The root item is required to 
        // have a depth of -1, and the rest of the items increment from that.
        var root = new TreeViewItem {id = 0, depth = -1, displayName = "Root"};
        var allItems = new List<TreeViewItem> 
        {
            new TreeViewItem {id = 1, depth = 0, displayName = "Animals"},
            new TreeViewItem {id = 2, depth = 1, displayName = "Mammals"},
            new TreeViewItem {id = 3, depth = 2, displayName = "Tiger"},
            new TreeViewItem {id = 4, depth = 2, displayName = "Elephant"},
            new TreeViewItem {id = 5, depth = 2, displayName = "Okapi"},
            new TreeViewItem {id = 6, depth = 2, displayName = "Armadillo"},
            new TreeViewItem {id = 7, depth = 1, displayName = "Reptiles"},
            new TreeViewItem {id = 8, depth = 2, displayName = "Crocodile"},
            new TreeViewItem {id = 9, depth = 2, displayName = "Lizard"},
        };
            
        // Utility method that initializes the TreeViewItem.children and .parent for all items.
        SetupParentsAndChildrenFromDepths (root, allItems);
            
        // Return root of the tree
        return root;
    }
}

この例では、ツリービューを構築するために depth (深度) の情報が使用されています。最後に、SetupDepthsFromParentsAndChildren の呼び出しで TreeViewItem の親と子のデータを設定します。

TreeViewItem を設定するには 2 つの方法があります。親と子を直接設定する方法と、以下の例に示す通り、AddChild メソッドを使う方法です。

protected override TreeViewItem BuildRoot()
{
    var root = new TreeViewItem      { id = 0, depth = -1, displayName = "Root" };
    var animals = new TreeViewItem   { id = 1, displayName = "Animals" };
    var mammals = new TreeViewItem   { id = 2, displayName = "Mammals" };
    var tiger = new TreeViewItem     { id = 3, displayName = "Tiger" };
    var elephant = new TreeViewItem  { id = 4, displayName = "Elephant" };
    var okapi = new TreeViewItem     { id = 5, displayName = "Okapi" };
    var armadillo = new TreeViewItem { id = 6, displayName = "Armadillo" };
    var reptiles = new TreeViewItem  { id = 7, displayName = "Reptiles" };
    var croco = new TreeViewItem     { id = 8, displayName = "Crocodile" };
    var lizard = new TreeViewItem    { id = 9, displayName = "Lizard" };

    root.AddChild(animals);
    animals.AddChild(mammals);
    animals.AddChild(reptiles);
    mammals.AddChild(tiger);
    mammals.AddChild(elephant);
    mammals.AddChild(okapi);
    mammals.AddChild(armadillo);
    reptiles.AddChild(croco);
    reptiles.AddChild(lizard);

    SetupDepthsFromParentsAndChildren(root);

    return root;
}

上記の SimpleTreeView クラスの代替となる BuildRoot メソッド

以下の例は、SimpleTreeView を含む EditorWindow を示しています。ツリービューは TreeViewState インスタンスで構築されます。このとき、ツリービューの状態をどのように処理するかを決定する必要があります。Unity の次のセッションまで状態を持続させるか、あるいは、スクリプトをリロード (再生モードに入るかスクリプトを再コンパイルするかのいずれか) した後の状態のみを維持するか、です。この例では、TreeViewStateEditorWindow でシリアライズされ、ツリービューはエディターを閉じて再度開いても状態を維持します。

using System.Collections.Generic;
using UnityEngine;
using UnityEditor.IMGUI.Controls;

class SimpleTreeViewWindow : EditorWindow
{
    // SerializeField is used to ensure the view state is written to the window 
    // layout file. This means that the state survives restarting Unity as long as the window
    // is not closed. If the attribute is omitted then the state is still serialized/deserialized.
    [SerializeField] TreeViewState m_TreeViewState;

    //The TreeView is not serializable, so it should be reconstructed from the tree data.
    SimpleTreeView m_SimpleTreeView;

    void OnEnable ()
    {
        // Check whether there is already a serialized view state (state 
        // that survived assembly reloading)
        if (m_TreeViewState == null)
            m_TreeViewState = new TreeViewState ();

        m_SimpleTreeView = new SimpleTreeView(m_TreeViewState);
    }

    void OnGUI ()
    {
        m_SimpleTreeView.OnGUI(new Rect(0, 0, position.width, position.height));
    }

    // Add menu named "My Window" to the Window menu
    [MenuItem ("TreeView Examples/Simple Tree Window")]
    static void ShowWindow ()
    {
        // Get existing open window or if none, make a new one:
        var window = GetWindow<SimpleTreeViewWindow> ();
        window.titleContent = new GUIContent ("My Window");
        window.Show ();
    }
}

例 2: MultiColumnTreeView (複数の列を持つツリービュー)

この例は、MultiColumnHeader クラスで作成した複数の列を持つツリービューを示しています。

MultiColumnHeader は、標準の IMGUI 制御 (スライダーやオブジェクトフィールドなど)、列のソート、行のフィルタリングと検索を使用して、項目名の変更、複数選択、項目の並べ替え、独自の行のコンテンツをサポートします。

この例では、TreeElementTreeModel クラスを使ってデータモデルを作成します。ツリービューは、この TreeModel からデータを取ってきます。この例では TreeElement クラスと TreeModel クラスは、TreeView クラスの機能を示すために組み込まれています。これらのクラスは、TreeView Examples Project (TreeViewExamples.zip) に含まれています。例では、木構造が ScriptableObject にシリアライズされ、アセットに保存される仕組みも示しています。

[Serializable]
//The TreeElement data class is extended to hold extra data, which you can show and edit in the front-end TreeView.
internal class MyTreeElement : TreeElement
{
    public float floatValue1, floatValue2, floatValue3;
    public Material material;
    public string text = "";
    public bool enabled = true;

    public MyTreeElement (string name, int depth, int id) : base (name, depth, id)
    {
        floatValue1 = Random.value;
        floatValue2 = Random.value;
        floatValue3 = Random.value;
    }
}

以下の ScriptableObject クラスは、ツリーがシリアライズされるときにデータをアセット内に保持します。

[CreateAssetMenu (fileName = "TreeDataAsset", menuName = "Tree Asset", order = 1)]
public class MyTreeAsset : ScriptableObject
{
    [SerializeField] List<MyTreeElement> m_TreeElements = new List<MyTreeElement> ();

    internal List<MyTreeElement> treeElements
    {
        get { return m_TreeElements; }
        set { m_TreeElements = value; }
    }
}

MultiColumnTreeView クラスの構造

以下の例は、MultiColumnTreeView クラスのスニペットを示しており、複数列を持つ GUI がどのように作成されるかを表しています。完全なソースコードはツリービューサンプルプロジェクト (TreeViewExamples.zip) にあります。

public MultiColumnTreeView (TreeViewState state, 
                            MultiColumnHeader multicolumnHeader, 
                            TreeModel<MyTreeElement> model) 
                            : base (state, multicolumnHeader, model)
{
    // Custom setup
    rowHeight = 20;
    columnIndexForTreeFoldouts = 2;
    showAlternatingRowBackgrounds = true;
    showBorder = true;
    customFoldoutYOffset = (kRowHeights - EditorGUIUtility.singleLineHeight) * 0.5f; 
    extraSpaceBeforeIconAndLabel = kToggleWidth;
    multicolumnHeader.sortingChanged += OnSortingChanged;
            
    Reload();
}

上のコードサンプルの独自の変更によって、以下の調整が行われています。

  • rowHeight = 20: デフォルトの高さ (EditorGUIUtility.singleLineHeight が 16 ポイント) を 20 に変更し、GUI 制御により多くのスペースを加えています。

  • columnIndexForTreeFoldouts = 2: この例では、値が 2 に設定されているため展開用の矢印が 3 番目の列に表示されます (上の画像を参照)。“columnIndexForTreeFoldouts” はデフォルトでは 0 になっているため、この値を変更しなければ展開用の矢印は 1 番目の列に表示されます。

  • showAlternatingRowBackgrounds = true: 行の背景色を変更でき、各行の区別が明確になります。

  • showBorder = true: ツリービューの周囲に余白を加えます。他のコンテンツと区別する細い境界が表示されます。

  • customFoldoutYOffset = (kRowHeights - EditorGUIUtility.singleLineHeight) * 0.5f: 折り畳みを行内で縦方向に中央揃えにします。詳細については、後述の GUI のカスタマイズ を参照してください。

  • extraSpaceBeforeIconAndLabel = 20: ツリーのラベルの前にスペースを入れて、切り替えボタンを表示させます。

  • multicolumnHeader.sortingChanged += OnSortingChanged: イベントにメソッドを指定してヘッダーコンポーネントのソートの変更 (ヘッダーのクリック) を検出します。そうすると、ツリービューの行にソートの状態が反映されます。

GUI のカスタマイズ

デフォルトの RowGUI 処理が行われている場合、ツリービューは上の SimpleTreeView の例のようになり、折り畳みの矢印とラベルのみが表示されます。各項目に複数のデータの値を使用する場合は、これらの値を視覚化するために RowGUI メソッドをオーバーライドする必要があります。

protected override void RowGUI (RowGUIArgs args)

以下のコードサンプルは RowGUIArgs 構造体の引数構造です。

protected struct RowGUIArgs
{
    public TreeViewItem item;
    public string label;
    public Rect rowRect;
    public int row;
    public bool selected;
    public bool focused;
    public bool isRenaming;

    public int GetNumVisibleColumns ()
    public int GetColumn (int visibleColumnIndex)
    public Rect GetCellRect (int visibleColumnIndex)
}

TreeViewItem を拡張して、ユーザーデータを追加できます (これにより、TreeViewItem から派生したクラスが作成されます)。その後、RowGUI コールバックでこのユーザーデータを使用できます。以下はその例です。override void RowGUI を参照してください。この例では、入力項目を TreeViewItem<MyTreeElement> にキャストしています。

列の処理に関連するメソッドには、GetNumVisibleColumnsGetColumnGetCellRect の 3 つがあります。ツリービューが MultiColumnHeader で構築されている場合、これらを呼び出せます。それ以外の場合は、例外がスローされます。

protected override void RowGUI (RowGUIArgs args)
{
    var item = (TreeViewItem<MyTreeElement>) args.item;

    for (int i = 0; i < args.GetNumVisibleColumns (); ++i)
    {
        CellGUI(args.GetCellRect(i), item, (MyColumns)args.GetColumn(i), ref args);
    }
}
void CellGUI (Rect cellRect, TreeViewItem<MyTreeElement> item, MyColumns column, ref RowGUIArgs args)
{
    // Center the cell rect vertically using EditorGUIUtility.singleLineHeight.
// This makes it easier to place controls and icons in the cells.
    CenterRectUsingSingleLineHeight(ref cellRect);

    switch (column)
    {

        case MyColumns.Icon1:
            
            // Draw custom texture
GUI.DrawTexture(cellRect, s_TestIcons[GetIcon1Index(item)], ScaleMode.ScaleToFit);
            break;

        case MyColumns.Icon2:

//Draw custom texture 
            GUI.DrawTexture(cellRect, s_TestIcons[GetIcon2Index(item)], ScaleMode.ScaleToFit);
            break;

        case MyColumns.Name:

            // Make a toggle button to the left of the label text
            Rect toggleRect = cellRect;
            toggleRect.x += GetContentIndent(item);
            toggleRect.width = kToggleWidth;
            if (toggleRect.xMax < cellRect.xMax)
                item.data.enabled = EditorGUI.Toggle(toggleRect, item.data.enabled); 

            // Default icon and label
            args.rowRect = cellRect;
            base.RowGUI(args);
            break;

        case MyColumns.Value1:

// Show a Slider control for value 1
            item.data.floatValue1 = EditorGUI.Slider(cellRect, GUIContent.none, item.data.floatValue1, 0f, 1f);
            break;

        case MyColumns.Value2:

// Show an ObjectField for materials
            item.data.material = (Material)EditorGUI.ObjectField(cellRect, GUIContent.none, item.data.material, 
                                          typeof(Material), false);
            break;

        case MyColumns.Value3:

// Show a TextField for the data text string
            item.data.text = GUI.TextField(cellRect, item.data.text);
            break;
    }
}

ツリービューのよくある質問

Q: ツリービューのサブクラスに、BuildRoot 関数と RowGUI 関数があります。RowGUI は、ビルド関数で加えられたすべての TreeViewItem に対して呼び出されますか? それとも、スクロールビューで画面に表示される項目に対してのみ呼び出されますか?

A: RowGUI は、画面上に表示されている項目に対してのみ呼び出されます。例えば、10,000 項目あっても 、画面上に表示される 20 の項目に対してのみ RowGUI が呼び出されます。

Q: 画面に表示される行のインデックスを取得できますか?

A: はい。GetFirstAndLastVisibleRows メソッドを使用します。

Q: BuildRows にビルトインされた行のリストを取得できますか?

A: はい。メソッド GetRows を使用します。

Q: すべてのオーバーライドされた関数に対して base.Method を呼び出さなければなりませんか?

A: その関数に拡張したいデフォルトの動作が含まれている場合のみです。

Q: アイテムのリスト (樹木ではない) だけを作成する場合は、ルートを作成する必要がありますか?

A: 常にルートを作成する必要があります。手間をかけないで設定するには、ルート項目を作成して root.children = rows に設定します。

Q: 行に切り替えボタンを加えました。なぜ、クリックしてもその行に飛ばないのですか?

A: デフォルトで、マウスの押下が行のコンテンツによって使用されていない場合にのみ行が選択されます。この問題の場合、切り替えボタンがイベントを使用しています。この問題を解決するには、切り替えボタンを呼び出す前にメソッド SelectionClick を使用します。

Q: RowGUI メソッドが呼び出される前/後に使用可能なメソッドがありますか?

A: はい。BeforeRowsGUI および AfterRowsGUI に関する API ドキュメントを参照してください。

Q: API からツリービューにキーフォーカスを戻す簡単な方法はありますか?行で FloatField を選択すると、行の選択がグレーになります。どうやってそれを再び青色にできますか?

A: 青色はキーフォーカスのある行を示しています。FloatField にフォーカスがあるため、ツリービューはフォーカスを失います。ですから、これは正しい動作です。必要に応じて GUIUtility.keyboardControl = treeViewControlID を設定します。

Q: id から TreeViewItem に変換する方法を教えてください。

A: FindItem か FindRows のどちらかを使用します。

Q: ユーザーがツリービューの選択を変更する場合、どのようにコールバックを受けとりますか?

A: SelectionChanged メソッドをオーバーライドしてください (その他の役立つコールバック: DoubleClickedItemContextClickedItem)。

IMGUI によるカスタムエディターの作成
Animation