第7章 EditorWindow - エディター拡張入門

第7章 EditorWindow

エディター拡張に初めて触れるとき、まずウィンドウを表示することから始めるかもしれません。この章では簡単に EditorWindow を表示する方法から目的に合った EditorWindow の選択、その特性について解説していきます。

7.1 EditorWindow とは

シーンウィンドウ、ゲームウィンドウ、インスペクターウィンドウなど、これらすべては EditorWindow です。Unity エディターはさまざまな機能を持つ EditorWindow の集まりでできています。

これらすべては EditorWindow

図7.1: これらすべては EditorWindow

7.2 HostView と SplitView と DockArea

EditorWindow は単体で描画されているわけではなく、親として DockArea を持ち、この DockArea によって EditorWindow は描画されています。

DockArea の上に EditorWindow があるイメージ

図7.2: DockArea の上に EditorWindow があるイメージ

DockArea は「Web ブラウザーのタブ」と同じ機能を提供します。

例えば、ウィンドウはそれぞれ独立した3つのウィンドウにもできますし、3つのタブにして1つのウィンドウにまとめることもできます。

Chrome のタブ機能

図7.3: Chrome のタブ機能

見た目も似ていますし、タブの機能として、タブを摘んで別のウィンドウにすることもできます。

DockArea のタブ機能

図7.4: DockArea のタブ機能

このように、DockArea には1つ以上の EditorWidow を描画するための機能が備わっています。例えば、2つ以上の EditorWindow が DockArea にある場合、タブ機能を使ってそれぞれの EditorWindow を表示するか SplitWindow で DockArea の領域を分割して表示します。

1つの DockArea にシーンビューとヒエラルキービューを分割して表示

図7.5: 1つの DockArea にシーンビューとヒエラルキービューを分割して表示

さらに DockArea は HostView の役割も持っています。HostView は、さまざまなオブジェクト・イベントとのやり取りを行うための機能があります。ウィンドウの「Update 関数」や「OnSelectionChange 関数」などを実行するための機能が含まれています。

3つのウィンドウ「HostView」「SplitView」「DockArea」を紹介しました。これらの領域(クラス)には残念ながらアクセスできません。ですが、覚えておくと EditorWindow の仕組みがより早く理解できるかもしれません。

7.3 EditorWindow の作成

まずは一般的な EditorWindow を作成してみましょう。

表示しただけの何もない EditorWindow

図7.6: 表示しただけの何もない EditorWindow

EditorWindow を表示するまで

EditorWindow を表示するまでの基本的な流れは3つのステップで構成されています。まずはこの3ステップを覚えましょう。

1. EditorWindow を作成するには EditorWindow を継承したクラスを作成します。

using UnityEditor;

public class Example : EditorWindow
{
}

2. 次に EditorWindow を表示するためのトリガーとしてメニューを追加します。

using UnityEditor;

public class Example : EditorWindow
{
    //MenuItem については 第8章「MenuItem」 をご覧ください。
    [MenuItem("Window/Example")]
    static void Open ()
    {
    }
}

3. 最後に EditorWindow の表示です。EditorWindow は ScriptableObject を継承しているので EditorWindow.CreateInstance で EditorWindow のオブジェクトを生成します。そして Show を呼び出すことにより EditorWindow が表示されます。

using UnityEditor;

public class Example : EditorWindow
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        var exampleWindow = CreateInstance<Example> ();
        exampleWindow.Show ();
    }
}
&quot;Window/Example&quot;メニューを実行して表示できる。実行するたびに EditorWindow を新規作成する。

図7.7: "Window/Example"メニューを実行して表示できる。実行するたびに EditorWindow を新規作成する。

7.4 EditorWindow.GetWindow

EditorWindow を作成する場合、「複数の存在を許可する EditorWindow」と「単数のみ許可する EditorWindow」の2種類が考えられます。複数の存在を許可する EditorWindow は先ほど説明した EditorWindow.CreateInstance を使って EditorWindow を作成し表示していきます。

単数のみ許可する EditorWindow

