【VBA】非モーダルUserFormで進捗表示を作る方法

非モーダルUserFormで進捗表示を作る方法(VBAのコード画面風のアイキャッチ) VBA

数千行を処理するマクロを、画面更新を止めて走らせると、Excelが固まったように見えます。終わるまで何も出ないので、動いているのか止まっているのか判断できません。

実務では、ラベルの文字だけを書き換える簡易な進捗表示を、非モーダルのUserFormで作っています。フォームには出すメッセージもサイズも位置も持たせず、枠だけ用意して、中身はすべてコードで指定する作りです。この記事では、その作り方と、使うときに押さえておく点を実測してまとめます。

先に結論

  • .Show vbModeless は0.016秒で戻り、フォームを出したままマクロが進みます
  • ラベルを書き換えただけでは、処理中の画面に文字が出ません。Repaint か DoEvents を呼ぶと出ます
  • Unload すると指定は消えて、枠だけの状態に戻ります。開くたびにコードで指定し直します
  • 閉じ忘れると、マクロが終わってもフォームが画面に残ります
  • 正常終了とエラーを同じ後片付けに流して、そこで Unload します

測った環境は、Excel 16.0(ビルド20527)、Windows 64bit、日本語版です。

フォームは名前だけ用意する

VBEで「挿入 → ユーザーフォーム」を選び、オブジェクト名だけ決めます。

  1. フォームのオブジェクト名を frmProgress にする
  2. ツールボックスからラベルを1つ置き、オブジェクト名を lblMsg にする
  3. ほかのプロパティ(Caption・サイズ・位置・文字)は触らない
VBEのユーザーフォームの設計画面。ラベルが1つ置かれた空のフォームがあり、プロジェクトとプロパティウィンドウにフォーム名 frmProgress が表示されている

この枠は、読み込んだ直後は幅240・高さ180、ラベルの文字は Label1 でした。Caption・サイズ・位置・メッセージは、開くときにコードで指定します。

Private Sub OpenProgress(ByVal msg As String)
    With frmProgress
        .Caption = "処理中"
        .StartUpPosition = 0
        .Width = 300
        .Height = 90
        .Left = Application.Left + (Application.Width - .Width) / 2
        .Top = Application.Top + (Application.Height - .Height) / 2
        With .lblMsg
            .Left = 12: .Top = 12: .Width = 260: .Height = 18
            .Caption = msg
        End With
        .Show vbModeless
    End With
End Sub

Private Sub UpdateProgress(ByVal msg As String)
    frmProgress.lblMsg.Caption = msg
    frmProgress.Repaint
End Sub

Public Sub RunMain()
    Dim eNo As Long, eSrc As String, eDesc As String
    Dim i As Long
    On Error GoTo Cleanup
    OpenProgress "準備中"
    For i = 1 To 5
        UpdateProgress "処理中 " & i & " / 5"
        '(ここに本処理)
    Next i
Cleanup:
    eNo = Err.Number: eSrc = Err.Source: eDesc = Err.Description
    On Error Resume Next
    Unload frmProgress
    On Error GoTo 0
    If eNo <> 0 Then Err.Raise eNo, eSrc, eDesc
End Sub

位置は、Excelのウィンドウの中央にしています。最大化したウィンドウ、小さくしたウィンドウ、別モニターに置いたウィンドウの3通りで、フォームはExcelウィンドウの中央に出ました。

Excelのウィンドウの中央に小さなフォームが出て、ラベルに「処理中 4 / 5」と表示されている

.Show vbModeless は表示したまま次の行へ進む

引数なしの .Show はモーダルで、フォームを閉じるまで次の行へ進みません。実際に、×で閉じるまで戻らず、今回は8.1秒かかりました。vbModeless を渡すと、0.016秒で戻ります。続きのコードが実行されることは、Microsoft Learnの Show method にも書かれています。

ラベルを書き換えても画面に出ない

