SSブログ

VB.NET でスレッドセーフで非同期処理からフォームのコントロールを更新する方法 [プログラミング]

「Windowsフォームアプリケーション」でマルチスレッドを実装した場合、フォームと異なるスレッドで実行している非同期処理からフォームのコントロールのプロパティを更新したいことがある。
しかし、フォームと異なるスレッドから「TextBox.Text = "aaaa"」のようなプロパティの更新は行えないため、スレッドセーフで更新を行うためのメソッドが必要となる。

以下の例ではフォームの「実行」ボタンを押下すると非同期デリゲートを利用した非同期処理が別スレッドで実行され、フォームのテキストボックスに処理の経過を出力する。
その際、ユーザの誤操作を防止するために「実行」ボタンを非活性にすると共に、フォームの「閉じる」ボタンおよびコントロールメニューからの「閉じる」操作が行われてもフォームを閉じることができないように対策している。

簡単にコードの説明をしよう。(非同期デリゲートを利用した非同期処理に関するコードは別途説明済みなので割愛する。)

・コールバック関数の宣言
コードの先頭に宣言されている"Delegate"ステートメントは、ボタンのEnabledプロパティ、フォームのTagプロパティ、テキストボックスのTextプロパティをスレッドセーフで更新するためのコールバック関数で、これらの関数は各々の更新を行うメソッドで利用する。

・コントロールをスレッドセーフで更新するためのメソッド
"subButtonEnabledSet"、"subFormTagSet"、"subTextBoxTextSet"がそのためのメソッドで対象は各々異なるが処理内容は変わらない。"subButtonEnabledSet"メソッドを例にとって説明する。
1.このメソッドの引数には"[Button]"と"[Value]"が宣言されており、この引数は"Delegate"ステートメントに宣言したコールバック関数の引数と一致している必要がある。一つ目の"[Button]"には更新の対象となるボタンコントロールのインスタンスを、二つ目の"[Value]"にはEnabledプロパティに設定する値を受け取る引数を指定する。
2.引数で受け取った"[Button]"をそのまま利用しても問題ないが、メソッド内に宣言した"oControl"に引数の"[Button]"を代入する。
3.引数で受け取った"[Button]"コントロールをスレッドセーフで更新する必要があるか否かをInvokeRequiredプロパティで確認する。この値がTrueならばこのコントロールは別スレッドで作成されたコントロールであるためInvokeメソッドを使用してスレッドセーフで更新する必要がある。但し、コントロールのハンドルがまだ作成されていない場合はInvokeRequiredプロパティはFalseを返す可能性があるため、その場合はコールバック関数を呼び出す必要はない。
4.スレッドセーフで更新する必要があると判断された場合、コールバック関数のインスタンスを生成して引数で受け取った"[Button]"コントロールのInvokeメソッドの呼び出しに利用する。
5.スレッドセーフで更新する必要がないと判断された場合は、引数で受け取った"[Button]"コントロールのEnabledプロパティに引数で受け取った"[Value]"を設定すれば良い。

・「実行」ボタンを押下時の処理
1.非同期デリゲートのインスタンスを生成して非同期処理"fncThread"を実行する。
2.非同期処理の起動が成功したらフォームのTagプロパティに「実行中」の文字列を設定し、「実行」ボタンのEnabledプロパティにFalseを設定してボタンを非活性にする。ここでフォームのTagプロパティに「実行中」の文字列を設定するのは非同期処理中にフォームが閉じられるのを防止するためで設定する値は何でも良い。また、ボタンを非活性にするのは非同期処理の実行中に再度「実行」ボタンが押下されるのを防止するためである。

・非同期処理("fncThread")
ここでは非同期処理が起動されてから10秒間、フォームのテキストボックスに処理経過を出力する。
1.現在日時に10秒を加算した日時の変数を生成する。
2.現在時刻が上記1で生成した日時未満の間、フォームのテキストボックスの値を取得し「非同期処理中...[現在時刻]("hh:mm:ss"形式)」の文字列を追加してフォームのテキストボックスを更新する。その際、テキストボックスの値の取得時は通常通り「strValue = txtResult.Text」で問題ないが、更新時は"subTextBoxTextSet"メソッドを呼び出してスレッドセーフで処理を行う。尚、今回は値の取得を行うコントロールがテキストボックスであるため取得時に特に考慮をしていないが、対象のコントロールによってはプロパティの参照時もスレッドセーフを考慮する必要がある。

・非同期処理のコールバック関数の処理
ここでは非同期処理の終了を検出して非同期処理が終了したことを通知すると共にフォームのTagプロパティとボタンのEnabledプロパティの更新を各々スレッドセーフで行う。

