第6章 EditorGUI (EdirotGUILayout) - エディター拡張入門

第6章 EditorGUI (EdirotGUILayout)

頑張ればこのような GUI スタイルにすることもできる

図6.1: 頑張ればこのような GUI スタイルにすることもできる

この機能を知らなくてはエディター拡張では何もできないと言っても過言ではありません。この章ではすべてを紹介はできませんが、知っておくと EditorGUI や EditorGUILayout でできることの幅が広がる機能を中心に説明していきます。サンプルコードは簡略化の関係で EditorGUILayout を使用することが多くなります。


6.1 EditorGUI とは

ランタイム側にある GUI クラスと役割は同じですが Editor 向けに機能が追加されているクラスです。逐次配置される EditorGUILayout クラスも用意されています。

まずは簡単に EditorWindow の上に EditorGUILayout で文字を表示してみましょう。

GUI の第一歩

図6.2: GUI の第一歩

using UnityEngine;
using UnityEditor;

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

    void OnGUI ()
    {
        EditorGUILayout.LabelField ("Example Label");
    }
}

6.2 ChangeCheck

BeginChangeCheck と EndChangeCheck で囲まれた GUI になんらかの変更が行われた時、EndChangeCheck が true を返します。

using UnityEngine;
using UnityEditor;

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

    bool toggleValue;

    void OnGUI ()
    {
        EditorGUI.BeginChangeCheck ();

        //toggle をマウスでクリックして値を変更する
        toggleValue = EditorGUILayout.ToggleLeft ("Toggle", toggleValue);

        //toggleValue の値が変更されるたびに true になる
        if (EditorGUI.EndChangeCheck ()) {



            if (toggleValue) {
                Debug.Log ("toggleValue が true になった瞬間呼び出される");
            }

        }
    }
}

実は GUI.changed

ChangeCheck は GUI.changed で実装されています。GUI.changed で ChangeCheck と同じ実装をすると、ネストのことを考えると次のような実装になります。

bool toggleValue;

Stack<bool> stack = new Stack<bool> ();

void OnGUI ()
{
    //BeginChangeCheck の役割
    {
        //先頭に値を push
        stack.Push (GUI.changed);
        GUI.changed = false;
    }


    toggleValue = EditorGUILayout.ToggleLeft ("Toggle", toggleValue);


    //EndChangeCheck の役割
    {
        bool changed = GUI.changed;

        //どちらかが true であれば以降はすべて変更されているものとする
        GUI.changed |= stack.Pop ();
    }

    if (changed) {
        Debug.Log ("toggleValue が true になった瞬間呼び出される");
    }
}

6.3 DisabledGroup

下側の GUI が薄くなっていて操作することができない

図6.3: 下側の GUI が薄くなっていて操作することができない

using UnityEngine;
using UnityEditor;

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

    void OnGUI ()
    {
        Display ();

        EditorGUILayout.Space ();

        EditorGUI.BeginDisabledGroup (true);

        Display ();

        EditorGUI.EndDisabledGroup ();
    }

    void Display ()
    {
        EditorGUILayout.ToggleLeft ("Toggle", false);
        EditorGUILayout.IntSlider (0, 10, 0);
        GUILayout.Button ("Button");
    }
}

実は GUI.enabled

DisabledGroup の中身は GUI.enabled で実装されています。GUI.enabled で DisabledGroup と同じ実装をすると、次のような実装になります。

void OnGUI ()
{
    Display ();

    EditorGUILayout.Space ();

    GUI.enabled = false;

    Display ();

    GUI.enabled = true;
}

void Display ()
{
    EditorGUILayout.ToggleLeft ("Toggle", false);
    EditorGUILayout.IntSlider (0, 10, 0);
    GUILayout.Button ("Button");
}

6.4 FadeGroup

左側がフェード中。右側が通常の状態

図6.4: 左側がフェード中。右側が通常の状態

GUI のグループをフェードイン・フェードアウトさせる場合に使用します。なんらかのトリガー、今回はボタンを押したらフェードで GUI が表示されるようにしてみます。

フェード中は GUI を操作できません。フェードのスピードはある程度早めにしてユーザーを待たせることのないようにしましょう。

