【Unity】エディタ拡張でフォルダを折り畳んで階層表示する

投稿者: | 2023-04-15

Assetsフォルダにあるすべてのフォルダを検索して、階層表示してみました。サブディレクトリは折り畳めるようにしています。

折りたたみを表示する

折りたたみはEditorGUILayout.Foldoutメソッドで表示します。第一引数に折りたたみの状態を表すbool型のグローバル変数を渡します。ユーザーによって変更された折りたたみの状態が戻るので、同じ変数に入れます。

using UnityEditor;

public class TestFoldouts : EditorWindow
{
    bool opened;

    // エディタウィンドウを表示
    [MenuItem("Window/Test/TestFoldouts")]
    public static void ShowDialog()
    {
        EditorWindow.GetWindow<TestFoldouts>();
    }

    private void OnGUI()
    {
        // フォールドアウトを表示
        opened = EditorGUILayout.Foldout(opened, "折りたたみ");

        // 開いているとき
        if(opened)
        {
            // 1つ字下げする
            EditorGUI.indentLevel++;

            // 下の階層のラベルを表示
            EditorGUILayout.LabelField("下の階層");
        }
    }
}

値がtrueのときだけ字下げをして、下の階層のラベルなどを表示しています。

False
True

折りたたみが開いているときにさらに折りたたみを表示することで階層を深くできます。

using UnityEditor;

public class TestFoldouts : EditorWindow
{
    bool[] foldoutValues = new bool[4];

    // エディタウィンドウを表示
    [MenuItem("Window/Test/TestFoldouts")]
    public static void ShowDialog()
    {
        EditorWindow.GetWindow<TestFoldouts>();
    }

    private void OnGUI()
    {
        // フォールドアウトを表示
        foldoutValues[0] = EditorGUILayout.Foldout(foldoutValues[0], "折りたたみレベル0");

        // 開いているとき
        if(foldoutValues[0])
        {
            // 1つ字下げする
            EditorGUI.indentLevel++;

            // 下の階層の折りたたみを表示
            foldoutValues[1] = EditorGUILayout.Foldout(foldoutValues[1], "折りたたみレベル1");

            // 折りたたみが開いているとき
            if(foldoutValues[1])
            {
                // 1つ字下げする
                EditorGUI.indentLevel++;

                // さらに下の階層の折りたたみを表示
                foldoutValues[2] = EditorGUILayout.Foldout(foldoutValues[2], "折りたたみレベル2");

                // 開いているとき
                if(foldoutValues[2])
                {
                    // 1つ字下げする
                    EditorGUI.indentLevel++;

                    EditorGUILayout.LabelField("ラベル1");

                    // インデントレベルを1つ戻す
                    EditorGUI.indentLevel--;
                }

                // インデントレベルを1つ戻す
                EditorGUI.indentLevel--;
            }

            foldoutValues[3] = EditorGUILayout.Foldout(foldoutValues[3], "折りたたみレベル1");

            if (foldoutValues[3])
            {
                // 1つ字下げする
                EditorGUI.indentLevel++;

                EditorGUILayout.LabelField("ラベル2");
            }
        }
    }
}

折りたたみと同じレベルにGUIを表示する場合、インデントを戻さないと階層がずれてしまいます。

フォルダを検索して階層構造を作る

フォルダを検索して階層表示するエディタウィンドウを作ってみます。まず、フォルダのパスの情報を持つクラスを作りました。このクラスは、パスの文字列と、同じクラスの子ディレクトリのインスタンスのリストをもっています。

public class PathData
{
    // 子のパスデータのリスト
    public List<PathData> children { get; } = new List<PathData>();

    // 文字列のパス
    public string Value { get; private set; }

    // コンストラクタ
    public PathData(string value)
    {
        Value = value;
    }

    // 折りたたみが開いているかどうか
    public bool foldout;
}

また、このパスを折りたたみ表示するときに、それが開いているかどうかを表すbool値も保持しています。

サブディレクトリのパスはAssetDatabase.GetSubFoldersメソッドで取得できます。このメソッドでは一つ下のフォルダだけが取得できるようなので、再帰処理で一番下の階層のフォルダまで取得します。

    void GetSubFoldersData(PathData parent)
    {
        if (parent.Value == "") return;

        // サブディレクトリのパスをすべて取得
        var childrenValues = AssetDatabase.GetSubFolders(parent.Value);

        // なければ終了
        if (childrenValues.Length == 0) return;

        // あれば一つずつ処理
        foreach(var v in childrenValues)
        {
            // サブディレクトリのパスのデータを作る
            var child = new PathData(v);

            // 現在のパスの子リストに追加
            parent.children.Add(child);

            // さらに下の階層のフォルダのパスを取得
            GetSubFoldersData(child);
        }
    }

このメソッドでは、まず引数のディレクトリのサブディレクトリのパスの配列をAssetDatabase.GetSubFoldersメソッドで取得し、サブディレクトリがある場合、一つ一つについて、パスのデータのクラスのインスタンスを作成して、親に子供として追加します。その後、メソッド自身を呼んでさらにそのサブディレクトリのパスを取得します。

