10
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Unity】Editorのbuilt-inアイコンを一覧表示してあれこれする【Editor拡張】

Last updated at Posted at 2020-01-29

image.png

#TL;DR
デフォルトのUnityEditor上で使われているアイコンはEditorGUIUtility.Load(path)で取得できます。
pathにはアイコンの名称だけ入れれば動作しますが、毎回欲しいアイコンに対応する文字列を調べるのが面倒だったので、検索機能その他もろもろつきのアイコン一覧ウィンドウを作りました。
特に技術的に大層なことは何もしていませんが、せっかくなので公開。

コードは記事の最後にあります。
Unity 2020.3.10f1 で動作確認済。

#Editorで使われているアイコン?
UnityEditor上で使われているアイコンはたくさんあります。
image.png
これとか
image.png
これとか。

Editor拡張においても、分かりやすいUIにするにはやはりアイコンは必要不可欠。
でも一からアイコン素材を作るのはめんどくさいので、UnityEditorで使われているものを流用したいわけですね。

アイコンのロード

しかしこれらのbuilt-inリソースはResources.Load()AssetDatabase.LoadAsset()などではロードできません。
Resources.FindObjectsOfTypeAll(typeof(Texture2D))であらゆるテクスチャをロードしてくることはできるので、この中から見つけるという手もありますが、重いし面倒です。

まともな取得方法は以下の3つです。私が知らないだけで他にもあるかも。

  • Object EditorResources.Load(string assetPath, Type type)
    • 非推奨。
      • 現状、EditorResourcesクラスはUnityEditor.Experimental名前空間に存在します。
    • assetPathを指定することになっていますが、Resources.Load()等と同じくファイル名(拡張子抜き)だけで動きます。
  • Object EditorGUIUtility.Load(string path)
    • 多分一番オーソドックスな、built-inリソースをロードするAPIです。
    • 内部でEditorResources.Load(path, typeof(UnityEngine.Object))が呼ばれています。
    • pathを指定することになっていますが、これもファイル名(拡張子抜き)だけで動きます。
    • ジェネリック版は存在せず返り値はUnityEngine.Objectなので、Texture2Dにキャストして扱います。
  • GUIContent EditorGUIUtility.IconContent(string name)
    • 指定したファイル名のアイコンをロードして、GUIContentのimageに入れた状態で返してくれます。
    • 内部でEditorGUIUtility.Load(name) as Texture2Dが呼ばれています。
    • textも一緒に指定するオーバーロードがあります。アイコンつきラベルを描画するときに便利。

アイコンのファイル名とは……?

で、どれを使うにしても問題なのが、引数に渡すアイコン名が分からないことです。
"unity icon list"とかで検索するといろいろ出てきます(これとか)が、情報が古いですね。
UIが大きく変わったUnity2019.3の正式リリースが昨日(記事公開時点)なのでそれはそうなんですが。

というわけで、アイコンとそのファイル名の対応表を表示するEditorWindowを自作していきます。

作成過程

アイコン画像を全て取得する

全取得はFindObjectsOfTypeAll()からのフィルタリングでよいでしょう。

//using System.Linq;

Texture2D[] icons = Resources.FindObjectsOfTypeAll(typeof(Texture2D))
    .Where(x => AssetDatabase.GetAssetPath(x) == "Library/unity editor resources") //アイコンのAssetPathは全てこれ
    .Select(x => x.name)    //同一名で複数のアイコンが存在する場合があるので(Proスキン関連?)
    .Distinct()             //重複を除去
    .OrderBy(x => x)        //ソートしておく
    .Select(x => EditorGUIUtility.Load(x) as Texture2D) //ロードしてみる
    .Where(x => x)          //FontTextureなど、ロードできないものがたまにあるので除外
    .ToArray();

これをOnEnabled()内で呼べばよさそうです。

フィルタ機能をつける

アイコンはかなりの量があるので、入力した文字列を含むアイコンのみ表示されるような機能が欲しいですね。
適当にstring filterをユーザー入力から取得してContainsで比較するだけなので簡単です。

foreach(var icon in icons)
{
    if (!string.IsNullOrEmpty(filter) && !icon.name.ToLower().Contains(filter.ToLower())) 
        continue;

    //表示処理
}

