Unityのエディタ拡張で使える「Tree View」が超絶便利なので実装方法を紹介したい！

この記事は [Akatsuki Games Advent Calendar 2023](https://adventar.org/calendars/8724) の21日目の記事です。

昨日はYuji Sugiyamaさんの『[難解プログラミング言語「FRACTRAN（フラクトラン）」を紐解く](https://qiita.com/ysugiyama12/items/d92bb717ff8b24d824a9)』でした。
初めて見る言語でしたが、「入力された1つの整数に分数列を適用して1つの整数の出力を得る」という操作だけで様々なことを実現できるのは面白く、難解プログラミング言語の奥深さに触れた気がします！


# はじめに
株式会社アカツキゲームスでクライアントエンジニアとして働いているyuyuです。

普段の業務ではUnityを使ったゲーム開発を行っています。ゲームに直接関係する部分以外にも、エディタ拡張やサウンド周りのコードを書くことも多いです。
今回のAdvent Calendarでは、業務で使って便利だった、Unityエディタ拡張の機能の1つ「TreeView」について紹介します。

# TreeViewとは
TreeViewは、階層構造のあるデータを表示するためのUI基盤機能の1つです。
純正機能の内部実装としては、HierarchyビューやProfilerウィンドウなどで利用されています。

▼ Hierarchyビュー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270409/b2d51bec-3e40-298f-127b-92d26c24ca56.png)

▼ Profilerウィンドウ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270409/39503c6b-0a5d-1e2c-f5cd-4a38566b9c04.png)

TreeViewには次のような嬉しい特徴があります。

* カラムの概念があり、表形式で表示できる
* 表示されている部分だけ描画処理が走るので、要素が増えても重くならない
* 別に階層構造でなくてもよい（子要素のない、ただのリストのようなものも表示できる）
* 検索バーに入力した文字列でフィルタリングする機能を備えている

# 基本的な部分の実装方法

基本となる、カラムを持たない（表形式でない）、Hierarchyビューで表示されるような単純なTreeViewを実装してみます。

## 1. 空のエディタウィンドウを作る

まずは何も中身のない、空のエディタウィンドウを作ります。
`SampleTreeViewWindow` というクラス名にしました。

```csharp:SampleTreeViewWindow.cs
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;

public class SampleTreeViewWindow : EditorWindow
{
    // メニューバーからウィンドウを開けるようにする
    [MenuItem("Window/Sample Tree View Window")]
    private static void ShowWindow()
    {
        var window = GetWindow<SampleTreeViewWindow>("Window");
        window.Show();
    }

    private void OnGUI()
    {
    }
}
```

メニューバーから開いてみると、特に何も中身のないウィンドウが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270409/9d3b5ebe-5c2a-aa81-575b-7081b7ad0245.png)

## 2. TreeViewを継承した最小のクラスを作る

TreeViewクラスは抽象クラスであり、表示するためにはこれを継承したクラスを作る必要があります。
今回は `SampleTreeView` というクラス名にします。
一旦、エラーが出ないギリギリのものを作ってみましょう。

```csharp:SampleTreeView.cs
using System.Collections.Generic;
using UnityEditor.IMGUI.Controls;

public class SampleTreeView : TreeView
{
    // コンストラクタ
    public SampleTreeView(TreeViewState state) : base(state)
    {
        Reload();
    }

    // ルート要素を構築して返す
    protected override TreeViewItem BuildRoot()
    {
        var root = new TreeViewItem { id = 0, depth = -1, displayName = "Root" };

        // 子要素は一旦空で
        root.children = new List<TreeViewItem>();

        return root;
    }
}
```

### コンストラクタ

```csharp
public SampleTreeView(TreeViewState state) : base(state)
{
    Reload();
}
```

コンストラクタは2種類あり、引数が1つのものがカラムを持たないTreeView用、2つのものがカラムを持つTreeView用です。
今回はカラムを持たないものを作成するので、引数が1つのコンストラクタを実装します。

引数に渡される `TreeViewState` はスクロール位置、要素の展開状態などを保持するクラスで、エディタウィンドウ側から渡してそのままbaseコンストラクタに渡します。