フォルダを表示する

このエディタウィンドウには「フォルダを検索」ボタンと「クリア」ボタンを表示します。フォルダを検索ボタンを押すと、上のメソッドでフォルダを検索し、パスを表すインスタンスつなげて階層構造にします。

    PathData rootPath;
    Vector2 scrollPosition;
    GUIStyle labelStyle;

    private void OnGUI()
    {
        // フォルダを検索ボタンを押す
        if (GUILayout.Button("フォルダを検索"))
        {
            // ルートのパスのデータを作る
            rootPath = new PathData("Assets");

            // 下の階層のフォルダのパスを取得
            GetSubFoldersData(rootPath);

            // ラベルフィールドのスタイルを調整
            labelStyle = new GUIStyle(GUI.skin.label);
            labelStyle.padding.left = 11;

        }

ルートのAssetsフォルダのパスを表すインスタンスを作って、上のメソッドの引数に渡すだけです。サブディレクトリを持たないフォルダのパスは折りたたみではなくラベルで表示するので、そのラベルのスタイルも調整しています。

デフォルトではプロジェクトウィンドウでの表示と違って、折りたたみの矢印と、ラベルの頭の文字が揃ってしまうので、ラベルの左側に余白を入れています。

エディタウィンドウ
プロジェクトウィンドウ

クリアボタンを押すと、ルートのパスを表す変数にnullが代入されるようにしました。

        else if(GUILayout.Button("クリア"))
        {
            rootPath = null;
        }

パスが取得済みのときは、まず、スクロールビューの中にAssetsフォルダのパスの折りたたみを表示し、それが開かれているときは、現在のインデントレベルを保存します。

        if(rootPath != null)
        {
            // スクロールビューを表示
            using (var scroll = new GUILayout.ScrollViewScope(scrollPosition))
            {
                scrollPosition = scroll.scrollPosition;

                // Assetsフォルダのフォールドアウトを表示
                rootPath.foldout =  EditorGUILayout.Foldout(rootPath.foldout, rootPath.Value);

                // Assetsフォルダの折りたたみが開いているとき
                if (rootPath.foldout)
                {
                    // 現在のインデントレベルを保存
                    int currentIndentLevel = EditorGUI.indentLevel;

                    // 下の階層のフォルダのパスを表示する
                    ShowPaths(rootPath);

                    // 最後にインデントレベルを戻す。
                    EditorGUI.indentLevel = currentIndentLevel;
                    EditorGUILayout.LabelField("スクロールビュー終わり");
                }
            }
        }
    }

保存したインデントレベルはスクロールビューの最後にインデントレベルを戻すのに使います。

            using (var scroll = new GUILayout.ScrollViewScope(scrollPosition))
            {
                // ...

                    // 最後にインデントレベルを戻す。
                    EditorGUI.indentLevel = currentIndentLevel;
                    EditorGUILayout.LabelField("スクロールビュー終わり");
                }
            }

Assetsフォルダの折りたたみが開かれているときは、Assetsフォルダより下の階層のフォルダを表示します。再帰処理を行うメソッドの引数にAssetsフォルダのパスを渡しています。

    void ShowPaths(PathData path)
    {
        // 現在のパスに子がないときは終了
        if (path.children.Count == 0) return;
        
        // インデントレベルを一つ上げる
        EditorGUI.indentLevel++;
        
        // 現在のインデントレベルを保存
        int currentIndentLevel = EditorGUI.indentLevel;
        
        // パスの子を一つずつ処理
        for (int i = 0; i < path.children.Count; i++)
        {
            // さらに子がなければラベルで表示
            if (path.children[i].children.Count == 0)
            {
                EditorGUILayout.LabelField(path.children[i].Value, labelStyle);
            }
            // さらに子があれば
            else
            {
                // 折りたたみを表示
                path.children[i].foldout = EditorGUILayout.Foldout(path.children[i].foldout, path.children[i].Value);

                // 開いているとき
                if (path.children[i].foldout)
                {
                    // さらに子のパスを表示
                    ShowPaths(path.children[i]);

                    // インデントレベルを戻す
                    EditorGUI.indentLevel = currentIndentLevel;
                }
            }
        }
    }

このメソッドは、引数のディレクトリがサブディレクトリを持つ場合、一つ字下げしてから、サブディレクトリのラベルや折りたたみを表示します。その折りたたみが開かれているときは、このメソッド自身を呼んで、さらに下のサブディレクトリを表示しています。

折りたたみを開くと同じメソッドが呼ばれて字下げされるので、開いた場合は最後にインデントレベルを戻せるように、はじめに現在のインデントレベルを保存しています。

さらに下に子がなければラベルを表示します。このときに第二引数に調整したスタイルを渡しています。

            if (path.children[i].children.Count == 0)
            {
                EditorGUILayout.LabelField(path.children[i].Value, labelStyle);
            }