これだけではつまらないので入力フィールドに凝っていきます。
UnityEditorの各所で使われているフィルター入力用フィールド、公開されていないんですよね。
というわけで、コガネブログさんの実装を参考に自作。

    public static string FilterField(string filter, System.Action repaintCallback, string controlName = "__FilterField__")
    {
        var evt = Event.current;
        using (new EditorGUILayout.HorizontalScope())
        {
            //入力中にEnterキーでフォーカスを外す
            if (GUI.GetNameOfFocusedControl() == controlName && evt.type == EventType.KeyDown && evt.keyCode == KeyCode.Return)
            {
                EditorGUI.FocusTextInControl("");
                repaintCallback?.Invoke();
            }

            //入力欄
            GUI.SetNextControlName(controlName);
            filter = GUILayout.TextField(filter, "SearchTextField");
            var lastrect = GUILayoutUtility.GetLastRect();

            //入力欄以外でクリックされたらフォーカスを外す
            if (evt.type == EventType.MouseDown && evt.button == 0 && !lastrect.Contains(evt.mousePosition))
            {
                EditorGUI.FocusTextInControl("");
                repaintCallback?.Invoke();
            }

            //クリアボタン
            using (new EditorGUI.DisabledGroupScope(string.IsNullOrEmpty(filter)))
            {
                if (GUILayout.Button("Clear", "SearchCancelButton"))
                {
                    filter = "";
                }
            }
        }
        return filter;
    }

これを

filter = FilterField(filter, Repaint);

こうすると
image.png
こうなります。

RepaintをCallbackとして受け取っているのは汎用化のためなので、
ここでしか使わないならstaticを外してそのままRepaint();してもいいかも。

コピペ機能をつける

ファイル名が分かってもそれを見ながら手打ちするのは面倒です。
コピペできるようにしましょう。

EditorGUIUtility.systemCopyBuffer = "コピーしたいテキスト";

これでクリップボードにコピーされます。簡単。

背景色変更機能をつける

背景が明るいとほとんど見えないアイコンがそこそこあります。
暗い背景と切り替えられるようにしてもいいですが、せっかくなので自由に指定できるようにしました。
EditorGUI.ColorField()で取ってるだけなので詳細省略。

PNG出力機能をつける

いつ内部実装やファイル名が変わるか分からないEditorGUIUtility.Loadを使わずに、
エクスポートしたファイルを直接扱いたい人も多いかもしれません。

アイコン画像はisReadable(Inspector上のRead/Write Enabled)がfalseのため、直接PNGにエンコードできません。
が、コピーは可能なので、コピーしてから出力します。
(こちらの実装を参考にしました)

var path = EditorUtility.SaveFilePanel($"Export the icon [{icon.name}]", Application.dataPath, icon.name, "png");
if (!string.IsNullOrEmpty(path))
{
    var output = new Texture2D(icon.width, icon.height, icon.format, icon.mipmapCount > 1);
    Graphics.CopyTexture(icon, output);
    System.IO.File.WriteAllBytes(path, output.EncodeToPNG());
    if (path.StartsWith(Application.dataPath))
    {
        AssetDatabase.Refresh();
        var assetPath = path.Replace(Application.dataPath, "Assets");
        var importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
        importer.alphaIsTransparency = true;
        AssetDatabase.ImportAsset(assetPath);
    }
}

#できたもの
あとはコツコツチマチマRectを並べていくだけなのでコード全文です。
メニューバー>Window>IconViewerで開きます。

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

public class IconViewerWindow : EditorWindow
{
    //読み込んだアイコンのキャッシュ
    Texture2D[] icons;

    //アイコン幅の最大値(動的計算)
    float maxIconWidth = -1;
    
    //アイコン名ラベル幅の最大値(動的計算)
    float maxLabelWidth = -1;
    
    //スクロールビューの位置
    Vector2 scrollpos;
    
    //フィルタ
    string filter;
    
    //各アイコンごとのリスト内Foldout状況
    Dictionary<string, bool> foldouts = new Dictionary<string, bool>();

    [MenuItem("Window/IconViewer")]
    public static void Open()
    {
        GetWindow<IconViewerWindow>(true, "IconViewer");
    }