・非同期処理実行中にフォームが閉じられるのを防止する
フォームのFormClosingイベントでユーザがフォームを閉じようとしている操作をキャッチし、非同期処理実行中の場合はフォームを閉じないようにする。
フォームのFormClosingイベントで引数に渡されるFormClosingEventArgsのCloseReasonプロパティを確認するとフォームが閉じられる理由が判る。この値がCloseReason.UserClosingであればユーザがフォームを閉じようとしているため、フォームのTagプロパティに「実行中」の文字列が設定されていれば非同期処理の実行中なのでFormClosingEventArgsのCancelプロパティにTrueを設定してフォームのクローズイベントをキャンセルする。


以下の例を実行するには、新規の「Windowsフォームアプリケーション」を作成しフォームのデザインで"Button"と"TextBox"コントロールをフォームに配置して、コードを"Form1"に貼り付けて実行すれば良い。
尚、"Button"コントロールのNameプロパティには"btnExec"をTextプロパティには"実行"を設定し、"TextBox"コントロールのMultilineプロパティにはTrueをNameプロパティには"txtResult"をScrollBarsプロパティにはVerticalを設定する。
プログラムを実行するとフォームが表示されるので、「実行」ボタンを押下すると非同期処理が起動してテキストボックスに処理経過が表示される。非同期処理の実行中にフォームの「閉じる」ボタンを押下しても「処理実行中にウインドウを閉じることはできません。」のメッセージボックスが表示されフォームを閉じることはできない。

今回はスレッドセーフでフォームのコントロールのプロパティを更新する方法を記載したが、同様の方法でメソッドを実行することも可能である。(例えばフォームのCloseメソッドなど…。)
また、フォームのコントロールのプロパティの値を取得する場合もほぼ同様の方法で可能だ。但し、その場合は取得した値を戻り値として戻すためSubプロシージャではなくFunctionプロシージャとする必要がある。

------------------------------------------------------------