using UnityEngine;
using UnityEditor;
using UnityEditor.AnimatedValues;
using UnityEngine.Events;

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

    //初期値が0だとフェードを行わないと判断されるため0.0001f というような0に近い値にする
    AnimFloat animFloat = new AnimFloat (0.0001f);
    Texture tex;
    void OnGUI ()
    {
        bool on = animFloat.value == 1;

        if (GUILayout.Button (on ? "Close" : "Open", GUILayout.Width (64))) {
            animFloat.target = on ? 0.0001f : 1;
            animFloat.speed = 0.05f;

            //値が変わるごとに EditorWindow を再描画する
            var env = new UnityEvent ();
            env.AddListener (() => Repaint ());
            animFloat.valueChanged = env;
        }

        EditorGUILayout.BeginHorizontal ();
        EditorGUILayout.BeginFadeGroup (animFloat.value);
        Display ();
        EditorGUILayout.EndFadeGroup ();
        Display ();
        EditorGUILayout.EndHorizontal ();
    }

    void Display ()
    {
        EditorGUILayout.BeginVertical ();
        EditorGUILayout.ToggleLeft ("Toggle", false);

        var options = new []{GUILayout.Width (128), GUILayout.Height (128)};

        tex = EditorGUILayout.ObjectField (
                tex, typeof(Texture), false, options) as Texture;

        GUILayout.Button ("Button");
        EditorGUILayout.EndVertical ();
    }
}

6.5 EditorGUI.ObjectField

オブジェクトの参照を扱うフィールドです。引数に受け付けるオブジェクトの型を指定できます。

また、テクスチャ系(Texture2D や Sprite)は特殊なサムネイル形式のフィールドになります。

通常はオブジェクトのアイコンと受け付けるオブジェクトの型名

図6.5: 通常はオブジェクトのアイコンと受け付けるオブジェクトの型名

void OnGUI ()
{
    EditorGUILayout.ObjectField (null, typeof(Object), false);

    EditorGUILayout.ObjectField (null, typeof(Material), false);

    EditorGUILayout.ObjectField (null, typeof(AudioClip), false);

    var options = new []{GUILayout.Width (64), GUILayout.Height (64)};

    EditorGUILayout.ObjectField (null, typeof(Texture), false, options);

    EditorGUILayout.ObjectField (null, typeof(Sprite), false, options);
}

6.6 EditorGUI.MultiFloatField

複数の float 値を編集するフィールドを1行で描画するためのものです。

インスペクターで Vector3 の値を編集するような描画になります。

決められた Rect 内ですべての要素を均等に描画する

図6.6: 決められた Rect 内ですべての要素を均等に描画する

float[] numbers = new float[] {
    0,
    1,
    2
};

GUIContent[] contents = new GUIContent[] {
    new GUIContent ("X"),
    new GUIContent ("Y"),
    new GUIContent ("Z")
};

void OnGUI ()
{
    EditorGUI.MultiFloatField (
        new Rect (30, 30, 200, EditorGUIUtility.singleLineHeight),
        new GUIContent ("Label"),
        contents,
        numbers);
}

6.7 EditorGUI.indentLevel

インデントのレベルを管理します。次のコードのように部分的にインデントを増減させることにより、インスペクターやヒエラルキーで見るような階層構造を構成できます。

ヒエラルキーでよく見る親子関係

図6.7: ヒエラルキーでよく見る親子関係

void OnGUI ()
{
    EditorGUILayout.LabelField ("Parent");

    EditorGUI.indentLevel++;

    EditorGUILayout.LabelField ("Child");
    EditorGUILayout.LabelField ("Child");

    EditorGUI.indentLevel--;

    EditorGUILayout.LabelField ("Parent");

    EditorGUI.indentLevel++;

    EditorGUILayout.LabelField ("Child");
}

これは EditorGUI と EditorGUILayout の両方に効果があります。

6.8 EditorGUILayout.Knob

角度や、決められた範囲内で値を設定するための「つまみ(ノブ)」を作成します。マウスでドラッグしたり、表示されているラベルをクリックすることで値を直接入力できます。

マウスでドラッグすることで値を調整できる

図6.8: マウスでドラッグすることで値を調整できる

float angle = 0;

void OnGUI ()
{
    angle = EditorGUILayout.Knob (Vector2.one * 64,
        angle, 0, 360, "度", Color.gray, Color.red, true);
}

6.9 Scope

EditorGUILayout.BeginHorizontal/EndHorizontal というように Begin/End で始まる GUI グループのヘルパー機能になります。標準で HorizontalScopeVerticalScopeScrollViewScope などのスコープが用意されています。スコープ自体は IDisposable オブジェクトで実装されており、using を使用できます。