    private void OnEnable()
    {
        //built-inアイコンを全て読み込み
        icons = Resources.FindObjectsOfTypeAll(typeof(Texture2D))
            .Where(x => AssetDatabase.GetAssetPath(x) == "Library/unity editor resources") //アイコンのAssetPathを取得すると全てこれ
            .Select(x => x.name)    //同一名で複数のアイコンが存在する場合があるので
            .Distinct()             //重複を除去
            .OrderBy(x => x)
            .Select(x => EditorGUIUtility.Load(x) as Texture2D)
            .Where(x => x)          //FontTextureなど、ロードできないものを除外
            .ToArray();

        //各項目のfoldoutを初期化
        foreach (var icon in icons)
        {
            foldouts[icon.name] = false;
        }
    }

    //GUIStyles
    GUIStyle blackLabel;
    GUIStyle whiteLabel;

    //Colors
    Color headerColor = new Color(0.11765f, 0.11765f, 0.11765f);
    Color elementBGColor0 = new Color(0.9f, 0.9f, 0.9f);
    Color elementBGColor1 = new Color(0.95f, 0.95f, 0.95f);
    Color elementBGColorConfig = new Color(0, 0, 0, 0.8f);
    Color headerSepalaterColor = new Color(1, 1, 1, 0.6f);
    Color elementSepalaterColor = new Color(0, 0, 0, 0.2f);
    Color foldedRectColor = new Color(0f, 0.2f, 0.2f);

