Windowsフォームでスレッドを作成した場合、フォームやフォーム上のコントロールに対しては、そのスレッドからの操作(フォームやコントロールが持つメソッドの呼び出しやプロパティの読み書き)は動作が保証されない。本稿ではそのような処理を<安全>に行うためのプログラミングについて解説する。
フォーカスの移動を行うサンプル・プログラム
例えば、Visual Studio .NET(以降、VS.NET)でWindowsアプリケーションのプロジェクトを新規作成し、次の画面のように、2つのテキストボックス(TextBox1とTextBox2)と1つのボタン(Button1)を配置したとする。
このアプリケーションを実行すると、起動時にはフォーカスはTextBox1に設定される。次に、ボタンがクリックされたときに、TextBox2にフォーカスが設定されるようにしてみよう。
これにはVS.NET上で配置したボタンをダブルクリックし、それにより生成されたイベント・ハンドラに次のようなコードを記述すればよい。
private void button1_Click(object sender, System.EventArgs e)
{
textBox2.Focus();
}
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
TextBox2.Focus()
End Sub
Focusメソッドはそのコントロールにフォーカスを設定するためのものだ。当然ながら、このコードは正しく実行される。
別スレッドからコントロールを操作する誤った例
次に、ボタンがクリックされたときにスレッドを起動し、そのスレッドでFocusメソッドを呼び出してみる。このコードは以下のようになる。
private void button1_Click(object sender, System.EventArgs e)
{
Thread t = new Thread(new ThreadStart(worker));
t.Start();
}
void worker()
{
textBox2.Focus(); // 誤った呼び出し
}
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim t As New Thread(New ThreadStart(AddressOf worker))
t.Start()
End Sub
Sub worker()
TextBox2.Focus() ' 誤った呼び出し
End Sub
プログラムの先頭でSystem.Threading名前空間を参照する必要がある。
このコードでは、button1_Clickメソッドはコントロールが作成されたスレッド(以下、メイン・スレッド)で実行されるのに対して、workerメソッドは別スレッドで実行される。このため、workerメソッド内で呼び出しているTextBox2のFocusメソッドの実行は、その動作が保証されない。実際にプログラムを実行しボタンをクリックしてもフォーカスは移動しないはずだ。
Invokeメソッドによるメソッドの呼び出し
Controlクラス(System.Windows.Forms名前空間)には、別スレッドからコントロールを操作する場合に使用できるInvokeメソッドが用意されている(フォームを始めとするすべてのコントロールはこのメソッドを継承している)。Invokeメソッドを使うと、コントロールに対する操作をメイン・スレッドで実行させることができる。
具体的には、メイン・メソッド上で実行したいメソッド(コントロールへの操作を含む)に対応したデリゲートを作成し、そのデリゲートのインスタンスをInvokeメソッドのパラメータで指定して呼び出せばよい。Invokeメソッドもコントロールが持つメソッドであるが、その呼び出しは別スレッドからでも動作が保証されている。
次のコードは、上記の誤ったコードをInvokeメソッドを使って書き換えたものだ。
private void button1_Click(object sender, System.EventArgs e)
{
Thread t = new Thread(new ThreadStart(worker));
t.Start();
}
delegate bool FocusDelegate();
void worker()
{
// textBox2.Focus()の実行
Invoke(new FocusDelegate(textBox2.Focus));
}
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim t As New Thread(New ThreadStart(AddressOf worker))
t.Start()
End Sub
Delegate Function FocusDelegate() As Boolean
Sub worker()
' textBox2.Focus()の実行
Invoke(New FocusDelegate(AddressOf TextBox2.Focus))
End Sub
このコードでは、TextBox2のFocusメソッドがメイン・スレッド上で実行されるため、プログラムは正しく動作する。
なお上記のコードでは、テキストボックスのFocusメソッドに対してデリゲートを作成しているが、通常は次のようにメソッドを1つ作成し(この場合にはSetFocusメソッド)、それをデリゲート経由で呼び出すようにするのが一般的だ。
private void button1_Click(object sender, System.EventArgs e)
{
Thread t = new Thread(new ThreadStart(worker));
t.Start();
}
delegate void SetFocusDelegate();
void SetFocus()
{
textBox2.Focus();
}
void worker()
{
Invoke(new SetFocusDelegate(SetFocus));
}
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim t As New Thread(New ThreadStart(AddressOf worker))
t.Start()
End Sub
Delegate Sub SetFocusDelegate()
Sub SetFocus()
TextBox2.Focus()
End Sub
Sub worker()
Invoke(New SetFocusDelegate(AddressOf SetFocus))
End Sub
Invokeメソッドが必要かどうかを示すInvokeRequiredプロパティ
Controlクラスには、Invokeメソッドを使う必要があるかどうかをチェックするためのInvokeRequiredプロパティが用意されている。このプロパティは、それを呼び出したスレッドがメイン・スレッドかどうかを調べ、trueあるいはfalseを返す。
次のコードは、上記のコードをInvokeRequiredプロパティを利用して書き換えたものだ。このコードのSetFocusメソッドは、メイン・スレッドからでも別スレッドからでも呼び出すことができるようになっている(別スレッドからSetFocusメソッドを呼び出した場合には、結果的にSetFocusメソッドが2度実行されることになる)。
private void button1_Click(object sender, System.EventArgs e)
{
Thread t = new Thread(new ThreadStart(worker));
t.Start();
}
delegate void SetFocusDelegate();
void SetFocus()
{
if (InvokeRequired)
{
// 別スレッドから呼び出された場合
Invoke(new SetFocusDelegate(SetFocus));
return;
}
textBox2.Focus();
}
void worker()
{
SetFocus();
}
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim t As New Thread(New ThreadStart(AddressOf worker))
t.Start()
End Sub
Delegate Sub SetFocusDelegate()
Sub SetFocus()
If InvokeRequired Then
' 別スレッドから呼び出された場合
Invoke(New SetFocusDelegate(AddressOf SetFocus))
Return
End If
TextBox2.Focus()
End Sub
Sub worker()
SetFocus()
End Sub
同じスレッド内であっても、Invokeメソッドを利用したコントロールの操作は特に問題ないと思われるが、同一スレッドからのコントロールの操作をInvokeメソッドで行うのは無駄である。InvokeRequiredプロパティを使えば、そのような無駄な処理を省くことができる。
カテゴリ:クラス・ライブラリ 処理対象:コントロール
カテゴリ:クラス・ライブラリ 処理対象:マルチスレッド
使用ライブラリ:Controlクラス(System.Windows.Forms名前空間)
使用ライブラリ:Threadクラス(System.Threading名前空間)
Copyright© Digital Advantage Corp. All Rights Reserved.