コンストラクタ内に記述した `Reload()` は、`TreeView.OnGUI()` を呼ぶ前に必ず実行する必要があるメソッドです。
`TreeView.OnGUI()` より前ならどこでも良く、コンストラクタで実行しても問題ないのでここに書きました。

:::note warn
`Reload()` を実行する前に `TreeView.OnGUI()` を呼ぶと、以下のエラーが出ます。

```
TreeView has not been properly intialized yet.
Ensure to call Reload() before using the tree view.
```
:::

### BuildRoot

```csharp
protected override TreeViewItem BuildRoot()
{
    var root = new TreeViewItem { id = 0, depth = -1, displayName = "Root" };

    // 子要素は一旦空で
    root.children = new List<TreeViewItem>();

    return root;
}
```

ルート要素を構築してreturnします。
ルート要素は **TreeView上に表示されない、仮想的な要素** です。実際に表示されるのは、ルート要素の子以下の要素になります。

TreeViewで表示する全ての要素は `TreeViewItem` 型で、以下のパラメータを持っています。
* ユニークIDを表す `id`
* 階層の深さを表す `depth`
* 表示内容を表す `displayName`
* 直接の子要素を表す `children`

ルート要素については、`id`、`displayName` は何でも良いですが、`depth` は `-1` 固定である必要があります。

:::note warn
ルート要素の `depth` の値が `-1` 以外だと以下のエラーが出ます。

```
BuildRoot should ensure the root item has a depth == -1.
The visible items start at depth == 0.
```
:::

子要素を表す `root.children` は初期状態ではnullであり、何らかのリストを代入する必要があります。ここでは一旦空のリストを代入しています。

:::note warn
`root.children` がnullのまま `TreeView.OnGUI()` を呼ぶと、以下のエラーが出ます。

```
InvalidOperationException: TreeView: 'rootItem.children == null'.
Did you forget to add children?
If you intend to only create the list of rows (not the full tree) then you need to override: BuildRows, GetAncestors and GetDescendantsThatHaveChildren.
```
:::

## 3. 表示側のコードを実装する

表示側（ウィンドウ側）のコードも実装します。

```diff_c_sharp
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;

public class SampleTreeViewWindow : EditorWindow
{
+   // TreeViewの状態を保持できるようにするための情報
+   private TreeViewState _treeViewState;
+   // TreeViewのインスタンス
+   private SampleTreeView _treeView;

    // メニューバーからウィンドウを開けるようにする
    [MenuItem("Window/Sample Tree View Window")]
    private static void ShowWindow()
    {
        var window = GetWindow<SampleTreeViewWindow>("Window");
        window.Show();
    }

    private void OnGUI()
    {
+       // 値がnullの場合は初期化
+       _treeViewState ??= new TreeViewState();
+       _treeView ??= new SampleTreeView(_treeViewState);

+       // ウィンドウ全体の領域を取得
+       var treeViewRect = EditorGUILayout.GetControlRect(false, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true));
+       // TreeViewを描画
+       _treeView.OnGUI(treeViewRect);
    }
}
```

### フィールド

```csharp
// TreeViewの状態を保持できるようにするための情報
private TreeViewState _treeViewState;
// TreeViewのインスタンス
private SampleTreeView _treeView;
```

`_treeViewState` は先述したように、TreeViewのスクロール位置、要素の展開状態などを保持するクラスです。
Serializableなクラスであり、EditorWindowのフィールドとして宣言すればウィンドウを開いている間は状態が保持されるようになります。

`_treeView` は `SampleTreeView` クラスのインスタンスです。

両方とも、初回のOnGUIの実行時に初期化されるようにしています。

### 描画処理
`EditorGUILayout.GetControlRect` でウィンドウ全体の領域を取得し、`TreeView.OnGUI()` メソッドに渡してTreeViewの描画を行っています。

この段階で、エディタウィンドウは次のような表示になります。
先ほどと全く変わりません。子要素 `root.children` が空なので、表示する要素がまだ何もないためです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270409/eb18a63f-f5b2-7bb8-b000-55f14e800cf5.png)