    private void OnGUI()
    {
        var evt = Event.current;

        //GUIStyleのキャッシュ
        if(evt.type == EventType.Repaint)
        {
            if (blackLabel == null)
            {
                blackLabel = new GUIStyle(GUI.skin.label) { richText = true, alignment = TextAnchor.MiddleLeft, padding = new RectOffset(5, 0, 0, 0) };
            }
            if (whiteLabel == null)
            {
                whiteLabel = new GUIStyle(blackLabel) { fontSize = 14 };
                whiteLabel.normal.textColor = Color.white;
            }
        }

        //ラベル描画領域に必要な幅を計算
        if (maxLabelWidth <= 0)
        {
            maxLabelWidth = icons.Max(x => GUI.skin.label.CalcSize(new GUIContent(x.name)).x);
        }
        var labelWidth = maxLabelWidth + 10;

        //アイコン描画領域に必要な幅を計算
        RectOffset iconPadding = new RectOffset(10, 10, 4, 4);
        if (maxIconWidth <= 0)
        {
            maxIconWidth = icons.Max(x => x.width);
        }
        var iconRectWidth = maxIconWidth + iconPadding.left + iconPadding.right;

        //フィルタ
        filter = FilterField(filter, Repaint);

        //ヘッダ
        var headerHeight = 24;
        var headerRect = EditorGUILayout.GetControlRect(GUILayout.Height(headerHeight));
        var labelHeaderRect = new Rect(headerRect) { x = headerRect.x, width = labelWidth };
        var headerSepalaterRect = new Rect(labelHeaderRect.xMax, headerRect.y, 1, headerRect.height);
        var iconHeaderRect = new Rect(headerRect) { x = labelHeaderRect.xMax, width = iconRectWidth };
        var colorPickerRect = new Rect(iconHeaderRect.xMax + iconRectWidth - 100, headerRect.y + (headerHeight - EditorGUIUtility.singleLineHeight)/2, 100, EditorGUIUtility.singleLineHeight);
        var colorPickerLabelRect = new Rect(colorPickerRect) { x = colorPickerRect.x - 65 };
        EditorGUI.DrawRect(headerRect, headerColor);
        GUI.Label(labelHeaderRect, "Name", whiteLabel);
        EditorGUI.DrawRect(headerSepalaterRect, headerSepalaterColor);
        GUI.Label(iconHeaderRect, "Icon", whiteLabel);
        
        elementBGColorConfig = EditorGUI.ColorField(colorPickerRect, elementBGColorConfig);
        GUI.Label(colorPickerLabelRect, "BGColor", whiteLabel);

        //リスト
        var elementMinHeight = 20;
        var foldedRectLineHeight = 20;
        var foldedRectLinePaddingHeight = 2;
        var copyRectHeight = (foldedRectLineHeight + foldedRectLinePaddingHeight * 2) * 2;
        var copyRectPadding = 6;
        scrollpos = EditorGUILayout.BeginScrollView(scrollpos);
        int i = 0;
        foreach (var icon in icons)
        {
            //フィルタ内容に応じてスキップ
            if (!string.IsNullOrEmpty(filter) && !icon.name.ToLower().Contains(filter.ToLower())) continue;

            var iconRectHeight = Mathf.Max(elementMinHeight, icon.height + iconPadding.top + iconPadding.bottom);
            var elementRect = EditorGUILayout.GetControlRect(GUILayout.Width(labelWidth + iconRectWidth * 2), GUILayout.Height(iconRectHeight + (foldouts[icon.name] ? copyRectHeight+copyRectPadding : 0)));
            var elementMainRect = new Rect(elementRect) { height = iconRectHeight };
            var labelRect = new Rect(elementRect) { width = labelWidth, height = iconRectHeight };
            var sepalaterRect = new Rect(labelRect.xMax, elementRect.y, 1, elementRect.height);
            var iconRect = new Rect(labelRect.xMax + iconPadding.left, elementRect.y + iconPadding.top, icon.width, icon.height);
            var darkRect = new Rect(labelRect.xMax + iconRectWidth, elementRect.y, iconRectWidth, elementRect.height);
            var darkIconRect = new Rect(darkRect.x + iconPadding.left, elementRect.y + iconPadding.top, icon.width, icon.height);

            if (foldouts[icon.name])
            {
                EditorGUI.DrawRect(new Rect(elementRect.position - Vector2.one, elementRect.size + Vector2.one * 2), new Color(0, 0.7f, 0.7f));
            }
            EditorGUI.DrawRect(elementRect, i % 2 == 0 ? elementBGColor0 : elementBGColor1);
            EditorGUI.LabelField(labelRect, icon.name, blackLabel);
            EditorGUI.DrawRect(sepalaterRect, elementSepalaterColor);
            EditorGUI.DrawRect(darkRect, elementBGColorConfig);
            GUI.DrawTexture(iconRect, icon);
            GUI.DrawTexture(darkIconRect, icon);

            //コピー用フォーム・詳細情報
            if (foldouts[icon.name])
            {
                var foldedRect = new Rect(elementRect.x + copyRectPadding, elementRect.y + iconRectHeight, elementRect.width - copyRectPadding * 2, copyRectHeight);
                var copyButton1Rect = new Rect(foldedRect.x + 2, foldedRect.y + foldedRectLinePaddingHeight, 60, foldedRectLineHeight);
                var copyContent1Rect = new Rect(foldedRect.x + 70, foldedRect.y + foldedRectLinePaddingHeight, foldedRect.width - 80, foldedRectLineHeight);
                var copyButton2Rect = new Rect(foldedRect.x + 2, foldedRect.y + foldedRectLineHeight + foldedRectLinePaddingHeight * 3, 60, foldedRectLineHeight);
                var copyContent2Rect = new Rect(foldedRect.x + 70, foldedRect.y + foldedRectLineHeight + foldedRectLinePaddingHeight * 3, foldedRect.width - 80, foldedRectLineHeight);
                var widthRect = new Rect(foldedRect.xMax - 190, foldedRect.y + foldedRectLinePaddingHeight, 90, foldedRectLineHeight);
                var heightRect = new Rect(foldedRect.xMax - 190, foldedRect.y + foldedRectLineHeight + foldedRectLinePaddingHeight * 3, 90, foldedRectLineHeight);
                var exportRect = new Rect(foldedRect.xMax - 80, foldedRect.y + foldedRectLineHeight/2 + foldedRectLinePaddingHeight * 2, 70, foldedRectLineHeight);
                EditorGUI.DrawRect(new Rect(foldedRect.position - Vector2.one, foldedRect.size + Vector2.one * 2), new Color(1,1,1,0.5f));
                EditorGUI.DrawRect(foldedRect, foldedRectColor);
                if (GUI.Button(copyButton1Rect, "Copy"))
                {
                    EditorGUIUtility.systemCopyBuffer = icon.name;
                }
                GUI.Label(copyContent1Rect, icon.name, whiteLabel);
                if (GUI.Button(copyButton2Rect, "Copy"))
                {
                    EditorGUIUtility.systemCopyBuffer = $"(Texture2D)EditorGUIUtility.Load(\"{icon.name}\")";
                }
                GUI.Label(copyContent2Rect, $"(<color=#4ec9b0>Texture2D</color>)<color=#4ec9b0>EditorGUIUtility</color>.<color=#dcdcaa>Load</color>(<color=#d69d85>\"{icon.name}\"</color>)", whiteLabel);
                whiteLabel.alignment = TextAnchor.MiddleRight;
                GUI.Label(widthRect, $"width = {icon.width}", whiteLabel);
                GUI.Label(heightRect, $"height = {icon.height}", whiteLabel);
                if(GUI.Button(exportRect, "Export"))
                {
                    var path = EditorUtility.SaveFilePanel($"Export the icon [{icon.name}]", Application.dataPath, icon.name, "png");
                    if (!string.IsNullOrEmpty(path))
                    {
                        var output = new Texture2D(icon.width, icon.height, icon.format, icon.mipmapCount > 1);
                        Graphics.CopyTexture(icon, output);
                        System.IO.File.WriteAllBytes(path, output.EncodeToPNG());
                        if (path.StartsWith(Application.dataPath))
                        {
                            AssetDatabase.Refresh();
                            var assetPath = path.Replace(Application.dataPath, "Assets");
                            var importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
                            importer.alphaIsTransparency = true;
                            AssetDatabase.ImportAsset(assetPath);
                        }
                    }
                }

                whiteLabel.alignment = TextAnchor.MiddleLeft;
            }

            //クリックでコピー用フォームを開閉
            if (evt.type == EventType.MouseDown && evt.button == 0 && elementMainRect.Contains(evt.mousePosition))
            {
                foldouts[icon.name] = !foldouts[icon.name];
                Repaint();
            }
            i++;
        }
        EditorGUILayout.EndScrollView();
    }

