【Unity】エディタ上 進捗バーの作り方【IMGUI, UIToolKit】

に公開

1:背景

  • 開発ツールで激重処理があり、フリーズを疑うレベルで処理待ちが発生することがあった
  • 進捗バー(プログレスバー)を使いたい
  • ちなみに↑の開発ツールは Tilemap を分割するもの。よかったら見てね

2:結論とプログラム

  • 実際に試したい場合は「具体的なプログラム」セクションをコピーして
    Unity プロジェクトの Editor フォルダ下に配置し、
    画面上部 → Examples → ProgressBarWindow_… を選択で同様のウィンドウが出る

uGUI(IMGUI) 版

  • EditorGUI.ProgressBar

  • EditorUtility.DisplayProgressBar

  • EditorUtility.DisplayCancelableProgressBar

具体的なプログラム

処理の流れ

実際のコード

using UnityEditor;
using UnityEngine;

public class ProgressBarWindow_IMGUI : EditorWindow
{
    private enum DisplayMode { ProgressBar, Modal_Bar, Cancelable_Modal_Bar}
    private DisplayMode displayMode = DisplayMode.ProgressBar;

    private float durationSeconds = 5f;

    private bool   isProcessing = false;
    private double startTime;
    private float  progress = 0f;

    [MenuItem("Examples/ProgressBarWindow_IMGUI")]
    public static void ShowWindow(){ GetWindow<ProgressBarWindow_IMGUI>("ProgressBarWindow_IMGUI"); }

    private void OnGUI()
    {
        GUILayout.Label("秒数指定プログレスバー", EditorStyles.boldLabel);

        //表示方法の選択, 処理時間設定
        displayMode     = (DisplayMode)EditorGUILayout.EnumPopup("表示方法", displayMode);
        durationSeconds = EditorGUILayout.FloatField("処理時間(秒)", durationSeconds);

        //開始ボタン設定
        EditorGUI.BeginDisabledGroup(isProcessing);
        if (GUILayout.Button("処理開始")) StartTimedOperation();
        EditorGUI.EndDisabledGroup();

        if (isProcessing) //進捗表示
        {
            GUILayout.Space(10);

            //IMGUI内に埋め込みバーを描く
            if (displayMode == DisplayMode.ProgressBar)
            {
                var rect = GUILayoutUtility.GetRect(200, 20);
                EditorGUI.ProgressBar(rect, progress, $"進捗: {progress * 100:0.0}%");
            }
            else //モーダル表示中は小さくラベルだけ
            {
                GUILayout.Label($"進捗: {progress * 100:0.0}%");
            }
        }
    }

    private void StartTimedOperation()
    {
        if (durationSeconds <= 0f)
        {
            EditorUtility.DisplayDialog("エラー", "0 より大きい値を指定せんかい", "OK");
            return;
        }

        isProcessing = true;
        startTime    = EditorApplication.timeSinceStartup;
        progress     = 0f;

        EditorApplication.update += UpdateProgress;
    }

    private void UpdateProgress()
    {
        //経過秒数計算
        var elapsed = EditorApplication.timeSinceStartup - startTime;
        progress    = Mathf.Clamp01((float)(elapsed / durationSeconds));

        //表示方法ごとにバー更新
        var info = $"処理進行中({elapsed:0.0}s / {durationSeconds:0.0}s)";
        switch (displayMode)
        {
            case DisplayMode.Modal_Bar:
                EditorUtility.DisplayProgressBar("", info, progress);
                break;

            case DisplayMode.Cancelable_Modal_Bar:
                bool canceled = EditorUtility.DisplayCancelableProgressBar("", info, progress);
                if (canceled)
                {
                    Cleanup("キャンセル");
                    return;
                }
                break;

            case DisplayMode.ProgressBar: //OnGUI 側で描画するので何もしない
                break;
        }

        if (progress >= 1f) Cleanup("処理完了");
    }

    private void Cleanup(string message)
    {
        EditorApplication.update -= UpdateProgress;
        isProcessing = false;

        //モーダルの場合はプログレスバーをクリア
        if (displayMode != DisplayMode.ProgressBar) EditorUtility.ClearProgressBar();

        EditorUtility.DisplayDialog("完了", message, "OK");
    }
}

UIElements(UIToolKit) 版

  • ProgressBar
具体的なプログラム

処理の流れ

実際のコード

using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

public class ProgressBarWindow_UIToolKit : EditorWindow
{
    private float durationSeconds = 5f;

    //内部状態管理用
    private bool   isProcessing;
    private double startTime;
    private float  progress;

    //UI 要素
    private FloatField  durationField;
    private Button      startButton;  
    private Button      cancelButton; 
    private ProgressBar progressBar;  
    private Label       progressLabel;
    private Label       messageLabel;

