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

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

不確定プログレスバーのパラドックス:メッセージループを掌握し、UIの「死」を防ぐ

業務アプリケーションの設計において、最も低俗ながら、最もユーザーの信頼を左右する要素がある。それは「進捗の表現」だ。

特に、データ移行、大規模なファイル解析、あるいは外部APIとの同期といった、完了までの時間が予測不能なバッチ処理において、我々は `ProgressBar` の `Marquee` スタイルに頼ることになる。しかし、多くの開発者は致命的な誤解をしている。

「Marqueeをセットすれば、動いているように見える」のではない。
「メッセージループが生存している限り、動いて見える」のだ。

本稿では、Windows Formsにおける `ProgressBar.Style = ProgressBarStyle.Marquee` の挙動の真実を解き明かし、UIスレッドを凍結(フリーズ)させることなく、かつリソースを枯渇させない、プロフェッショナルな非同期制御の実装手法を伝授する。

1. なぜ Marquee は「止まる」のか:メッセージループの飢餓

まず、メカニズムを理解せよ。`ProgressBar` の Marquee アニメーションは、ハードウェアによる独立した描画プロセスではない。Windows のメッセージループ(`GetMessage` / `DispatchMessage`)を通じて、システムから送られてくる `WM_PAINT` メッセージを、UIスレッドが適切に処理することで初めて成立する「擬似的なアニメーション」である。

もし、あなたが重い計算処理や I/O 待ちを UIスレッド上で直接実行した場合、メッセージループは「処理」に占有され、「描画(Paint)」のメッセージを処理する余裕を失う。これが、ユーザーが目にする「プログレスバーが止まったままの、応答なしの画面」の正体だ。

これを回避するには、「重い処理」と「UIの描画」を、物理的に異なるコンテキスト(スレッド)へ完全に隔離しなければならない。

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

レガシーな `BackgroundWorker` を持ち出すのは容易だが、現代の .NET においては `Task` クラスを用いた非同期パターン(TAP)こそが、リソース管理と例外処理の観点から最適解である。

ここで重要なのは、単に `Task.Run` を使うことではない。「UIスレッドへのコンテキスト回帰」と「キャンセル操作の伝搬」を、いかにエレガントに設計するかである。

実装の要件

1. 非同期実行: 重い処理は `Task.Run` によりスレッドプールへ委譲する。
2. UIの制御: プログレスバーの表示・非表示、および状態変更は、必ず UI スレッドから行う。
3. キャンセル制御: `CancellationTokenSource` を用い、ユーザーが中断を要求した際に、即座にリソースを解放する経路を確保する。
4. 例外の捕捉: 非同期スレッド内で発生した例外は、UIスレッドへ安全に伝播させ、アプリケーションのクラッシュを防ぐ。

3. 極限の実装例:`AsyncMarqueeController`

以下に、私が設計現場で推奨する、堅牢なコントロールパターンのコードを示す。これは単なるコード例ではない。メモリリークとデッドロックを回避するための「防壁」である。

Imports System.Threading
Imports System.Threading.Tasks