    /// <summary>
    /// フィルタ入力用フィールド。repaintCallbackにはEditorWindowのRepaintやそれに準ずるものを渡すこと。
    /// </summary>
    public static string FilterField(string filter, System.Action repaintCallback, string controlName = "__FilterField__")
    {
        var evt = Event.current;
        using (new EditorGUILayout.HorizontalScope())
        {
            //入力中にEnterキーでフォーカスを外す
            if (GUI.GetNameOfFocusedControl() == controlName && evt.type == EventType.KeyDown && evt.keyCode == KeyCode.Return)
            {
                EditorGUI.FocusTextInControl("");
                repaintCallback?.Invoke();
            }

            //入力欄
            GUI.SetNextControlName(controlName);
            filter = GUILayout.TextField(filter, "SearchTextField");
            var lastrect = GUILayoutUtility.GetLastRect();

            //入力欄以外でクリックされたらフォーカスを外す
            if (evt.type == EventType.MouseDown && evt.button == 0 && !lastrect.Contains(evt.mousePosition))
            {
                EditorGUI.FocusTextInControl("");
                repaintCallback?.Invoke();
            }

            //クリアボタン
            using (new EditorGUI.DisabledGroupScope(string.IsNullOrEmpty(filter)))
            {
                if (GUILayout.Button("Clear", "SearchCancelButton"))
                {
                    filter = "";
                }
            }
        }
        return filter;
    }
}

#注意
PNG出力の項でも触れましたが、アイコンのファイル名は不変であるとは限らず、
そもそも2019.3へのアップデートではアイコン画像も一新されたわけなので、
built-inアイコンをそのまま使う場合はUnityアップデートにお気をつけください。

#おわりに
cs Script Icon君、さすがに縮小し忘れでは?
image.png

#参考
UnityCsReference: EditorGUIUtility.cs
https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/EditorGUIUtility.cs

UnityCsReference: EditorResources.cs
https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/EditorResources.cs

Unity - Scripting API: EditorGUIUtility.systemCopyBuffer
https://docs.unity3d.com/ScriptReference/EditorGUIUtility-systemCopyBuffer.html

halak氏によるbuilt-inアイコン列挙スクリプト
https://github.com/halak/unity-editor-icons

コガネブログ:【Unity】エディタ拡張で検索欄を自作してみる
http://baba-s.hatenablog.com/entry/2017/12/28/145400

10
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?