【実務・中級編】Windows FormsのProgressBarを用いた不確定プロパティ(Marquee)のスマートな制御:処理時間が予測できないバッチ処理中のUI演出手法 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク
スポンサーリンク

「動いているのか、死んでいるのか」をユーザーに悟らせない——ProgressBar Marquee制御の極意

業務自動化ツールの開発において、我々エンジニアが直面する最も卑俗かつ、最もユーザー体験(UX)を損なう問題がある。それは、「処理に時間がかかること自体」ではなく、「処理中、アプリがフリーズしたように見えること」だ。

進捗率(%)が算出できない、例えば「数万件のDBレコードをスキャンして条件に合うものを抽出する」「ネットワーク越しに巨大なファイル群を走査する」といった不確定なバッチ処理において、プログレスバーは唯一の「生命維持装置」となる。

しかし、多くの開発者はここで致命的なミスを犯す。UIスレッドをブロックし、プログレスバーの更新すら止めてしまうのだ。本稿では、Windows Formsにおける`ProgressBar.Style = ProgressBarStyle.Marquee`を、単なる「動く棒」から「信頼の証」へと昇華させるための、モダンかつ堅牢な設計手法を伝授する。

1. 悪手としての `Application.DoEvents()` —— なぜそれを使ってはいけないのか

まず、初心者が陥りがちな「禁忌」について触れておこう。
「処理の途中で `Application.DoEvents()` を呼び出せば、UIが更新されてプログレスバーも動く」という教えだ。

断じて、これに従ってはならない。

`DoEvents` は、メッセージキューにある未処理のイベントを強制的に処理させる手法だが、これは「再入可能性(Re-entrancy)」という地獄を招く。ボタンをクリックした直後の処理中に、ユーザーが再び同じボタンを連打した場合、同じメソッドが多重にスタックされ、変数の状態が破壊されたり、リソースの競合が発生したりする。

プロフェッショナルは、UIスレッドを「無理やり動かす」のではなく、「UIスレッドを解放し、重い処理を別スレッドに逃がす」という設計を選択すべきだ。

2. アーキテクチャ設計:Task-based Asynchronous Pattern (TAP)

現代の .NET 開発において、バックグラウンド処理のスタンダードは `Task` を用いた非同期プログラミングである。`BackgroundWorker` も存在するが、より柔軟で、例外処理や継続処理(Continuation)の記述に優れる `Async/Await` を採用するのが、設計上の正解である。

設計の三原則

1. UIスレッドの解放: 重いI/O(ファイル・DB)は必ず `Task.Run` でバックグラウンドへ投げる。
2. Marqueeのライフサイクル管理: 処理開始時に `Marquee` モードへ切り替え、終了時(成功・失敗問わず)に必ず元の状態へ戻す。
3. 例外の捕捉: バックグラウンドスレッドで発生した例外は、UIスレッドで適切にキャッチし、ユーザーに通知しなければならない。

3. プロダクション級の実装コード

以下に、そのまま実務に投入可能な、堅牢な実装例を示す。ここでは、ファイル探索という「進捗が予測できない」処理を想定している。

Imports System.Threading.Tasks
Imports System.IO