Public Class Form1

    'スレッドセーフのコールバック関数
    Delegate Sub subButtonEnabledSetCallBack( _
                         ByVal [Button] As Button, _
                         ByVal [Value] As Boolean)
    Delegate Sub subFormTagSetCallBack( _
                         ByVal [Value] As String)
    Delegate Sub subTextBoxTextSetCallBack( _
                         ByVal [TextBox] As TextBox, _
                         ByVal [Text] As String)

    '非同期処理のデリゲート宣言
    Delegate Function Thread1Delegate() As Boolean

    'ユーザ定義変数
    '非同期処理デリゲートインスタンス
    Private gobjThread1Delegate As Thread1Delegate

    '非同期処理戻り値
    Private gobjThread1Return As IAsyncResult

    Private Sub Form1_FormClosing(sender As Object, _
             e As System.Windows.Forms.FormClosingEventArgs) _
             Handles Me.FormClosing
        '*************************
        'フォームが閉じられる時の処理
        '*************************

        Select Case e.CloseReason
            Case CloseReason.UserClosing
                'コントロールメニューから閉じるを選択
                If Me.Tag.ToString = "実行中" Then
                    '処理実行中の場合、終了操作をキャンセル
                    MsgBox("処理実行中にウインドウを" & _
                           "閉じることはできません。", _
                           MsgBoxStyle.Critical)
                    e.Cancel = True
                    Exit Sub
                End If
        End Select

    End Sub

    Private Sub Form1_Load(sender As Object, _
             e As System.EventArgs) Handles Me.Load
        '*************************
        'フォームロード時の処理
        '*************************

        Me.Tag = ""

    End Sub

    Private Sub btnExec_Click(sender As Object, _
             e As System.EventArgs) Handles btnExec.Click
        '*************************
        '実行ボタン押下時の処理
        '*************************

        'テキストボックスの初期化
        txtResult.Text = "非同期処理を開始..." & _
                         ControlChars.CrLf

        Try
            '非同期処理の開始
            '非同期処理①のデリゲートインスタンスを生成
            gobjThread1Delegate = New Thread1Delegate( _
                                  AddressOf fncThread)
            '非同期処理①の非同期実行を開始
            gobjThread1Return = _
                         gobjThread1Delegate.BeginInvoke( _
                         New AsyncCallback( _
                         AddressOf subThreadCallback), _
                         Nothing)

            'フォームのTagプロパティを設定
            Me.Tag = "実行中"
            'ボタンの動作設定
            btnExec.Enabled = False

        Catch ex As Exception
            '非同期処理の実行に失敗した場合
            MsgBox(ex.Message, MsgBoxStyle.Exclamation)

        End Try

    End Sub

    Private Function fncThread() As Boolean
        '*************************
        '非同期処理
        '*************************

        Dim blnReturn As Boolean

        Try
            '一定間隔でフォームのテキストボックスに経過を出力
            Dim dTime As Date = Now.AddSeconds(10)
            Do While dTime > Now
                'テキストボックスのTextプロパティを取得
                Dim strValue As String = txtResult.Text
                If strValue <> "" Then
                    '記入済ならば改行文字を追加
                    strValue = strValue & ControlChars.CrLf
                End If
                strValue = strValue & "非同期処理中..." & _
                           Now.ToString("hh:mm:ss")
                'スレッドセーフでテキストボックスの
                'Textプロパティを更新
                Call subTextBoxTextSet(txtResult, strValue)
                '1秒待機
                System.Threading.Thread.Sleep(1000)
            Loop
            '非同期処理の戻り値を設定
            blnReturn = True

        Catch ex As Exception
            'エラーが発生した場合
            MsgBox(ex.Message, MsgBoxStyle.Exclamation)
            blnReturn = False

        End Try

        Return blnReturn

    End Function

    Public Sub subThreadCallback(ByVal ar As IAsyncResult)
        '*************************
        '非同期処理のコールバック関数
        '
        '   ar  :   非同期処理情報インターフェース
        '
        '*************************

        Try
            Do
                If gobjThread1Return.IsCompleted = True Then
                    '非同期処理①が完了した場合
                    Dim blnReturn As Boolean = _
                              gobjThread1Delegate.EndInvoke( _
                              gobjThread1Return)
                    If blnReturn = False Then
                        '異常終了メッセージを表示
                        MsgBox("非同期処理で異常を検出" & _
                               "しました。", _
                               MsgBoxStyle.Exclamation)
                    End If
                    '非同期処理のデリゲートインスタンスを破棄
                    gobjThread1Delegate = Nothing
                    '非同期処理の戻り値インスタンスを破棄
                    gobjThread1Return = Nothing

                    'テキストボックスのTextプロパティを取得
                    Dim strValue As String = txtResult.Text
                    If strValue <> "" Then
                        '記入済ならば改行文字を追加
                        strValue = strValue & _
                                   ControlChars.CrLf
                    End If
                    strValue = strValue & _
                               ControlChars.CrLf & _
                               "非同期処理を終了..."
                    'スレッドセーフでテキストボックスの
                    'Textプロパティを更新
                    Call subTextBoxTextSet(txtResult, _
                                           strValue)
                    'スレッドセーフでフォームの
                    'Tagプロパティを更新
                    Call subFormTagSet("")
                    'スレッドセーフでボタンの
                    'Enabledプロパティを更新
                    Call subButtonEnabledSet(btnExec, True)
                    Exit Do
                Else
                    '非同期処理が実行中の場合
                    System.Threading.Thread.Sleep(250)
                End If
            Loop

        Catch ex As Exception
            MsgBox(ex.Message, MsgBoxStyle.Exclamation)

        End Try

    End Sub

    Public Sub subButtonEnabledSet(ByVal [Button] As Button, _
                                   ByVal [Value] As Boolean)
        '*************************
        'ボタンのEnabledプロパティを設定
        '
        '   Button          :   設定するボタンのインスタンス
        '   Value           :   設定するEnabledプロパティの値
        '
        '*************************

        Dim oControl As Button = [Button]

        'スレッドセーフで実行する必要があるか否かをチェック
        If oControl.InvokeRequired = True Then
            'スレッドセーフで実行する必要がある場合
            Dim d As New subButtonEnabledSetCallBack( _
                         AddressOf subButtonEnabledSet)
            oControl.Invoke(d, New Object() {[Button], _
                                             [Value]})
        Else
            'スレッドセーフで実行する必要がない場合
            oControl.Enabled = [Value]
        End If

    End Sub

    Public Sub subFormTagSet(ByVal [Value] As String)
        '*************************
        'フォームのタグを設定
        '
        '   Value           :   タグに設定する値
        '
        '*************************

        'スレッドセーフで実行する必要があるか否かをチェック
        If Me.InvokeRequired = True Then
            'スレッドセーフで実行する必要がある場合
            Dim d As New subFormTagSetCallback( _
                         AddressOf subFormTagSet)
            Me.Invoke(d, New Object() {[Value]})
        Else
            'スレッドセーフで実行する必要がない場合
            Me.Tag = [Value]
        End If

    End Sub

    Public Sub subTextBoxTextSet(ByVal [TextBox] As TextBox, _
                                 ByVal [Text] As String)
        '*************************
        'テキストボックスのTextプロパティを設定
        '
        '   TextBox         :   設定するテキストボックスの
        '                       インスタンス
        '   Text            :   設定するTextプロパティの値
        '
        '*************************

        Dim oControl As TextBox = [TextBox]

        'スレッドセーフで実行する必要があるか否かをチェック
        If oControl.InvokeRequired = True Then
            'スレッドセーフで実行する必要がある場合
            Dim d As New subTextBoxTextSetCallBack( _
                         AddressOf subTextBoxTextSet)
            oControl.Invoke(d, New Object() {[TextBox], _
                                             [Text]})
        Else
            'スレッドセーフで実行する必要がない場合
            oControl.Text = [Text]
        End If

    End Sub

End Class

------------------------------------------------------------


タグ:VB.NET
nice!(0)  コメント(0)  トラックバック(0) 

nice! 0

コメント 0

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

トラックバック 0

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。