単数のみになると「すでに EditorWindow が存在している場合は生成しない」というチェックを実装しなければいけません。そのチェックを加えたものが次のコードになります。

using UnityEditor;

public class Example : EditorWindow
{
    static Example exampleWindow;

    [MenuItem("Window/Example")]
    static void Open ()
    {
        if (exampleWindow == null) {
            exampleWindow = CreateInstance<Example> ();
        }
        exampleWindow.Show ();
    }
}

これでもよいのですが、「すでに EditorWindow が存在すればそのインスタンスを取得する。なければ生成する。最後に Show 関数を実行する。」という複数の機能を1つにまとめた API が存在します。それが EditorWindow.GetWindow です。

using UnityEditor;

public class Example : EditorWindow
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        GetWindow<Example> ();
    }
}

GetWindow を実行すると内部でインスタンスがキャッシュされます。

さらに GetWindow 関数には便利な機能があり、特定の EditorWindow にタブウィンドウとして表示することが可能です。(DockArea に EditorWindow を追加)

シーンウィンドウにタブウィンドウとして追加できる

図7.8: シーンウィンドウにタブウィンドウとして追加できる

using UnityEditor;

public class Example : EditorWindow
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        GetWindow<Example> (typeof(SceneView));
    }
}

7.5 どの Show 関数を呼ぶかで変わる特殊な EditorWindow

今までこの章で扱った EditorWidow はデフォルト状態のタブ機能が使えるウィンドウです。他にも EditorWindow はさまざまな種類のウィンドウを作成することができます。

Show 関数は複数用意されており、どの Show 関数を呼び出すかで表示される EditorWindow が変化します。

Show

デフォルト状態のタブウィンドウとして扱えます。EditorWindow.GetWindow を使用している場合、内部で Show が呼び出されています。

ShowUtility

タブウィンドウとして扱えず、常に手前に表示し続けるウィンドウです。たとえ他のウィンドウにフォーカスを当ててもその Window 自体が裏側に回り込むことがありません。設定ウィンドウのような頻繁に他のウィンドウを操作してても最前面に表示したい場合に使用してください。

ヒエラルキーの Main Camera を選択してもウィンドウは最前面に表示されている

図7.9: ヒエラルキーの Main Camera を選択してもウィンドウは最前面に表示されている

GetWindow は使用できないので代わりに CreateInstance を使用します。

using UnityEditor;

public class Example : EditorWindow
{
    static Example exampleWindow;

    [MenuItem("Window/Example")]
    static void Open ()
    {
        if (exampleWindow == null) {
            exampleWindow = CreateInstance<Example> ();
        }
        exampleWindow.ShowUtility ();
    }
}

ShowPopup

ウィンドウタイトルと閉じるボタンがないウィンドウです。たとえ他の Window にフォーカスを当ててもその Window 自体が裏側に回り込むことがありません。閉じるボタンがないため、ウィンドウを閉じる処理は自前で実装する必要があります。

シーンウィンドウの上に表示されている

図7.10: シーンウィンドウの上に表示されている

using UnityEditor;
using UnityEngine;

public class Example : EditorWindow
{
    static Example exampleWindow;

    [MenuItem("Window/Example")]
    static void Open ()
    {
        if (exampleWindow == null) {
            exampleWindow = CreateInstance<Example> ();
        }
        exampleWindow.ShowPopup ();
    }


    void OnGUI ()
    {
        //エスケープを押した時に閉じる
        if (Event.current.keyCode == KeyCode.Escape) {
            exampleWindow.Close();
        }
    }
}

使いドコロとしては SpriteEditor のようなスライスメニューボタンから Popup を表示します。

これも EditorWindow の1種

図7.11: これも EditorWindow の1種

PopupWindow

直前で紹介した ShowPopup とほぼ同じもので Popup を表示するための機能です。PopupWindow は Popup を汎用的に扱えるようにしたものと考えてください。

ボタンを押すと真下に Popup が表示される

図7.12: ボタンを押すと真下に Popup が表示される

使い方はとても簡単です。まず PopupWindowContent を継承したクラスを作成します。そして PopupWindow.Show で Popup を表示します。