5段階の処理を、各段階で1秒待つだけの処理にして、フォームに見えた表示を記録しました。

やり方見えた表示
ラベルを書き換えるだけ文字なし
書き換え+Repaint1〜5の全部
書き換え+DoEvents1〜5の全部
段階ごとに Unload、指定し直して再表示文字なし
ScreenUpdating = False+書き換えるだけ文字なし
ScreenUpdating = False+書き換え+Repaint1〜5の全部

書き換えるだけでは、フォームの枠は出ても文字が出ません。段階ごとに閉じて開き直しても、待つだけの処理では出ませんでした。ScreenUpdating = False でも、Repaint を呼べば文字は出ます。Microsoft Learnの Repaint method は、再描画を待たずに表示を更新するメソッドです。

更新は毎回やらない

ループ2万回で、更新の方法ごとに3回ずつ測りました。

パターン平均(秒)1回あたり
更新なし0.003–
書き換えるだけ(画面には出ない)0.0910.004ミリ秒
毎回 Repaint5.0810.25ミリ秒
毎回 DoEvents7.1640.36ミリ秒
100回に1回 Repaint(200回)0.057–
100回に1回 DoEvents(200回)0.117–
ステータスバーを毎回5.6410.28ミリ秒

毎回更新すると、2万回で5秒前後かかります。100回に1回にすると、0.06秒で済みました。ステータスバーも、1回あたりの時間は同じくらいです。Timer の刻みが粗いので、0.1秒未満の値は目安です。

シート操作が重くなる仕組みは、【VBA】処理が遅い本当の原因|シート操作が重くなる仕組み で測っています。

Unloadを忘れると何が残るか

Unload を忘れて正常終了すると、フォームは画面に残ります。UserForms.Count は1、Visible は True、ラベルの文字は最後に書いたメッセージのままでした。右上の×で閉じると、Count は0になります。

Hide は見えなくするだけです。Hide → Show では、文字もサイズも指定したままで、Unload → 再表示では、最初の枠(Label1、幅240・高さ180)に戻ります。

Unload したあとに frmProgress.lblMsg.Caption を読み書きすると、フォームが見えないまま読み込み直されます(Count が1)。Set f = New frmProgress で変数に持つ場合は、Unload f のあとで中身を読んでも Count は0のままでした。

エラーでも正常終了でも必ずUnloadする

エラー処理を書いていないと、エラーは呼び出し元に届き、Unload まで進まないのでフォームが残ります(ゼロ除算のエラーで、UserForms.Count は1でした)。

上のコードのように Cleanup に流す書き方では、エラーが途中でも、Show の前でも、正常終了でも、Count は0です。先に Err の内容を退避して、Unload のあとで Err.Raise するので、エラー番号とメッセージもそのまま呼び出し元に届きます。Exit Sub を Cleanup の手前に置くと、正常終了のときに Unload が走らず、Count は1になります。

ScreenUpdating などの設定を元に戻す後片付けも、同じ形で書けます。詳しくは 【VBA】エラー時も設定を元に戻す|ScreenUpdating等の後片付け にまとめています。

止めたときもフォームは残らない

エラーのダイアログで「終了」を押したとき、「デバッグ」からVBEのリセットを押したとき、どちらもフォームは残りませんでした(UserForms.Count は0)。エラーのダイアログが出ている間は、フォームは画面に見えています。

Escキーでの中断は、今回の待つだけの処理では効きませんでした。マクロを終了させたあとは、フォームは残っていません。

まとめ

  • .Show vbModeless で、フォームを出したままマクロを進められます
  • 文字を出すには、書き換えたあとに Repaint(または DoEvents)を呼びます。毎回ではなく、一定の件数ごとに呼べば、時間はほとんど増えません
  • Unload で指定は消えるので、開くたびにコードで指定し直します
  • 後片付けは Cleanup に集めて、正常終了でもエラーでも Unload します

コメント

タイトルとURLをコピーしました