''' <summary>
''' 業務アプリケーションにおける、不確定進捗制御のマスタークラス。
''' </summary>
Public Class AsyncMarqueeController
    Private WithEvents _cts As CancellationTokenSource
    Private ReadOnly _uiThreadContext As SynchronizationContext

    ''' <summary>
    ''' コンストラクタ。現在のUIコンテキストを捕捉する。
    ''' </summary>
    Public Sub New()
        ' UIスレッドでの処理を保証するためのコンテキストを保持
        _uiThreadContext = SynchronizationContext.Current
        If _uiThreadContext Is Nothing Then
            Throw New InvalidOperationException("このクラスはUIスレッドから初期化される必要があります。")
        End If
    End Sub

    ''' <summary>
    ''' 非同期バッチ処理を実行する。
    ''' </summary>
    ''' <param name="progressBar">制御対象のProgressBar</param>
    ''' <param name="batchProcess">実行する重い処理(非同期関数)</param>
    Public Async Function ExecuteAsync(progressBar As ProgressBar, batchProcess As Func(Of CancellationToken, Task)) As Task
        ' 以前の実行があればキャンセルを試みる(二重起動防止のロジックは呼び出し側で制御すること)
        _cts = New CancellationTokenSource()
        Dim token = _cts.Token

        Try
            ' 1. UIの準備: Marqueeスタイルを有効化
            progressBar.Style = ProgressBarStyle.Marquee
            progressBar.MarqueeAnimationSpeed = 30 ' スピードの微調整

            ' 2. 重い処理をスレッドプールへ投げる
            ' 注意: batchProcess は UI スレッドをブロックしてはならない
            Await Task.Run(Async Function()
                               Await batchProcess(token)
                           End Function, token)

        Catch ex As OperationCanceledException
            ' ユーザーによるキャンセル時の処理
            Debug.WriteLine("処理がユーザーによって中断されました。")
        Catch ex As Exception
            ' 致命的なエラーのハンドリング
            ' ここでログ出力やエラーダイアログの表示を行う(UIスレッドへの回帰が必要)
            Await ShowErrorAsync(ex.Message)
        Finally
            ' 3. 後処理: 確実にUIを元の状態に戻し、リソースを解放する
            ' Finallyブロックは、例外が発生しても必ず実行される
            progressBar.Style = ProgressBarStyle.Blocks
            _cts.Dispose()
            _cts = Nothing
        End Try
    End Function

    ''' <summary>
    ''' キャンセル命令を発行する。
    ''' </summary>
    Public Sub Cancel()
        _cts?.Cancel()
    End Sub

    ''' <summary>
    ''' エラー表示をUIスレッドで行う。
    ''' </summary>
    Private Async Function ShowErrorAsync(message As String) As Task
        ' SynchronizationContextを利用してUIスレッドへ回帰
        _uiThreadContext.Post(Sub()
                                  MessageBox.Show($"致命的なエラーが発生しました: {vbCrLf}{message}", 
                                                  "システムエラー", MessageBoxButtons.OK, MessageBoxIcon.Error)
                              End Sub, Nothing)
        Await Task.CompletedTask
    End Function
End Class

実装におけるアーキテクトの視点

# 1. `SynchronizationContext` の重要性

`Task.Run` で起動したスレッドは、UIスレッドとは無関係なスレッドプールスレッドである。そこから直接 `ProgressBar.Style` を変更しようとすれば、`InvalidOperationException`(クロススレッド操作)が投げられる。`SynchronizationContext.Post` または `Await` によるコンテキストの自動回帰を利用することで、この問題をエレガントに解決している。

# 2. `CancellationToken` による「即時停止」

レガシーなシステムでは、重いループの中で `Application.DoEvents()` を呼び出してフリーズを防ごうとする愚かな設計が見られる。これは再入(Re-entrancy)問題を引き起こし、スタックオーバーフローや予期せぬ状態遷移の温床となる。
我々は `DoEvents` には頼らない。代わりに、処理の各ステップで `token.ThrowIfCancellationRequested()` を呼び出し、スレッドを安全に、かつ確実に終了させる。

# 3. メモリとライフサイクルの管理

`CancellationTokenSource` は `IDisposable` である。これを適切に `Dispose` しないことは、メモリリーク、ひいてはハンドルリークを招く。`Finally` ブロックでの明示的な解放は、長時間稼働するサーバーサイドのバッチや、頻繁に呼び出されるクライアントアプリにおいて、システムの安定性を担保するための鉄則である。

4. 結論:ユーザーとの「信頼」を設計せよ

プログレスバーのアニメーションは、単なる装飾ではない。それは、システムが「生きている」ことをユーザーに伝えるための、非言語的な通信プロトコルである。

Marqueeが止まるということは、通信が途絶えたことを意味する。通信が途絶えたシステムは、ユーザーから「死んだ」と判断される。シニアエンジニアとして、我々が追求すべきは、コードの短さではなく、ユーザーがシステムに対して抱く「安心感」の設計である。

この知見を、あなたのアーキテクチャに組み込んでほしい。

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