using UnityEditor;
using UnityEngine;

public class Example : EditorWindow
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        GetWindow<Example> ();
    }

    //インスタンス化
    ExamplePupupContent popupContent = new ExamplePupupContent ();

    void OnGUI ()
    {
        if (GUILayout.Button ("PopupContent",GUILayout.Width(128))) {
            var activatorRect = GUILayoutUtility.GetLastRect ();
            //Popup を表示する
            PopupWindow.Show (activatorRect, popupContent);
        }
    }
}

public class ExamplePupupContent : PopupWindowContent
{
    public override void OnGUI (Rect rect)
    {
        EditorGUILayout.LabelField ("Lebel");
    }

    public override void OnOpen ()
    {
        Debug.Log ("表示するときに呼び出される");
    }

    public override void OnClose ()
    {
        Debug.Log ("閉じるときに呼び出される");
    }

    public override Vector2 GetWindowSize ()
    {
        //Popup のサイズ
        return new Vector2 (300, 100);
    }
}

ShowAuxWindow

タブウィンドウとして扱えないウィンドウを作成します。見た目は ShowUtility と同じですが他のウィンドウにフォーカスを当てるとウィンドウは削除されます。ウィンドウの消し忘れが起こらないので数を増やすことがなく、ちょっとした設定・操作で使用することをオススメします。

見た目だけでは ShowUtility か ShowAuxWindow なのか判断は難しい

図7.13: 見た目だけでは ShowUtility か ShowAuxWindow なのか判断は難しい

ShowAsDropDown

Popup と同じで、「ウィンドウのタイトル」「閉じるボタン」がないウィンドウです。ただし、PC の画面サイズを考慮して、ウィンドウを表示する位置で十分な広さを確保できなかった場合、ウィンドウの表示領域を画面内に収めるために X/Y 軸の位置が自動的に補正されるようになります。言い換えると、画面の隅っこでウィンドウを出したとしても必ず PC の表示領域にすべて表示されます。

黒いのが ShowAsDropDown で表示したウィンドウだとする

図7.14: 黒いのが ShowAsDropDown で表示したウィンドウだとする

using UnityEditor;
using UnityEngine;

public class Example : EditorWindow
{
    static Example exampleWindow;

    [MenuItem("Window/Example")]
    static void Open ()
    {
        if (exampleWindow == null) {
            exampleWindow = CreateInstance<Example> ();
        }

        var buttonRect = new Rect (100, 100, 300, 100);
        var windowSize = new Vector2 (300, 100);
        exampleWindow.ShowAsDropDown (buttonRect, windowSize);
    }
}

これ以外は Popup と同じ機能です。

ScriptableWizard

「GameObject を作る」「Prefab を作る」「アセットを作る」、このように何かを「作る」時に使用するウィンドウです。ScriptableWizard は今まで紹介してきたウィンドウとは少し異なります。

ScriptableWizard の作り方

1. ScriptableWizard を継承したクラスを作成します。

using UnityEditor;

public class Example : ScriptableWizard
{
}

2. 次に ScriptableWizard を表示するためのトリガーとしてメニューを追加します。

using UnityEditor;

public class Example : ScriptableWizard
{
    //MenuItem については 第8章「MenuItem」 をご覧ください。
    [MenuItem("Window/Example")]
    static void Open ()
    {
    }
}

3. ScriptableWizard を表示します。表示は ScriptableWizard.DisplayWizard で行います。

using UnityEditor;

public class Example : ScriptableWizard
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        DisplayWizard<Example> ("Example Wizard");
    }
}
標準で右下に Create ボタンが表示される

図7.15: 標準で右下に Create ボタンが表示される

ScriptableWizard にはクラスのフィールドが表示される

他の EditorWindow では GUI の表示に EditorGUI クラスを使用しますが ScriptableWizard では使用できません。ScriptableWizard ではインスペクターで表示されるような「public なフィールド」「シリアライズ可能なフィールド」がウィンドウに表示されます。

インスペクターで表示されるようなものがそのまま表示されるイメージ