## 4. 子要素を追加する

子要素を追加してみます。
子要素を構築する方法は2パターン用意されていますが、この記事では両方とも紹介します。
今回構築するのは次のような階層構造になった要素です。

```
(ルート)
└ 1
  └ 1-1
└ 2
  └ 2-1
  └ 2-2
└ 3
  └ 3-1
    └ 3-1-1
```

どちらのパターンでも結果は変わらないので、作りやすい方を使えばよいです。

### パターン1：階層の深さを指定して構築する
`depth` パラメータを明示的に指定した要素のリストを用意し、その後 `SetupParentsAndChildrenFromDepths()` メソッドを実行することで、`depth` パラメータをもとに親子関係を登録する方法です。

```diff_c_sharp:SampleTreeView.cs
using System.Collections.Generic;
using UnityEditor.IMGUI.Controls;

public class SampleTreeView : TreeView
{
    // コンストラクタ
    public SampleTreeView(TreeViewState state) : base(state)
    {
        Reload();
    }

    // ルート要素を構築して返す
    protected override TreeViewItem BuildRoot()
    {
        var root = new TreeViewItem { id = 0, depth = -1, displayName = "Root" };

-       // 子要素は一旦空で
-       root.children = new List<TreeViewItem>();

+       int id = 0;
+       var items = new List<TreeViewItem>
+       {
+           new TreeViewItem { id = ++id, depth = 0, displayName = "1" },
+           new TreeViewItem { id = ++id, depth = 1, displayName = "1-1" },
+           new TreeViewItem { id = ++id, depth = 0, displayName = "2" },
+           new TreeViewItem { id = ++id, depth = 1, displayName = "2-1" },
+           new TreeViewItem { id = ++id, depth = 1, displayName = "2-2" },
+           new TreeViewItem { id = ++id, depth = 0, displayName = "3" },
+           new TreeViewItem { id = ++id, depth = 1, displayName = "3-1" },
+           new TreeViewItem { id = ++id, depth = 2, displayName = "3-1-1" },
+       };
+
+       // 親子関係を登録
+       SetupParentsAndChildrenFromDepths(root, items);

        return root;
    }
}
```

ルート直下の要素を `depth = 0` として、`depth` を明示的に指定した要素を格納したリストを用意します。
このリストには直接の子（「1」「2」「3」）以外の要素も含めています。
その後、`SetupParentsAndChildrenFromDepths()` メソッドを実行することで、ルート要素以下に要素を登録します。

ウィンドウはこのような表示になります。
![パターン1で構築した状態のウィンドウのスクリーンショット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270409/b981cab2-554b-08ed-76f5-4fbc9d54c364.png)

:::note warn
もしも `SetupParentsAndChildrenFromDepths()` メソッドを実行せず、`root.children = items;` と直接代入を行った場合、階層の深さ `depth` は設定されているものの親子関係が設定されない状態になるため、インデントは効くものの折りたたみができない状態になります。
![折りたたみができない状態のウィンドウのスクリーンショット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270409/43ee220c-94ad-8e72-3a03-b285a7744912.png)
:::

### パターン2：親子関係を指定して構築する

```diff_c_sharp:SampleTreeView.cs
using System.Collections.Generic;
using UnityEditor.IMGUI.Controls;

public class SampleTreeView : TreeView
{
    // コンストラクタ
    public SampleTreeView(TreeViewState state) : base(state)
    {
        Reload();
    }

    // ルート要素を構築して返す
    protected override TreeViewItem BuildRoot()
    {
        var root = new TreeViewItem { id = 0, depth = -1, displayName = "Root" };

-       // 子要素は一旦空で
-       root.children = new List<TreeViewItem>();

+       int id = 0;
+       root.children = new List<TreeViewItem>
+       {
+           new TreeViewItem
+           {
+               id = ++id, displayName = "1",
+               children = new List<TreeViewItem>
+               {
+                   new TreeViewItem { id = ++id, displayName = "1-1" }
+               }
+           },
+           new TreeViewItem
+           {
+               id = ++id, displayName = "2",
+               children = new List<TreeViewItem>
+               {
+                   new TreeViewItem { id = ++id, displayName = "2-1" },
+                   new TreeViewItem { id = ++id, displayName = "2-2" }
+               }
+           },
+           new TreeViewItem
+           {
+               id = ++id, displayName = "3",
+               children = new List<TreeViewItem>
+               {
+                   new TreeViewItem
+                   {
+                       id = ++id, displayName = "3-1",
+                       children = new List<TreeViewItem>
+                       {
+                           new TreeViewItem { id = ++id, displayName = "3-1-1" }
+                       }
+                   }
+               }
+           }
+       };
+
+       SetupDepthsFromParentsAndChildren(root);

        return root;
    }
}
```