子があればラベルでなく折りたたみを表示します。折りたたみの状態のbool値は、パスのデータのインスタンスのフィールドに入れています。

            else
            {
                // 折りたたみを表示
                path.children[i].foldout = EditorGUILayout.Foldout(path.children[i].foldout, path.children[i].Value);

                // 開いているとき
                if (path.children[i].foldout)
                {
                    // ...
                }
            }

エディタウィンドウを表示

静的メソッドにMenuItem属性を付けて、メインメニューから実行できるようにしています。そのメソッドでこのエディタウィンドウを表示します。

    [MenuItem("Window/Test/HierarchicalFoldouts")]
    public static void ShowDialog()
    {
        EditorWindow.GetWindow<HierarchicalFoldouts>();
    }

エディタウィンドウを表示して、「フォルダを検索」ボタンを押すと、フォルダが階層表示されます。「クリア」ボタンで消去されます。

プロジェクトウィンドウと同じものが表示されていると思います。

エディタウィンドウ
プロジェクトウィンドウ

これで、フォルダをエディタウィンドウに折り畳みで階層表示できました。

スクリプト

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

public class HierarchicalFoldouts : EditorWindow
{
    // エディタウィンドウを表示する
    [MenuItem("Window/Test/HierarchicalFoldouts")]
    public static void ShowDialog()
    {
        EditorWindow.GetWindow<HierarchicalFoldouts>();
    }

    // パスを取得する
    void GetSubFoldersData(PathData parent)
    {
        if (parent.Value == "") return;

        // サブディレクトリのパスをすべて取得
        var childrenValues = AssetDatabase.GetSubFolders(parent.Value);

        // なければ終了
        if (childrenValues.Length == 0) return;

        // あれば一つずつ処理
        foreach(var v in childrenValues)
        {
            // サブディレクトリのパスのデータを作る
            var child = new PathData(v);

            // 現在のパスの子リストに追加
            parent.children.Add(child);

            // さらに下の階層のフォルダのパスを取得
            GetSubFoldersData(child);
        }
    }

    PathData rootPath;
    Vector2 scrollPosition;
    GUIStyle labelStyle;

    private void OnGUI()
    {
        // フォルダを検索ボタンを押す
        if (GUILayout.Button("フォルダを検索"))
        {
            // ルートのパスのデータを作る
            rootPath = new PathData("Assets");

            // 下の階層のフォルダのパスを取得
            GetSubFoldersData(rootPath);

            // ラベルフィールドのスタイルを調整
            labelStyle = new GUIStyle(GUI.skin.label);
            labelStyle.padding.left = 11;

        }
        // クリアボタンを推す
        else if(GUILayout.Button("クリア"))
        {
            rootPath = null;
        }

        // パスが取得済み
        if(rootPath != null)
        {
            // スクロールビューを表示
            using (var scroll = new GUILayout.ScrollViewScope(scrollPosition))
            {
                scrollPosition = scroll.scrollPosition;

                // Assetsフォルダのフォールドアウトを表示
                rootPath.foldout =  EditorGUILayout.Foldout(rootPath.foldout, rootPath.Value);

                // Assetsフォルダの折りたたみが開いているとき
                if (rootPath.foldout)
                {
                    // 現在のインデントレベルを保存
                    int currentIndentLevel = EditorGUI.indentLevel;

                    // 下の階層のフォルダのパスを表示する
                    ShowPaths(rootPath);

                    // 最後にインデントレベルを戻す。
                    EditorGUI.indentLevel = currentIndentLevel;
                    EditorGUILayout.LabelField("スクロールビュー終わり");
                }
            }
        }
    }

    // パスを表示
    void ShowPaths(PathData path)
    {
        // 現在のパスに子がないときは終了
        if (path.children.Count == 0) return;
        
        // インデントレベルを一つ上げる
        EditorGUI.indentLevel++;
        
        // 現在のインデントレベルを保存
        int currentIndentLevel = EditorGUI.indentLevel;
        
        // パスの子を一つずつ処理
        for (int i = 0; i < path.children.Count; i++)
        {
            // さらに子がなければラベルで表示
            if (path.children[i].children.Count == 0)
            {
                EditorGUILayout.LabelField(path.children[i].Value, labelStyle);
            }
            // さらに子があれば
            else
            {
                // 折りたたみを表示
                path.children[i].foldout = EditorGUILayout.Foldout(path.children[i].foldout, path.children[i].Value);

                // 開いているとき
                if (path.children[i].foldout)
                {
                    // さらに子のパスを表示
                    ShowPaths(path.children[i]);

                    // インデントレベルを戻す
                    EditorGUI.indentLevel = currentIndentLevel;
                }
            }
        }
    }
}

// パスのデータ
public class PathData
{
    // 子のパスデータのリスト
    public List<PathData> children { get; } = new List<PathData>();

    // 文字列のパス
    public string Value { get; private set; }

    // コンストラクタ
    public PathData(string value)
    {
        Value = value;
    }

    // 折りたたみが開いているかどうか
    public bool foldout;
}

コメントを残す

メールアドレスが公開されることはありません。