図7.16: インスペクターで表示されるようなものがそのまま表示されるイメージ

using UnityEditor;

public class Example : ScriptableWizard
{
    public string gameObjectName;

    [MenuItem("Window/Example")]
    static void Open ()
    {
        DisplayWizard<Example> ("Example Wizard");
    }
}

OnWizardCreate

ScriptableWizard の右下にある Create ボタンを押した時に呼び出されるメソッドです。

using UnityEditor;
using UnityEngine;

public class Example : ScriptableWizard
{
    public string gameObjectName;

    [MenuItem("Window/Example")]
    static void Open ()
    {
        DisplayWizard<Example> ("Example Wizard");
    }

    void OnWizardCreate ()
    {
        new GameObject (gameObjectName);
    }
}

OnWizardOtherButton

Create ボタンのほかにもう1つボタンを追加できます。作成に関して2つのパターンを作りたい場合に使用してください。ボタンを追加するには ScriptableWizard.DisplayWizard の第3引数でボタン名を指定する必要があります。

using UnityEditor;
using UnityEngine;

public class Example : ScriptableWizard
{
    public string gameObjectName;

    [MenuItem("Window/Example")]
    static void Open ()
    {
        DisplayWizard<Example> ("Example Wizard", "Create", "Find");
    }

    void OnWizardCreate ()
    {
        new GameObject (gameObjectName);
    }

    void OnWizardOtherButton ()
    {
        var gameObject = GameObject.Find (gameObjectName);

        if (gameObject == null)
        {
            Debug.Log ("ゲームオブジェクトが見つかりません");
        }
    }
}

OnWizardUpdate

すべてのフィールドの値を対象に、値の変更があった場合に呼び出されるメソッドです。

using UnityEditor;
using UnityEngine;

public class Example : ScriptableWizard
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        DisplayWizard<Example> ("Example Wizard");
    }

    void OnWizardUpdate ()
    {
        Debug.Log ("Update");
    }
}

DrawWizardGUI

ウィザート内の GUI を描画するためのメソッドです。このメソッドをオーバーライドすることにより GUI をカスタマイズできます。

ただし、戻り値として true を返すようにしてください。true を返さなければ OnWizardUpdate が呼び出されなくなってしまいます。

今まで表示されていたプロパティーがなくなりラベルが表示された

図7.17: 今まで表示されていたプロパティーがなくなりラベルが表示された

using UnityEditor;
using UnityEngine;

public class Example : ScriptableWizard
{
    public string gameObjectName;

    [MenuItem ("Window/Example")]
    static void Open ()
    {
        DisplayWizard<Example> ("Example Wizard");
    }

    protected override bool DrawWizardGUI ()
    {
        EditorGUILayout.LabelField ("Label");
        //false を返すことで OnWizardUpdate が呼び出されなくなる
        return true;
    }
}

OnGUI メソッドは使用しないこと

ScriptableWizard クラスは EditorWindow を継承しています。なので OnGUI メソッドを使用すると通常の EditorWindow としての表示となってしまい、フィールドの値や Create ボタンが表示されなくなってしまいます。

OnGUI メソッドを記述すると Create ボタンが消えてしまう

図7.18: OnGUI メソッドを記述すると Create ボタンが消えてしまう

PreferenceItem

PreferenceItem は Unity Preferences にメニューを追加するための機能です。Unity Preferences には Unity エディター全体に影響のある設定を行うためにあります。

追加されるメニューは一番最後の位置に追加される

図7.19: 追加されるメニューは一番最後の位置に追加される

using UnityEditor;

public class Example
{
    [PreferenceItem("Example")]
    static void OnPreferenceGUI ()
    {

    }
}

7.6 メニューを追加する IHasCustomMenu

タブ上で右クリック、または ≡ をクリックすることで表示されるコンテキストメニューにメニューを追加します。

example と example2が追加された

図7.20: example と example2が追加された

IHasCustomMenu はインターフェースとして実装されています。

using UnityEditor;
using UnityEngine;

public class Example : EditorWindow, IHasCustomMenu
{