`depth` は明示的に指定せず、それぞれ、`children` に **直接の** 子要素を登録したTreeViewItemを作成します。
その後、`SetupDepthsFromParentsAndChildren()` メソッドを実行することで、階層を構築します。

パターン1と同様、ウィンドウはこのような表示になります。
![パターン2で構築した状態のウィンドウのスクリーンショット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270409/b981cab2-554b-08ed-76f5-4fbc9d54c364.png)

:::note warn
もしも `SetupDepthsFromParentsAndChildren()` メソッドを実行しなかった場合、親子関係は設定されるものの階層の深さ `depth` が設定されないため、折りたたみは効くもののインデントが効かない状態になります。
![インデントが効いていない状態のウィンドウのスクリーンショット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270409/8ac99ff1-5f6a-f9eb-2743-6b8c84fa28be.png)
:::

# 発展的な実装

いくつか、発展的な実装の方法を紹介します。

## 要素の表示をカスタマイズする
サンプルとして、`displayName` に渡した文字列をTextFieldに表示し、差し替え可能にしてみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270409/e0e6cb09-c883-43bf-68bf-a8d58c22f2b7.png)

表示をカスタマイズするには、次のように `RowGUI()` メソッドをoverrideします。

```diff_c_sharp:SampleTreeView.cs
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.IMGUI.Controls;

public class SampleTreeView : TreeView
{
    // コンストラクタ
    public SampleTreeView(TreeViewState state) : base(state)
    {
        Reload();
    }

    // ルート要素を構築して返す
    protected override TreeViewItem BuildRoot()
    {
        var root = new TreeViewItem { id = 0, depth = -1, displayName = "Root" };

        int id = 0;
        var items = new List<TreeViewItem>
        {
            new TreeViewItem { id = ++id, depth = 0, displayName = "1" },
            new TreeViewItem { id = ++id, depth = 1, displayName = "1-1" },
            new TreeViewItem { id = ++id, depth = 0, displayName = "2" },
            new TreeViewItem { id = ++id, depth = 1, displayName = "2-1" },
            new TreeViewItem { id = ++id, depth = 1, displayName = "2-2" },
            new TreeViewItem { id = ++id, depth = 0, displayName = "3" },
            new TreeViewItem { id = ++id, depth = 1, displayName = "3-1" },
            new TreeViewItem { id = ++id, depth = 2, displayName = "3-1-1" },
        };

        // 親子関係を登録
        SetupParentsAndChildrenFromDepths(root, items);

        return root;
    }

+   protected override void RowGUI(RowGUIArgs args)
+   {
+       var rect = args.rowRect;
+
+       // インデントされた領域を取得する
+       rect.xMin += GetContentIndent(args.item);
+
+       // TextFieldを表示
+       args.item.displayName = EditorGUI.TextField(rect, args.item.displayName);
+   }
}
```

`RowGUI()` メソッドは各行を描画するたびに呼ばれるメソッドで、`args.item` には各列のTreeViewItemが格納されています。
`args.rowRect` は行全体のRectを返すため、インデントされた領域を取得するために左端の座標に `GetContentIndent(args.item)` を加算します。

これで、TextFieldが描画され名前を書き換えることができるようになります。

## 各要素に追加の情報を格納する

サンプルとして、各要素にfloat値を格納し、スライダーで調整できるようにしてみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270409/626571ad-e36f-7322-f5f7-c3be84433133.png)

