Unityのエディタコードを読んでいたらTreeViewというものの存在を知ったので軽くまとめます。
- TreeView
- 最低限の実装(パフォーマンス悪い)
- 表示する要素だけを処理する
- 補足① 選択中の項目が変わった時の処理を書く
- 補足② 行のGUIをカスタムする
- 補足③ リネームできるようにする
- 補足④ 行の色を交互に付ける
- 参考
TreeView
TreeViewを使うとヒエラルキーのようなViewを簡単に作れます。
選択した項目の背景に青くハイライトが掛かったり、折り畳みを管理してくれたり。
なんと検索まで簡単に実装できます。
最低限の実装(パフォーマンス悪い)
まずは最低限の実装をしてみます。
後述しますが、この方法は要素が多い場合にはパフォーマンスが悪くなります。
using UnityEngine; using UnityEditor.IMGUI.Controls; using UnityEditor; using System.Collections.Generic; // 親子構造を表現するためのモデルを定義しておく // これがTreeViewに渡すモデルになる public class ExampleTreeElement { public int Id { get; set; } public string Name { 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; } } } // TreeViewを表示するWindow class TreeViewExampleWindow : EditorWindow { // Stateはシリアライズする(Unity再起動しても状態を保持するため) [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 () { // Stateは生成されていたらそれを使う if (_treeViewState == null) { _treeViewState = new TreeViewState (); } // TreeViewを作成 _treeView = new ExampleTreeView(_treeViewState); // 親子関係を適当に構築したモデルを作成 // IDは任意だが被らないように 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に検索文字列を入れると勝手に表示するItemを絞ってくれる _treeView.searchString = _searchField.OnToolbarGUI (_treeView.searchString); } // TreeViewを描画 var rect = EditorGUILayout.GetControlRect(false, 200); _treeView.OnGUI(rect); } } // 抽象クラスTreeViewを継承したクラスを作る public class ExampleTreeView : TreeView { private ExampleTreeElement[] _baseElements; public ExampleTreeView(TreeViewState treeViewState) : base(treeViewState) { } public void Setup(ExampleTreeElement[] baseElements) { // モデルを入れて _baseElements = baseElements; // Reload()で更新(BuildRootが呼ばれる) Reload(); } // ルートとなるTreeViewItemを作って返す // Reloadが呼ばれるたびに呼ばれる protected override TreeViewItem BuildRoot () { // RootのItemはdepth = -1として定義する var root = new TreeViewItem { id = 0, depth = -1, displayName = "Root" }; // モデルからTreeViewItemの親子関係を構築 var elements = new List<TreeViewItem>(); foreach (var baseElement in _baseElements) { var baseItem = CreateTreeViewItem(baseElement); root.AddChild(baseItem); AddChildrenRecursive(baseElement, baseItem); } // 親子関係に基づいてDepthを自動設定するメソッド SetupDepthsFromParentsAndChildren(root); return root; } /// <summary> /// モデルとItemから再帰的に子Itemを作成・追加する /// </summary> private void AddChildrenRecursive (ExampleTreeElement model, TreeViewItem item) { foreach (var childModel in model.Children) { var childItem = CreateTreeViewItem(childModel); item.AddChild(childItem); AddChildrenRecursive(childModel, childItem); } } /// <summary> /// ExampleTreeElementからTreeViewItemを作成する /// </summary> private TreeViewItem CreateTreeViewItem(ExampleTreeElement model) { return new TreeViewItem { id = model.Id, displayName = model.Name }; } }
長いですが難しいことはしていないのでコメントを追っていただければわかると思います。
クラスとしては以下の三つを定義しています。
- ExampleTreeElement : 親子構造を表現するクラス
- TreeViewExampleWindow : TreeViewを表示するウィンドウ
- ExampleTreeView : UnityのTreeViewを継承したクラス
親子関係をExampleTreeElement
を使って作ったモデルをExampleTreeView
に渡して初期化します。
ツリー構造の管理はExampleTreeView
がやってくれるのでTreeViewExampleWindow
からTreeView.OnGUI()
を呼べばいい感じに表示されます。
ちなみに上記ではTreeView.SetupDepthsFromParentsAndChildren()
を使ってdepthを構築していますが、
親子関係を指定せずにdepthを指定してからSetupParentsAndChildrenFromDepths()
する方法もあります。
本記事ではこれには触れませんが、マニュアルのSimpleTreeView
の例を見ればわかります。
表示する要素だけを処理する
さて前述の方法では、モデルが更新されるたびにTreeView.BuildRoot()
が呼ばれます。
この結果、折りたたまれて非表示になっている要素があったとしてもそれらすべてのTreeViewItem
を生成することになります。
要素数がそこまで多くないならこの方法でOKなのですが、要素数が多い場合はあまりイケてません。
パフォーマンスのことを考えるならTreeView.BuildRows()
を使うのがよさそうです。
マニュアルにもそんな感じのことが書かれています。
For very large trees, it is not optimal to create the entire tree on every reload. In this situation, create the root and then override the BuildRows method to only create items for the current rows.
それではTreeView.BuildRows()
を使って実装してみます。変更するのはExampleTreeView
のみです。
public class ExampleTreeView : TreeView { private ExampleTreeElement[] _baseElements; public ExampleTreeView(TreeViewState treeViewState) : base(treeViewState) { } public void Setup(ExampleTreeElement[] baseElements) { _baseElements = baseElements; Reload(); } protected override TreeViewItem BuildRoot () { // BuildRootではRootだけを返す return new TreeViewItem { id = 0, depth = -1, displayName = "Root" }; } protected override IList<TreeViewItem> BuildRows(TreeViewItem root) { // 現在のRowsを取得 var rows = GetRows() ?? new List<TreeViewItem>(); rows.Clear (); foreach (var baseElement in _baseElements) { var baseItem = CreateTreeViewItem(baseElement); // Itemはrootとrowsの両方に追加していく root.AddChild (baseItem); rows.Add (baseItem); if (baseElement.Children.Count >= 1) { if (IsExpanded (baseItem.id)) { AddChildrenRecursive(baseElement, baseItem, rows); } else { // 折りたたまれている場合はダミーのTreeViewItemを作成する(そういう決まり) baseItem.children = CreateChildListForCollapsedParent(); } } } // depthを設定しなおす SetupDepthsFromParentsAndChildren(root); // rowsを返す return rows; } /// <summary> /// モデルとItemから再帰的に子Itemを作成・追加する /// </summary> 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 { // 折りたたまれている場合はダミーのTreeViewItemを作成する(そういう決まり) childItem.children = CreateChildListForCollapsedParent(); } } } } /// <summary> /// ExampleTreeElementからTreeViewItemを作成する /// </summary> private TreeViewItem CreateTreeViewItem(ExampleTreeElement model) { return new TreeViewItem { id = model.Id, displayName = model.Name }; } }
Reload時のほか、折りたたみや検索の状態が変わった時にTreeView.BuildRows()
が呼ばれ、行の情報が再構築されます。
細かい説明はコメントを読んでいただければ十分かと思います。
結果は前節と変わりませんが次の通りとなります。
正常に表示されていることがわかります。
補足① 選択中の項目が変わった時の処理を書く
選択中の項目が切り替わった時の処理はTreeView
のSelectionChanged
をオーバーライドして書きます。
/// <summary> /// 選択されているものが切り替わった時の処理 /// </summary> protected override void SelectionChanged (IList<int> selectedIds) { Debug.Log(selectedIds.Select(x => x.ToString()).Aggregate((a, b) => a + ", " + b)); }
補足② 行のGUIをカスタムする
行の見た目を変えるにはRowGUI
をオーバーライドします。
protected override void RowGUI (RowGUIArgs args) { // もしTreeViewItemを使いたければはargs.itemに入っている // アイコンを描画する var texture = EditorGUIUtility.Load("SceneAsset Icon") as Texture2D; Rect toggleRect = args.rowRect; toggleRect.x += GetContentIndent(args.item); // 描画位置はこのように取得 toggleRect.width = 16f; GUI.DrawTexture(toggleRect, texture); // テキストを描画する extraSpaceBeforeIconAndLabel = toggleRect.width + 2f; // アイコンを表示した分ラベルをの開始位置をずらす base.RowGUI(args); } // 行の高さを変えたければこのメソッドをオーバーライドする protected override float GetCustomRowHeight (int row, TreeViewItem item) { return base.GetCustomRowHeight(row, item); }
こんな感じになります。
補足③ リネームできるようにする
TreeViewItemをリネームできるようにするためにはCanRename
とRenameEnded
をオーバーライドします。
// 入力した名前をバリデーション protected override bool CanRename(TreeViewItem item) { return item.displayName.Length <= 10; } // リネームされた名前に基づいてモデルを更新する protected override void RenameEnded(RenameEndedArgs args) { if (args.acceptedRename) { // モデルを更新する処理 Reload(); } }
補足④ 行の色を交互に付ける
行に交互色を付けるにはshowAlternatingRowBackgrounds
をtrueにします。
showAlternatingRowBackgrounds = true;
参考
↑からDLできるTreeViewExamples.zip
がとても参考になります