using (new EditorGUILayout.HorizontalScope ()) {
  GUILayout.Button ("Button1");
  GUILayout.Button ("Button2");
}
ボタンが横に並んだ状態

図6.9: ボタンが横に並んだ状態

Scope の自作 - BackgroundColorScope

HorizontalScope のように、スコープは GUI.Scope クラスを継承して作成されています。

public class HorizontalScope : GUI.Scope
{
  public HorizontalScope()
  {
    EditorGUILayout.BeginHorizontal();
  }

  protected override void CloseScope()
  {
    EditorGUILayout.EndHorizontal();
  }

  //略
}

CloseScope メソッドは Dispose 時に呼び出されるメソッドです。コンストラクター内で Begin、CloseScope メソッド内で End を呼び出しています。

同じようにして GUI.Scope を継承したクラスでスコープを作成することが可能です。今回は試しに 図6.10 のようなスコープ内のみ GUI の背景を変更する BackgroundColorScope を作成してみましょう。

1つのボタンごとに色が変更されている

図6.10: 1つのボタンごとに色が変更されている

using UnityEngine;

public class BackgroundColorScope : GUI.Scope
{
    private readonly Color color;
    public BackgroundColorScope(Color color)
    {
        this.color = GUI.backgroundColor;
        GUI.backgroundColor = color;
    }


    protected override void CloseScope()
    {
        GUI.backgroundColor = color;
    }
}

このように GUI.backgroundColor に Color 情報を渡す前に変数として保持し、CloseScope で元の色に戻します。

using (new BackgroundColorScope (Color.green)) {
  // 緑色のボタン
  GUILayout.Button ("Button");

  using (new BackgroundColorScope (Color.yellow)) {
    // 黄色のボタン
    GUILayout.Button ("Button");
  }
}

6.10 見た目はボタン、中身はトグル

Unity エディターの GUI には見た目のスタイルはボタンなのに、動作はトグルのオン/オフ機能が備わった、ボタン群がいくつも存在します。

ツール、再生ボタン、PlayerSettings のプラットフォーム別設定

図6.11: ツール、再生ボタン、PlayerSettings のプラットフォーム別設定

これらのボタン?トグル?の実装方法を紹介していきます。

スタイルがボタンなトグル(シングル)

on の時はボタンがずっと押されている状態

図6.12: on の時はボタンがずっと押されている状態

作り方はいたってシンプルで、Toggle に ボタンのスタイルを適用するだけです。

bool on;
void OnGUI ()
{
    //GUIStyle は文字列で指定することも出来る
    on = GUILayout.Toggle (on, on ? "on" : "off", "button");
}

スタイルがボタンなトグル(マルチプル)

複数の選択肢の中から1つを選ばせる時に 図6.1 のようなトグル群を作成します。

bool one, two, three;

void OnGUI ()
{
  using (new EditorGUILayout.HorizontalScope ()) {
    one = GUILayout.Toggle (one, "1", EditorStyles.miniButtonLeft);
    two = GUILayout.Toggle (two, "2", EditorStyles.miniButtonMid);
    three = GUILayout.Toggle (three, "3", EditorStyles.miniButtonRight);
  }
}

おそらく、複数のトグルを配置しようとすると上記のようなコードになるかもしれません。ですがこれは悪手です。これだと bool 変数がトグルの数だけ増えることになりますし、その管理も面倒です。これらは GUILayout.Toolbar を使って簡単に解決することができます。

int selected;

void OnGUI ()
{
  selected = GUILayout.Toolbar (selected, new string[]{ "1", "2", "3" });
}

何が選択されているかを int 変数で管理し、表示するトグル(の文字列)は string 配列で管理します。

また、GUIStyle を変更することでさまざまな表現が可能になります。

EditorStyles.toolbarButton を使用することでツールバーや、PlayerSettings のプラットフォーム別設定にあるようなトグル群を表現できます。

int selected;

void OnGUI ()
{
  selected = GUILayout.Toolbar (selected,
      new string[]{ "1", "2", "3" }, EditorStyles.toolbarButton);
}

1列に表示した GUILayout.SelectionGrid で、スタイルを PreferencesKeysElement(Unity 内部で実装されている GUIStyle)にすると Preferences ウィンドウで表現されている選択メニューとなります。

int selected;

void OnGUI ()
{
  selected = GUILayout.SelectionGrid (selected,
      new string[]{ "1", "2", "3" },1, "PreferencesKeysElement");
}
第5章 SerializedObject について 第7章 EditorWindow