要素に追加の情報を持たせるには、`TreeViewItem` クラスを継承したクラスを作ります。
ここでは `TreeViewItemWithFloat` としました。

```diff_c_sharp:SampleTreeView.cs
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.IMGUI.Controls;

public class SampleTreeView : TreeView
{
    // コンストラクタ
    public SampleTreeView(TreeViewState state) : base(state)
    {
        Reload();
    }

    // ルート要素を構築して返す
    protected override TreeViewItem BuildRoot()
    {
        var root = new TreeViewItem { id = 0, depth = -1, displayName = "Root" };

        int id = 0;
        var items = new List<TreeViewItem>
        {
+           new TreeViewItemWithFloat { id = ++id, depth = 0, displayName = "1", Value = 1.0f },
+           new TreeViewItemWithFloat { id = ++id, depth = 1, displayName = "1-1", Value = 1.1f },
+           new TreeViewItemWithFloat { id = ++id, depth = 0, displayName = "2", Value = 2.0f },
+           new TreeViewItemWithFloat { id = ++id, depth = 1, displayName = "2-1", Value = 2.1f },
+           new TreeViewItemWithFloat { id = ++id, depth = 1, displayName = "2-2", Value = 2.2f },
+           new TreeViewItemWithFloat { id = ++id, depth = 0, displayName = "3", Value = 3.0f },
+           new TreeViewItemWithFloat { id = ++id, depth = 1, displayName = "3-1", Value = 3.1f },
+           new TreeViewItemWithFloat { id = ++id, depth = 2, displayName = "3-1-1", Value = 3.11f },
        };

        // 親子関係を登録
        SetupParentsAndChildrenFromDepths(root, items);

        return root;
    }

+   protected override void RowGUI(RowGUIArgs args)
+   {
+       // 要素をキャスト
+       var item = (TreeViewItemWithFloat)args.item;
+
+       // インデントされた領域を取得する
+       var rect = args.rowRect;
+       rect.xMin += GetContentIndent(item);
+
+       // ラベルを表示
+       EditorGUI.LabelField(rect, item.displayName);
+
+       // スライダーを表示
+       var sliderRect = rect;
+       sliderRect.xMin = 100;
+       item.Value = EditorGUI.Slider(sliderRect, item.Value, 0.0f, 10.0f);
+   }

+   // TreeViewItemクラスを継承したクラスを宣言
+   private class TreeViewItemWithFloat : TreeViewItem
+   {
+       // 追加でfloat値を保持できるようにする
+       public float Value;
+   }
}
```

`RowGUI()` メソッドをoverrideし、渡ってくる `args.item` を作成した `TreeViewItemWithFloat` 型にキャストすることでfloat値を取得・設定することができます。
これで、各要素に追加でfloat値を格納し、スライダーを表示して変更することができました。

## 検索窓を表示し、要素をフィルタリングできるようにする

検索窓を表示し、検索窓に入力したテキストにマッチする要素だけ表示できるようにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270409/60453a5b-da1d-4b63-c80b-5e0ddc5c517b.png)

TreeView側は特に追加の実装は不要です。

```diff_c_sharp:SampleTreeView.cs
using System.Collections.Generic;
using UnityEditor.IMGUI.Controls;

public class SampleTreeView : TreeView
{
    // コンストラクタ
    public SampleTreeView(TreeViewState state) : base(state)
    {
        Reload();
    }

    // ルート要素を構築して返す
    protected override TreeViewItem BuildRoot()
    {
        var root = new TreeViewItem { id = 0, depth = -1, displayName = "Root" };

        int id = 0;
        var items = new List<TreeViewItem>
        {
            new TreeViewItem { id = ++id, depth = 0, displayName = "1" },
            new TreeViewItem { id = ++id, depth = 1, displayName = "1-1" },
            new TreeViewItem { id = ++id, depth = 0, displayName = "2" },
            new TreeViewItem { id = ++id, depth = 1, displayName = "2-1" },
            new TreeViewItem { id = ++id, depth = 1, displayName = "2-2" },
            new TreeViewItem { id = ++id, depth = 0, displayName = "3" },
            new TreeViewItem { id = ++id, depth = 1, displayName = "3-1" },
            new TreeViewItem { id = ++id, depth = 2, displayName = "3-1-1" },
        };

        // 親子関係を登録
        SetupParentsAndChildrenFromDepths(root, items);

        return root;
    }
}
```

