その時気をつけなければならないのが、コントロールの操作。コントロールは、コントロール ( の基になるウィンドウハンドル ) を所有しているスレッドで操作しなければならない。別スレッド内でそのままコントロールを操作してしまうと、正常に動作してくれない。 ( .NET 2.0から特に。 )
では、別スレッドで重い処理を行ったあと、どうやってコントロールを所有しているスレッドに処理を行わせるか?それには、Control.Invokeメソッドを使用する。Control.Invokeは、引数で受け取ったデリゲートオブジェクトを、そのコントロールを所有しているスレッド上で実行するメソッドである。
Contorol.Invokeの第一引数はDelegate型である。この引数に渡すためのデリゲートには、System.Windows.Forms名前空間にある、MethodInvokerデリゲートというデリゲートを使うのがいい 。MethodInvokerには、引数も戻り値もない。このデリゲートを使えば、Invoke用にわざわざデリゲートを自作する必要がなくなる。MSDNにもはっきりと、 「 簡単なデリゲートが必要だが定義するのが面倒な場合に使用できます。 」 と書かれている。
C#2.0の新機能である匿名メソッドを用いると、これら一連の処理が ( わりと ) 見やすくかける。匿名メソッドによって、無駄にメソッドを定義する必要がなくなり、デリゲートが扱いやすくなった。 ( 匿名メソッドはデリゲートとして扱えるが、直接Delegateクラスに変換することはできないことに注意。 )
先に述べたとおり、Control.Invokeメソッドはコントロール ( の基になるウィンドウハンドル ) を所有しているスレッドに処理を行わせる。従って、もし、重たい処理を行っている最中にフォームが閉じられたりして、ウィンドウハンドルが破棄されてしまうと、その後に呼び出されるControl.InvokeメソッドがInvalidOperationExceptionをスローすることになる。
重たい処理中に、フォームが閉じられてしまう可能性がある場合は、Control.IsHandleCreatedプロパティを利用し、コントロールにハンドルが関連付けられているかどうかを判別する必要がある。
以下に、重い処理を別スレッドで行い、処理の前後でフォーム上の各コントロールのEnabledプロパティを切り替えるサンプルコードを示す。
サンプル1// Form1.cs
private void button1_Click(object sender, EventArgs e)
{
this.SetEnabledOfControls(false);
Thread process = new Thread(
delegate()
{
Thread.Sleep(5000); // 重たい処理の代用
if (this.IsHandleCreated)
{
this.Invoke(
(MethodInvoker)delegate()
{
this.SetEnabledOfControls(true);
});
}
});
process.Start();
}
private void SetEnabledOfControls(bool value)
{
// 各コントロールのEnabledプロパティにvalueを設定する処理
}
コントロール操作を行うメソッド内でInvokeメソッドを利用するという手もある。その場合、コントロール操作を行うメソッドが別スレッド上で実行されるとは限らないので、
Control.InvokeRequiredプロパティを利用し、現在のスレッドがコントロールの所有スレッドなのかどうかを判別する。
以下にサンプルを示す。
サンプル2// Form1.cs
private void button1_Click(object sender, EventArgs e)
{
this.SetEnabledOfControls(false);
Thread process = new Thread(
delegate()
{
Thread.Sleep(5000); // 重たい処理の代用
this.SetEnabledOfControls(true);
});
process.Start();
}
private void SetEnabledOfControls(bool value)
{
if (!(this.IsHandleCreated))
{
return;
}
MethodInvoker process =
(MethodInvoker)delegate()
{
// 各コントロールのEnabledプロパティにvalueを設定する処理
};
if (this.InvokeRequired)
{
this.Invoke(process);
}
else
{
process.Invoke();
}
}
以上2つの方法の内どちらを採用するにしろ、コントロール操作の度にControl.IsHandleCreatedプロパティとControl.InvokeRequiredプロパティ ( これは一つ目の方法だと不要 ) のチェックを行ってControl.Invokeメソッドを呼び出すのでは、コードがやや冗長である。
以下のように、静的クラスを用意して、静的メソッドとして定義してやると良い。
public static class ControlUtil
{
public static object SafelyOperated(Control context, Delegate process)
{
return ControlUtil.SafelyOperated(context, process, null);
}
public static object SafelyOperated(Control context, Delegate process, params object[] args)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (process == null)
{
throw new ArgumentNullException("process");
}
if (!(context.IsHandleCreated))
{
return null;
}
if (context.InvokeRequired)
{
return context.Invoke(process, args);
}
else
{
return process.DynamicInvoke(args);
}
}
}
この静的メソッドは、以下のように利用する。 ( Control.Invokeメソッドと同じく、引数を指定したり、戻り値を取得することも可能。 )
SafelyOperatedメソッドを用いてサンプル2のSetEnabledOfControlsメソッドを記述private void SetEnabledOfControls(bool value)
{
MethodInvoker process =
(MethodInvoker)delegate()
{
// 各コントロールのEnabledプロパティにvalueを設定する処理
};
ControlUtil.SafelyOperated(this, process);
}
勉強になりました。Control.InvokeRequiredは、一応、UIスレッドで実行されるであろう箇所でも埋め込んでおいた方がいいのでしょうか?たとえば、BackgroundWorkerのProgressChangedイベントのときとかです。
こちらの方も、Windows Formsのスレッド処理についての記事などを書いたりしております。なにぶん、Javaが専門なので変なこと書いているかもしれないです。突っ込みしていただけると感謝です。m(_ _)m
2006.11.27 19:59 URL | gsf_zero1 #- [ 編集 ]
gsf_zero1さん初めまして。コメントありがとうございます。
確実にUIスレッド上で呼び出されるのならば、Control.InvokeRequiredやControl.Invokeを使用する必要はありません。
BackgroundWorkerのProgressChangedイベントのハンドラは、BackgroundWorkerのインスタンスを生成したスレッド上で呼び出されます。
つまり、確実にUIスレッド上で呼び出されることなります。
そもそも、BackgroundWorkerは、Control.Invokeメソッドを使わずにWindowsフォームでのマルチスレッドを簡単に実現する、新しい手段として.NET Fx 2.0に登場しました。
詳しくは知りませんが、BackgroundWorkerクラスの内部では、AsyncOperationクラスが要となっているようです。
( 実は、私はまだBackgroundWorkerを使ったことがないのですが^^; )
2006.11.28 12:47 URL | よこけん #- [ 編集 ]
大変参考になりました
リモート処理で、コントロールを操作する作業をやっていました。
本掲載内容が大変参考になりました。
ありがとうございます。m(_ _)m
2007.01.21 11:06 URL | yoshi #- [ 編集 ]
yoshiさん初めまして
私の記事が少しでもお役に立てたようで何よりです
コメントありがとうございました^^
2007.01.23 15:00 URL | よこけん #- [ 編集 ]
とても参考になりました
2009.04.27 11:55 URL | anony #- [ 編集 ]
IsHandleCreatedで確認してから、Invokeするまでの間にハンドルが破棄される可能性があります。
なので、ウィンドウを閉じる際に OnClosing() などで、自分で立てたスレッドそのものを停止させるのがよいかと思います。
2011.02.04 14:48 URL | cthirase #6fYKGC0Q [ 編集 ]
とても参考になりました。
ありがとうございます!
2011.09.17 17:12 URL | null #Su2uqQjI [ 編集 ]
トラックバックURL↓
http://csharper.blog57.fc2.com/tb.php/36-b55b0fe7