''' <summary>
''' 業務自動化ツールにおける、非同期バッチ処理の模範的実装
''' </summary>
Public Class AsyncBatchForm
    ' UIコントロールへのアクセスを安全に行うため、コントロールはプロパティとして保持
    ' (実際にはデザイナで配置)

    ''' <summary>
    ''' 実行ボタンのクリックイベント
    ''' </summary>
    Private Async Sub btnExecute_Click(sender As Object, e As EventArgs) Handles btnExecute.Click
        ' 1. UIの状態を「実行中」にロックする(二重押し防止)
        SetUiState(isProcessing:=True)
        
        ' 2. ProgressBarを不確定モード(Marquee)に設定
        ' これにより、進捗率が不明でも「動いていること」を視覚的に示せる
        ProgressBar1.Style = ProgressBarStyle.Marquee
        ProgressBar1.MarqueeAnimationSpeed = 30 ' アニメーション速度の調整

        Try
            ' 3. 重い処理をバックグラウンドスレッドで実行
            ' Awaitを使うことで、UIスレッドをブロックせずに完了を待機できる
            Dim result As String = Await Task.Run(Function() PerformHeavyWork())

            ' 4. 成功時の処理
            MessageBox.Show($"処理が完了しました。結果: {result}", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information)

        Catch ex As IOException
            ' ファイル/ネットワーク関連の例外
            MessageBox.Show($"ファイルアクセス中にエラーが発生しました: {ex.Message}", "I/Oエラー", MessageBoxButtons.OK, MessageBoxIcon.Error)
        Catch ex As Exception
            ' その他の予期せぬ例外
            MessageBox.Show($"予期せぬエラーが発生しました: {ex.Message}", "致命的エラー", MessageBoxButtons.OK, MessageBoxIcon.Error)
        Finally
            ' 5. 【極めて重要】成功・失敗に関わらず、必ずUIを元の状態に戻す
            ' これを忘れると、エラー時にアプリが「実行中モード」のまま固まる
            ProgressBar1.Style = ProgressBarStyle.Continuous
            ProgressBar1.Value = 0
            SetUiState(isProcessing:=False)
        End Try
    End Sub

    ''' <summary>
    ''' 実際の重いロジック(バックグラウンドで動作)
    ''' </summary>
    ''' <returns>処理結果の文字列</returns>
    Private Function PerformHeavyWork() As String
        ' ここでは、進捗が予測できないファイル走査をシミュレート
        ' 実際にはここでDBアクセスやファイルI/Oを行う
        
        ' 注意: このメソッド内では、UIコントロール(ProgressBarなど)を直接操作してはいけない
        ' それは「クロススレッド操作」の例外を引き起こす原因となる
        
        Dim dummyPath As String = "C:\Windows\System32" ' 例としてSystem32を走査
        Dim fileCount As Integer = 0

        ' 重い処理のシミュレーション
        For Each file In Directory.EnumerateFiles(dummyPath, "*.*", SearchOption.AllDirectories)
            ' 擬似的な負荷
            Threading.Thread.Sleep(10) 
            fileCount += 1
            
            ' 途中でエラーが発生するシナリオ(テスト用)
            ' If fileCount > 100 Then Throw New IOException("アクセス拒否されました。")
        Next

        Return $"{fileCount} 個のファイルを処理しました。"
    End Function

    ''' <summary>
    ''' UIの有効/無効を切り替える(再入防止)
    ''' </summary>
    Private Sub SetUiState(isProcessing As Boolean)
        btnExecute.Enabled = Not isProcessing
        txtLog.ReadOnly = isProcessing
        ' その他、キャンセルボタンの有効化などの制御を行う
    End Sub

End Class

4. チーフアーキテクトの視点:実務における落とし穴

コードを書くだけでは不十分だ。以下の「設計上の重み」を理解していなければ、真のプロフェッショナルとは呼べない。

① ファイル・DB連携時の「ゾンビ・プロセス」問題

`Task.Run` で実行している最中に、ユーザーが強制終了(タスクマネージャー等)したり、ネットワークが切断されたりした場合、バックグラウンドの処理が「ゾンビ」として残り、ファイルハンドルやDB接続を掴み続けることがある。
対策: `CancellationTokenSource` を導入し、処理に「中断の権利」を与える設計を常に検討せよ。

② クロススレッド操作の回避

上記のコードで、`PerformHeavyWork` 内で `ProgressBar1.Value = 10` と書いた瞬間、アプリはクラッシュする。バックグラウンドスレッドからUIを触ることは禁忌だ。進捗を伝えたい場合は、`IProgress(Of T)` インターフェースを使用し、UIスレッドへの「報告」という形を取るのが洗練された作法である。

③ エラーハンドリングの階層化

`Try…Catch` を `PerformHeavyWork` の中に入れるべきか、呼び出し側の `btnExecute_Click` に入れるべきか?
答えは、「回復可能なエラーは内部で、回復不能な致命的エラーは外部(UI側)で」だ。DBのタイムアウトなどは内部でリトライを試みるべきだが、アクセス権限エラーなどは即座にUIへ通知し、ユーザーに判断を仰ぐべきである。

結びに代えて

ProgressBarの `Marquee` は、単なるアニメーションではない。それは、「我々のプログラムは、あなたの命令を忘れていない。ただ、今一生懸命に戦っている最中なのだ」という、ユーザーに対する誠実なメッセージである。

`DoEvents` という安易な魔法に頼らず、`Async/Await` による正攻法の非同期設計を貫くこと。その一歩が、バグの起きない、堅牢で、プロフェッショナルな業務アプリケーションを形作るのだ。

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