EditorWindow側で検索窓を表示し、`TreeView.searchString` フィールドにテキストを設定することで自動的にフィルタリングが行われます。

```diff_c_sharp:SampleTreeViewWindow.cs
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;

public class SampleTreeViewWindow : EditorWindow
{
    // TreeViewの状態を保持できるようにするための情報
    private TreeViewState _treeViewState;
    // TreeViewのインスタンス
    private SampleTreeView _treeView;
+   // 検索窓のインスタンス
+   private SearchField _searchField;

    // メニューバーからウィンドウを開けるようにする
    [MenuItem("Window/Sample Tree View Window")]
    private static void ShowWindow()
    {
        var window = GetWindow<SampleTreeViewWindow>("Window");
        window.Show();
    }

    private void OnGUI()
    {
        // 値がnullの場合は初期化
        _treeViewState ??= new TreeViewState();
        _treeView ??= new SampleTreeView(_treeViewState);
+       _searchField ??= new SearchField();

+       // 検索窓の領域を取得
+       var searchRect = EditorGUILayout.GetControlRect(false, GUILayout.ExpandWidth(true), GUILayout.Height(EditorGUIUtility.singleLineHeight));
+       // 検索窓を描画
+       _treeView.searchString = _searchField.OnGUI(searchRect, _treeView.searchString);

        // ウィンドウ全体の領域を取得
        var treeViewRect = EditorGUILayout.GetControlRect(false, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true));
        // TreeViewを描画
        _treeView.OnGUI(treeViewRect);
    }
}
```

`TreeView.searchString` への代入さえできれば特別 `SearchField` クラスを利用する必要はない（TextFieldなどでもよい）のですが、`SearchField` クラスを使えば最低限の実装で虫眼鏡アイコンや×ボタンを表示することができます。

## 表形式で表示する

サンプルとして、要素名と3つのフィールドを表示できるTreeViewを作ってみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270409/05635519-732f-6478-2455-0704eceba389.png)

表形式で表示する（複数のカラムを表示する）ためには、2つの引数を持つコンストラクタを実装します。
2つ目の引数には `MultiColumnHeader` 型のインスタンスを渡します。
`MultiColumnHeader`　は `MultiColumnHeaderState` 型のインスタンスから作成でき、更に `MultiColumnHeaderState` は `MultiColumnHeaderState.Column` 型の配列から作成できます。