    public void AddItemsToMenu (GenericMenu menu)
    {
        menu.AddItem (new GUIContent ("example"), false, () => {

        });

        menu.AddItem (new GUIContent ("example2"), true, () => {

        });
    }

    [MenuItem("Window/Example")]
    static void Open ()
    {
        GetWindow<Example> ();
    }
}

7.7 EditorWindow のサイズを変更できないようにする

右下にあるリサイズするための三角マークが消えている

図7.21: 右下にあるリサイズするための三角マークが消えている

EditorWindow.minSizeEditorWindow.maxSize によって EditorWindow の大きさの制限を行うことができます。最小値と最大値が同じであれば EditorWindow の大きさを変更する必要がないと判断され右下に表示されていた三角マークが非表示となります。

using UnityEditor;
using UnityEngine;

public class Example : EditorWindow
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        var window = GetWindow<Example> ();
        window.maxSize = window.minSize = new Vector2 (300, 300);
    }
}

7.8 ウィンドウにアイコンを追加する

アイコンを追加するには EditorWindow.titleContent にアイコンを持つ GUIContent を代入します。

アイコンはとても小さいので分かりやすいものにすること

図7.22: アイコンはとても小さいので分かりやすいものにすること

using UnityEditor;
using UnityEngine;

public class Example : EditorWindow
{
    [MenuItem ("Window/Example")]
    static void SaveEditorWindow ()
    {
        var window = GetWindow<Example> ();

        var icon = AssetDatabase.LoadAssetAtPath<Texture> ("Assets/Editor/Example.png");

        window.titleContent = new GUIContent ("Hoge", icon);
    }
}

7.9 GetWindow を使わずにすでにある EditorWindow を取得するには?

自前でシングルトンを実装するか、GetWindow によって内部にキャッシュしておく方法で EditorWindow へとアクセスできます。しかし、先ほど上げた2つの方法が使えない状況も出てきます。その時は Resources クラスにある Resources.FindObjectsOfTypeAll を使用します。

FindObjectsOfTypeAll現在ロードされているすべてのオブジェクトから特定のオブジェクトを検索し、取得します。これはランタイムで使用するオブジェクトだけではなく、エディターで使用するオブジェクトも検索対象となります。

using UnityEditor;
using UnityEngine;

public class Example : EditorWindow
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        //すべてのシーンビューを取得する
        var sceneViews = Resources.FindObjectsOfTypeAll<SceneView> ();
    }
}

7.10 EditorWindow も ScriptableObject であることを知る

アセンブリブラウザーで見ると ScriptableObject が継承されていることが分かる

図7.23: アセンブリブラウザーで見ると ScriptableObject が継承されていることが分かる

EditorWindow は、ScriptableObject の動作にしたがい、EditorWindow オブジェクトをアセットとして保存することができます。その時に、インスペクターにはシリアライズされたプロパティーも表示されます。

Example ウインドウをアセットとして保存した時のインスペクター

図7.24: Example ウインドウをアセットとして保存した時のインスペクター

using UnityEditor;
using UnityEngine;
public class Example : EditorWindow
{
    [MenuItem ("Assets/Save EditorWindow")]
    static void SaveEditorWindow ()
    {
        AssetDatabase.CreateAsset (CreateInstance<Example> (), "Assets/Example.asset");
        AssetDatabase.Refresh ();
    }

    [SerializeField]
    string text;

    [SerializeField]
    bool boolean;
}

ウィンドウ位置やサイズなども保存されています。それらのデータは直接 YAML 形式のファイルをテキストエディターで見れば確認できます。

  m_MinSize: {x: 100, y: 100}
  m_MaxSize: {x: 4000, y: 4000}
  m_TitleContent:
    m_Text: Example
    m_Image: {fileID: 0}
    m_Tooltip:
  m_DepthBufferBits: 0
  m_AntiAlias: 0
  m_Pos:
    serializedVersion: 2
    x: 0
    y: 0
    width: 320
    height: 240
第6章 EditorGUI (EdirotGUILayout) 第8章 MenuItem