    [MenuItem("Examples/ProgressBarWindow_UIToolKit")]
    public static void ShowWindow()
    {
        // ウィンドウを開き、最小サイズを設定
        var wnd = GetWindow<ProgressBarWindow_UIToolKit>("ProgressBarWindow_UIToolKit");
        wnd.minSize = new Vector2(350, 160);
    }

    //ウィンドウ初期化時に呼ばれ、UI Toolkit の要素を構築する
    public void CreateGUI()
    {
        var root = rootVisualElement;
        root.style.paddingLeft  = 10;
        root.style.paddingRight = 10;
        root.style.paddingTop   = 10;

        //処理時間入力フィールド
        durationField = new FloatField("処理時間(秒)");
        durationField.value = durationSeconds;
        durationField.RegisterValueChangedCallback(evt => durationSeconds = evt.newValue);
        root.Add(durationField);

        //開始, キャンセルボタン
        var buttonRow = new VisualElement();
        buttonRow.style.flexDirection  = FlexDirection.Row;
        buttonRow.style.marginTop      = 10;
        buttonRow.style.justifyContent = Justify.SpaceBetween;
        startButton  = new Button(OnStart)  { text = "開始" };
        cancelButton = new Button(OnCancel) { text = "キャンセル" };
        cancelButton.SetEnabled(false);
        buttonRow.Add(startButton);
        buttonRow.Add(cancelButton);
        root.Add(buttonRow);

        //プログレスバー
        progressBar = new ProgressBar();
        progressBar.lowValue        = 0;
        progressBar.highValue       = 1;
        progressBar.value           = 0;
        progressBar.style.height    = 18;
        progressBar.style.marginTop = 10;
        root.Add(progressBar);

        //進捗表示テキスト
        progressLabel = new Label("進捗: 0.0%");
        progressLabel.style.marginTop = 5;
        root.Add(progressLabel);

        //エラーや完了メッセージを表示するテキスト
        messageLabel = new Label("");
        messageLabel.style.color          = new StyleColor(Color.red);
        messageLabel.style.marginTop      = 5;
        messageLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
        root.Add(messageLabel);
    }

    private void OnEnable() { EditorApplication.update -= UpdateProgress; }
    private void OnDisable() { EditorApplication.update -= UpdateProgress; }

    private void OnStart()
    {
        messageLabel.text = "";

        if (durationSeconds <= 0f)
        {
            messageLabel.text = "エラー:0 より大きい値を指定せんかい";
            return;
        }

        //内部状態をリセット, 更新
        isProcessing = true;
        startTime    = EditorApplication.timeSinceStartup;
        progress     = 0f;
        startButton.SetEnabled(false);
        cancelButton.SetEnabled(true);
        progressBar.value  = 0f;
        progressLabel.text = "進捗: 0.0%";

        EditorApplication.update += UpdateProgress;
    }

    private void OnCancel()
    {
        if (isProcessing == false) return;
        Cleanup("処理キャンセル");
    }

    private void UpdateProgress()
    {
        //進捗計算
        var elapsed = (float)(EditorApplication.timeSinceStartup - startTime);
        progress    = Mathf.Clamp01(elapsed / durationSeconds);
        Debug.Log(progress);
        //プログレスバーとテキストに反映
        progressBar.value      = progress;
        progressLabel.text     = $"進捗: {progress * 100:0.0}%";

        if (progress >= 1f) Cleanup("処理完了");
    }

    //処理終了後の共通の処理
    private void Cleanup(string message)
    {
        isProcessing = false;
        EditorApplication.update -= UpdateProgress;
        startButton.SetEnabled(true);
        cancelButton.SetEnabled(false);
        messageLabel.text = message;
    }
}

3:実装のポイント

UniTask, EditorCoroutineUtility を導入するかどうか

  • 「一部だけフレームを跨いで処理したい」, 「フリーズさせず非同期で処理を流したい」
  • ↑のような要望がある場合、導入はアリ
  • プログレスバーのためだけの導入は要注意

EditorApplication.update の解除管理

  • OnDisable, Cleanup(または OnCancel)両方で EditorApplication.update -= UpdateProgress を行い、ウィンドウ破棄, キャンセル時のリークを防ぐ

バーの再描画(IMGUI版)

  • UpdateProgressRepaint() 実行か EditorApplication.QueuePlayerLoopUpdate() を用いて即時に UI を更新すると、埋め込みバーが途切れず滑らかに動作する

CancelableProgressBar からの復帰

  • モーダルのキャンセル後に progressBar(UIElements 版)や進捗ラベルを 0 に設定しておくと、再度開始したときに前の状態が残らない

4:参考

Discussion