```diff_c_sharp:SampleTreeView.cs
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;

public class SampleTreeView : TreeView
{
    // コンストラクタ
-   public SampleTreeView(TreeViewState state) : base(state)
+   public SampleTreeView(TreeViewState state) : base(state, CreateHeader())
    {
        Reload();
    }

+   private static MultiColumnHeader CreateHeader()
+   {
+       // MultiColumnHeaderState.Column型の配列を作成
+       var columns = new[]
+       {
+           new MultiColumnHeaderState.Column
+           {
+               // カラムのヘッダに表示する要素
+               headerContent = new GUIContent("Name"),
+           },
+           new MultiColumnHeaderState.Column
+           {
+               headerContent = new GUIContent("Field1"),
+           },
+           new MultiColumnHeaderState.Column
+           {
+               headerContent = new GUIContent("Field2"),
+           },
+           new MultiColumnHeaderState.Column
+           {
+               headerContent = new GUIContent("Field3"),
+           },
+       };
+
+       // 配列から MultiColumnHeaderState を構築
+       var state = new MultiColumnHeaderState(columns);
+       // State から MultiColumnHeader を構築
+       return new MultiColumnHeader(state);
+   }

    // ルート要素を構築して返す
    protected override TreeViewItem BuildRoot()
    {
        var root = new TreeViewItem { id = 0, depth = -1, displayName = "Root" };

        int id = 0;
        var items = new List<TreeViewItem>
        {
+           new SampleTreeViewItem { id = ++id, depth = 0, displayName = "1", Field1 = "1A", Field2 = "1B", Field3 = "1C" },
+           new SampleTreeViewItem { id = ++id, depth = 1, displayName = "1-1", Field1 = "1-1A", Field2 = "1-1B", Field3 = "1-1C" },
+           new SampleTreeViewItem { id = ++id, depth = 0, displayName = "2", Field1 = "2A", Field2 = "2B", Field3 = "2C" },
+           new SampleTreeViewItem { id = ++id, depth = 1, displayName = "2-1", Field1 = "2-1A", Field2 = "2-1B", Field3 = "2-1C" },
+           new SampleTreeViewItem { id = ++id, depth = 1, displayName = "2-2", Field1 = "2-2A", Field2 = "2-2B", Field3 = "2-2C" },
+           new SampleTreeViewItem { id = ++id, depth = 0, displayName = "3", Field1 = "3A", Field2 = "3B", Field3 = "3C" },
+           new SampleTreeViewItem { id = ++id, depth = 1, displayName = "3-1", Field1 = "3-1A", Field2 = "3-1B", Field3 = "3-1C" },
+           new SampleTreeViewItem { id = ++id, depth = 2, displayName = "3-1-1", Field1 = "3-1-1A", Field2 = "3-1-1B", Field3 = "3-1-1C" },
        };

        // 親子関係を登録
        SetupParentsAndChildrenFromDepths(root, items);

        return root;
    }

+   protected override void RowGUI(RowGUIArgs args)
+   {
+       var item = (SampleTreeViewItem)args.item;
+
+       // 各列のフィールドを描画
+       for (var i = 0; i < args.GetNumVisibleColumns(); ++i)
+       {
+           var rect = args.GetCellRect(i);
+           var columnIndex = args.GetColumn(i);
+
+           switch (columnIndex)
+           {
+               case 0:
+                   // 1列目は要素名
+                   // インデントする必要がある
+                   rect.xMin += GetContentIndent(item);
+                   EditorGUI.LabelField(rect, item.displayName);
+                   break;
+               case 1:
+                   // 2列目以降はフィールド
+                   EditorGUI.LabelField(rect, item.Field1);
+                   break;
+               case 2:
+                   EditorGUI.LabelField(rect, item.Field2);
+                   break;
+               case 3:
+                   EditorGUI.LabelField(rect, item.Field3);
+                   break;
+           }
+       }
+   }

+   // 追加の情報を持たせるため、TreeViewItemを継承したクラスを作成
+   private class SampleTreeViewItem : TreeViewItem
+   {
+       public string Field1 { get; set; }
+       public string Field2 { get; set; }
+       public string Field3 { get; set; }
+   }
}
```

`args.GetNumVisibleColumns()` メソッドで、現在表示されている列の総数が取得できます。
forループのインデックスを `args.GetCellRect()` メソッドに渡すことで各セルのRectを、`args.GetColumn()`メソッドに渡すことで列のインデックスを取得できます。
列のインデックスは、コンストラクタに渡した `MultiColumnHeader` を構築するときに利用した `MultiColumnHeaderState.Column` のインデックスに対応する値です。

:::note
定義したカラムの数だけforループで回して、ループカウンタを列のインデックスとして利用していないのは、カラムのヘッダーを右クリックすることで一部の列を非表示にすることができるためです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270409/ee489b3d-18a0-1c7c-c936-02b6f1440eaa.png)
:::

あとは列のインデックスに応じて表示内容を変えればよいです。

# おわりに
複数のカラムからなるTreeViewに関してはまだまだ書けることが大量にあるため、別の機会に記事を書こうかと思います（行のソート機能、列の入れ替え機能など）。

明日のアドベントカレンダーは、Fluffy Mossさんが「xiaolingを出したい」という表明をしています。楽しみですね！

# 宣伝
自分が所属している株式会社アカツキゲームスでは、一緒に働くエンジニアを募集しています。カジュアル面談もやっていますので、お気軽にご応募ください！

https://games.aktsk.jp/recruit/