VB のたまご

作成日: 2015/12/19, 更新日: 2015/12/19


別スレッドとか気にしないで、コントロールの設定を変更したい

コントロールの設定を変えるためには、Invoke 経由で。

  •  通常、スレッドを気にしない場合は、簡単にコントロールの設定を変更することができます。 例えば、TextBox の Text プロパティに文字列をセットする、ボタンをグレーアウトにして、誤クリックできないようにする等です。 (この場合、 UI スレッド上で操作しているということです。)

  •  しかし、バックグラウンドとか別スレッド経由で呼び出したメソッド内では、コントロールの設定を変更しようとすると、 例外エラーが発生してしまいます。( InvalidOperationException )。 これは、コントロールを安全に使うための仕様なのですが、もうそういうルールだからしょーがない、と思うしかないわけです。 でもねー、アクセスの仕方を Invoke メソッド経由で書くと、見づらいんだなー。これが。

  • ' 画面上に、ボタンと BackgroundWorker を貼っています。
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Me.BackgroundWorker1.RunWorkerAsync()
    End Sub
    
    Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
    
        'Me.Button1.Text = "hello" ' ←ダメー。って言われる。
        Me.Invoke(Sub() Me.Button1.Text = "hello") ' Invoke 経由で頑張ってますよ感が出ててイヤ。
    
    End Sub
    
  •  こんな感じ。なんかねー、いや、いいっちゃいいんだけど、なんかしっくり来ないのですよ。 ただ単純に、コントロールにアクセスしたいだけなのに。でも大丈夫。そんな時は、拡張メソッドでちょちょっと調整です。

スポンサーリンク


みんな平等。みんな仲良し。

  •  それではいつものとおり、サンプルを先に見てみましょう。

  • Imports System.Runtime.CompilerServices
    
    Module ControlExtensions
    
        <Extension()>
        Public Sub SetText(target As Control, value As String)
    
            If Not target.IsHandleCreated Then
                Exit Sub
            End If
    
            Dim ac As New Action(Of String)(Sub(v) target.Text = v)
            If target.InvokeRequired Then
                target.Invoke(ac, New Object() {value})
            Else
                ac(value)
            End If
    
        End Sub
    
    End Module
    
    ' 画面上に、ボタンと BackgroundWorker を貼っています。
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Me.Button1.SetText("hello1")
        Me.BackgroundWorker1.RunWorkerAsync()
    End Sub
    
    Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
        Me.Button1.SetText("hello2") ' 単純にボタンの設定変えてますよ。っていう感じなので良い感じ。
    End Sub
    
  •  うん、これならまだ良いかも。パッと見て、コントロールにアクセスしているのが分かります。

  • スポンサーリンク


  •  それでは作り方です。拡張メソッドには、2つの命令とジェネリックデリゲートを準備しています。 ジェネリックデリゲートにはラムダ式をセットしていて、メソッドの役割を担当します。 そして、このコントロールにアクセスしたいんだけど、今って別スレッド上だっけ?普段のスレッド上だっけ?と確認する役割を、 InvokeRequired プロパティが担当します。 別スレッド上だと True を返すので、その場合は、Invoke メソッド経由でコントロールにアクセスしますし、 違う場合は、普段通りにコントロールにアクセスします。

  •  後回しにしておいたこちら、一番最初に IsHandleCreated プロパティを確認しています。 直訳すると、ハンドルが作られているかどうかの確認、ですね。 これは簡単に言うと、相手側の事情に合わせる、という気づかいをしています。 別スレッド側が実行中の間、その中で重たい処理をしていても、画面側は応答なしになりませんよね。それは、別々に動作しているからです。 別々に動作しているということは、どちらも、どちらかのことを気にしていない、ということです。

  • Imports System.Threading.Thread
    
    Private Sub Form1_Disposed(sender As Object, e As EventArgs) Handles Me.Disposed
        Try
            Sleep(100)
            Button1.Invoke(Sub() Me.Button1.Text = "hello")
        Catch ex As Exception
            Console.WriteLine(ex.ToString())
        End Try
    End Sub
    
    ' 出力結果
    System.InvalidOperationException: ウィンドウ ハンドルが作成される前、コントロールで Invoke または BeginInvoke を呼び出せません。
       場所 System.Windows.Forms.Control.MarshaledInvoke(Control caller, Delegate method, Object[] args, Boolean synchronous)
       場所 System.Windows.Forms.Control.Invoke(Delegate method, Object[] args)
       場所 System.Windows.Forms.Control.Invoke(Delegate method)
    
  •  そういう関係だと発生してしまう問題が、このサンプルです。 わざと発生するように意図的にイベントを書いていますが、書かなくても、このようなことが操作のタイミングによっては、自然に発生する可能性があるんです。 実際に陥る流れはこうです。別スレッドが動作していて、コントロールにアクセスしようとします。 画面側は、まだ起動の準備中でした、または破棄している最中でした、という場合があり、 この時、ウィンドウハンドルというプログラムに紐づく ID みたいなものが準備できていない、または破棄されてしまった、という状態になっています。 要するに、画面側では、やり取りすることができない状態だということです。 それなのに、ノックもしないで入ってくる、みたいに、コントロールにアクセスしようとすると、あーダメだっていってるのにー、うわー。とぽしゃってしまうわけです。 相手を思いやるって大事ですね。これが、IsHandleCreated プロパティが担当している役割となります。

  • ' 画面上に、ボタンと BackgroundWorker を貼っています。
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Me.Button1.SetText("hello1")
        Me.BackgroundWorker1.RunWorkerAsync()
    End Sub
    
    Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
        Me.Button1.SetText("hello2") ' 単純にボタンの設定変えてますよ。っていう感じなので良い感じ。
    End Sub
    
  •  ということで、戻りますが、このようにして、別スレッドだ何だといちいち気にせずに、コントロールの設定を変更することができるようになりました。 よかったよかった。

  • 参考にさせていただいた記事
  • いげ太さんの記事


  •  最後までこの記事を読んでいただき、ありがとうございました。


スポンサーリンク