カスタムエディター
起動時エディタースクリプト実行

ツリービュー

このページの説明は、ユーザーが IMGUI (イミディエイトモード GUI) の概念についてすでに基本的な知識をもっていることを前提としています。IMGUI とエディターウィンドウのカスタマイズについては、エディターの拡張IMGUI とエディター拡張の深層へ を参照してください。

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

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

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

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

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

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

重要なクラスとメソッド

TreeView 自体を除いたもっとも重要なクラスは TreeViewItemTreeViewState です。

TreeViewState (TreeViewState) contains state information that is changed when interacting with TreeView fields in the Editor, such as selection state, expanded state, navigation state, and scroll state. TreeViewState is the only state that is serializable. The TreeView itself is not serializable - it is reconstructed from the data that it represents when it is constructed or reloaded. Add the TreeViewState as a field in your EditorWindow-derived class to ensure that user-changed states are not lost when reloading scripts or entering Play mode (see documentation on extending the Editor for information on how to do this). For an example of a class containing a TreeViewState field, see Example 1: A simple TreeView, below.

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

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

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

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

この図はツリービューの存続期間中の BuildRootBuildRows イベントメソッドの順序と繰り返しをまとめたものです。Reload が呼び出されるたびに BuildRoot メソッドが呼び出されています。BuildRows はより頻繁に呼び出されます。なぜなら、BuildRowsReload (BuildRoot の直後) が呼び出されるときに 1 回と、TreeViewItem の展開/折りたたみのたびに呼び出されるからです。

ツリービューの初期化

ツリービューは、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 ()
    {
        // Reload が呼び出されるたびに BuildRoot が呼び出され、データから TreeViewItems を作成します。 
        // ここでは、固定の項目のセットを作成します。実際の使用の際には、
        // データモデルが TreeView に渡され、項目はモデルから作成されます。 

        // この部分は ID が独自のものであるべきだということを示しています。 
        // ルート項目の depth は必ず -1 で、その他の項目はそこからインクリメントされます。
        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"},
        };
            
        // すべての項目の TreeViewItem.children と .parent を初期化する Unity メソッド
        SetupParentsAndChildrenFromDepths (root, allItems);
            
        // ツリーのルートを返します
        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 を使用します。 
    // つまり、ウインドウを閉じない限り、 Unity を再スタートしても状態は維持されます。
    //たとえ属性が削除されても、状態はまだ、シリアライズ/非シリアライズされます。
    [SerializeField] TreeViewState m_TreeViewState;

    //TreeView はシリアライズできません。ですから、木構造のデータから再構築する必要があります。
    SimpleTreeView m_SimpleTreeView;

    void OnEnable ()
    {
        // すでにシリアライズされたビュー状態 (アセンブリのリロード後も 
        // 維持されている状態) があるかどうかを確認
        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));
    }

    // "My Window" という名のメニューをウインドウメニューに加えます。to the Window menu
    [MenuItem ("TreeView Examples/Simple Tree Window")]
    static void ShowWindow ()
    {
        // 既存の開いているウインドウを取得。もしウインドウがない場合は、新しく作成します。
        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]
//TreeElement データクラスは追加のデータを格納するために拡張されています。そのデータは、フロントエンドのツリービューで表示と編集が可能です。
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 がどのように作成されるかを表しています。完全なソースコードは TreeView Examples Project (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)
{
    // EditorGUIUtility.singleLineHeight を使ってセルの矩形を垂直方向にセンタリングします。
// これにより、セル内の制御とアイコンの配置を容易にします。
    CenterRectUsingSingleLineHeight(ref cellRect);

    switch (column)
    {

        case MyColumns.Icon1:
            
            // カスタムのテクスチャを描画
GUI.DrawTexture(cellRect, s_TestIcons[GetIcon1Index(item)], ScaleMode.ScaleToFit);
            break;

        case MyColumns.Icon2:

//カスタムのテクスチャを描画 
            GUI.DrawTexture(cellRect, s_TestIcons[GetIcon2Index(item)], ScaleMode.ScaleToFit);
            break;

        case MyColumns.Name:

            // 切り替えボタンをラベルテキストの左に作成
            Rect toggleRect = cellRect;
            toggleRect.x += GetContentIndent(item);
            toggleRect.width = kToggleWidth;
            if (toggleRect.xMax < cellRect.xMax)
                item.data.enabled = EditorGUI.Toggle(toggleRect, item.data.enabled); 

            // デフォルトのアイコンとラベル
            args.rowRect = cellRect;
            base.RowGUI(args);
            break;

        case MyColumns.Value1:

// Value 1 のスライダー制御を表示
            item.data.floatValue1 = EditorGUI.Slider(cellRect, GUIContent.none, item.data.floatValue1, 0f, 1f);
            break;

        case MyColumns.Value2:

// マテリアルの ObjectField を表示
            item.data.material = (Material)EditorGUI.ObjectField(cellRect, GUIContent.none, item.data.material, 
                                          typeof(Material), false);
            break;

        case MyColumns.Value3:

// データテキスト文字列の TextField を表示
            item.data.text = GUI.TextField(cellRect, item.data.text);
            break;
    }
}

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

Q: TreeView のサブクラスに、BuildRootRowGUI 関数があります。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: By default, the row is only selected if the mouse down is not consumed by the contents of the row. Here, your Toggle consumes the event. To fix this, use the method SelectionClick before your Toggle button is called.

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

A: はい。Unity スクリプトリファレンスの BeforeRowsGUIAfterRowsGUI を参照してください。

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

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

Q: idTreeViewItem に変換するにはどうしたら良いですか?

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

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

A: SelectionChanged メソッドをオーバーライドします。 他に DoubleClickedItemContextClickedItem) も有用なコールバックです。

カスタムエディター
起動時エディタースクリプト実行