【Unity】【エディタ拡張】マルチカラムのTreeViewを作る

TreeViewをマルチカラム表示する方法です。

前提

TreeViewの基本的な使い方は下記の記事で説明しています。

light11.hatenadiary.com

本記事は、上の記事の実装を元にしています。
重複する部分は説明を省いていますので、理解しづらい部分などあれば上の記事を参照してください。

実装

まずは実装の全貌を書きます。

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

public class ExampleTreeElement
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public ExampleTreeElement Parent { get; private set; }
    private List<ExampleTreeElement> _children = new List<ExampleTreeElement>();
    public List<ExampleTreeElement> Children { get { return _children; } }

    /// <summary>
    /// 子を追加する
    /// </summary>
    public void AddChild(ExampleTreeElement child)
    {
        if (child.Parent != null) {
            child.Parent.RemoveChild(child);
        }
        Children.Add(child);
        child.Parent    = this;
    }

    /// <summary>
    /// 子を削除する
    /// </summary>
    public void RemoveChild(ExampleTreeElement child)
    {
        if (Children.Contains(child)) {
            Children.Remove(child);
            child.Parent        = null;
        }
    }
}

class TreeViewExampleWindow : EditorWindow
{
    [SerializeField]
    private TreeViewState _treeViewState;

    private ExampleTreeView _treeView;
    private SearchField _searchField;
        
    [MenuItem ("Window/Tree View Example")]
    private static void Open ()
    {
        GetWindow<TreeViewExampleWindow> (ObjectNames.NicifyVariableName(typeof(TreeViewExampleWindow).Name));
    }
        
    private void OnEnable ()
    {
        if (_treeViewState == null) {
            _treeViewState = new TreeViewState ();
        }

        var nameColumn      = new MultiColumnHeaderState.Column()
        {
            headerContent           = new GUIContent("Name"),
            headerTextAlignment     = TextAlignment.Center,
            canSort                 = false,
            width                   = 100, 
            minWidth                = 50,
            autoResize              = true,
            allowToggleVisibility   = false
        };
        var descriptionColumn       = new MultiColumnHeaderState.Column()
        {
            headerContent           = new GUIContent("Description"),
            headerTextAlignment     = TextAlignment.Center,
            canSort                 = false,
            width                   = 150, 
            minWidth                = 50,
            autoResize              = true,
            allowToggleVisibility   = false
        };
        var headerState             = new MultiColumnHeaderState(new MultiColumnHeaderState.Column[]{ nameColumn, descriptionColumn });
        var multiColumnHeader       = new MultiColumnHeader(headerState);
        // カラムヘッダーとともにTreeViewを作成
        _treeView                   = new ExampleTreeView(_treeViewState, multiColumnHeader);

        // モデルを作成
        var currentId       = 0;
        var root            = new ExampleTreeElement { Id = ++currentId, Name = "1" };
        for (int i = 0; i < 2; i++) {
            var element     = new ExampleTreeElement { Id = ++currentId, Name = "1-" + (i + 1) };
            for (int j = 0; j < 2; j++) {
                element.AddChild(new ExampleTreeElement { Id = ++currentId, Name = "1-" + (i + 1) + "-" + (j + 1) });
            }
            root.AddChild(element);
        }
        // TreeViewを初期化
        _treeView.Setup(new List<ExampleTreeElement>{root}.ToArray());
        
        // SearchFieldを初期化
        _searchField                                = new SearchField();
        _searchField.downOrUpArrowKeyPressed        += _treeView.SetFocusAndEnsureSelectedItem;
    }

    private void OnGUI ()
    {
        // 検索窓を描画
        using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) {
            GUILayout.Space (100);
            GUILayout.FlexibleSpace();
            _treeView.searchString                  = _searchField.OnToolbarGUI (_treeView.searchString);
        }

        // TreeViewを描画
        var rect    = EditorGUILayout.GetControlRect(false, 200);
        _treeView.OnGUI(rect);
    }
}

