連載
» 2005年06月17日 05時00分 UPDATE

.NET TIPS:Windowsフォームで別スレッドからコントロールを操作するには?

[遠藤孝信,デジタルアドバンテージ]
.NET TIPS
Insider.NET


「.NET TIPS」のインデックス

連載目次

 Windowsフォームでスレッドを作成した場合、フォームやフォーム上のコントロールに対しては、そのスレッドからの操作(フォームやコントロールが持つメソッドの呼び出しやプロパティの読み書き)は動作が保証されない。本稿ではそのような処理を<安全>に行うためのプログラミングについて解説する。

フォーカスの移動を行うサンプル・プログラム

 例えば、Visual Studio .NET(以降、VS.NET)でWindowsアプリケーションのプロジェクトを新規作成し、次の画面のように、2つのテキストボックス(TextBox1とTextBox2)と1つのボタン(Button1)を配置したとする。

VS.NETで2つのテキストボックスと1つのボタンを配置したWindowsフォーム VS.NETで2つのテキストボックスと1つのボタンを配置したWindowsフォーム

 このアプリケーションを実行すると、起動時にはフォーカスは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

TextBox2コントロールにフォーカスを設定するコード(上:C#、下:VB.NET)

 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

別スレッドからコントロールを操作する誤ったコード(上:C#、下:VB.NET)
プログラムの先頭で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

別スレッドからInvokeメソッドによりコントロールを操作するコードその1(上:C#、下:VB.NET)

 このコードでは、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メソッドによりコントロールを操作するコードその2(上:C#、下:VB.NET)

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

InvokeRequiredプロパティを利用したコード(上:C#、下:VB.NET)

 同じスレッド内であっても、Invokeメソッドを利用したコントロールの操作は特に問題ないと思われるが、同一スレッドからのコントロールの操作をInvokeメソッドで行うのは無駄である。InvokeRequiredプロパティを使えば、そのような無駄な処理を省くことができる。

カテゴリ:クラス・ライブラリ 処理対象:コントロール
カテゴリ:クラス・ライブラリ 処理対象:マルチスレッド
使用ライブラリ:Controlクラス(System.Windows.Forms名前空間)
使用ライブラリ:Threadクラス(System.Threading名前空間)


「.NET TIPS」のインデックス

.NET TIPS

Copyright© 1999-2017 Digital Advantage Corp. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。