public class ExampleTreeView : TreeView
{
    class ExampleTreeViewItem : TreeViewItem
    {
        public ExampleTreeElement Data { get; set; }
    }

    private ExampleTreeElement[] _baseElements;

    // 初期化時にMultiColumnHeaderを渡す
    public ExampleTreeView(TreeViewState treeViewState, MultiColumnHeader multiColumnHeader) : base(treeViewState, multiColumnHeader) 
    {
    }

    public void Setup(ExampleTreeElement[] baseElements)
    {
        _baseElements       = baseElements;
        Reload();
    }
    
    protected override TreeViewItem BuildRoot ()
    {
        return new TreeViewItem { id = 0, depth = -1, displayName = "Root" };
    }

    protected override IList<TreeViewItem> BuildRows(TreeViewItem root)
    {
        var rows        = GetRows() ?? new List<TreeViewItem>();
        rows.Clear ();

        foreach (var baseElement in _baseElements)
        {
            var baseItem        = CreateTreeViewItem(baseElement);
            root.AddChild (baseItem);
            rows.Add (baseItem);
            if (baseElement.Children.Count >= 1) {
                if (IsExpanded (baseItem.id))
                {
                    AddChildrenRecursive(baseElement, baseItem, rows);
                }
                else
                {
                    baseItem.children   = CreateChildListForCollapsedParent();
                }
            }
        }
        SetupDepthsFromParentsAndChildren(root);

        return rows;
    }
    
    private void AddChildrenRecursive (ExampleTreeElement element, TreeViewItem item, IList<TreeViewItem> rows)
    {
        foreach (var childElement in element.Children) 
        {
            var childItem       = CreateTreeViewItem(childElement);
            item.AddChild (childItem);
            rows.Add (childItem);
            if (childElement.Children.Count >= 1) {
                if (IsExpanded (childElement.Id))
                {
                    AddChildrenRecursive(childElement, childItem, rows);
                }
                else
                {
                    childItem.children  = CreateChildListForCollapsedParent();
                }
            }
        }
    }

    private ExampleTreeViewItem CreateTreeViewItem(ExampleTreeElement model)
    {
        return new ExampleTreeViewItem { id = model.Id, displayName = model.Name, Data = model };
    }

    protected override void RowGUI (RowGUIArgs args)
    {
        var item            = (ExampleTreeViewItem)args.item;

        // 表示されているカラム毎に処理
        for (int i = 0; i < args.GetNumVisibleColumns (); ++i)
        {
            // いまのセルのRect
            var cellRect            = args.GetCellRect(i);
            var columnIndex         = args.GetColumn(i);

            // セルのRectを上下センタリングするユーティリティメソッド(不要なら使わなくていい)
            CenterRectUsingSingleLineHeight(ref cellRect);

            if (columnIndex == 0) {
                // デフォルトのセル描画
                base.RowGUI(args);
            }
            else if (columnIndex == 1) {
                // テキストフィールド(モデルを書き換える)
                item.Data.Description       = GUI.TextField(cellRect, item.Data.Description);
            }
        }
    }
}

クラス構成は前の記事のものと変わっていません。

変更点としては、まずExampleTreeViewクラスの中でExampleTreeViewItemを定義しています。
これはTreeViewItemにモデルを持たせただけのものです。

またExampleTreeViewのコンストラクタではMultiColumnHeaderを渡しています。
これはカラムのヘッダ部分を定義するもので、TreeViewExampleWindowOnEnableあたりで作成しています。

これらを実装したうえで、RowGUIを上記のようにカラムに対応した形で実装することで、
複数カラムが実現できます。

結果

Windowを表示すると次のようになります。

f:id:halya_11:20181214011706p:plain

関連

light11.hatenadiary.com

参考

Unity - Manual: TreeView

↑からDLできるTreeViewExamples.zipがとても参考になります