C#でサブスレッドからプログレスバーにアクセスする(デリゲート・ラムダ式は快適!)

in

C#/.Netへの移行は徐々に軌道に乗りつつあります。MFC時代は面倒だったクエリを別スレッドで処理しつつメインフォームのプログレスバーをインクリメントするのも簡単に実現できたので気分良くしてます。

スレッドを起こすのは実に簡単。引数も渡せます。

System.Threading.Thread ehqt = new System.Threading.Thread(
  new System.Threading.ParameterizedThreadStart(execute_heavy));
ehqt.Start(var);

で、このheavy_query()内でプログレスバーをインクリメントできれば便利。といっても、サブスレッドからメインフォームのプログレスバーにアクセスするのは危険この上ないのは承知なので一応msdnで確認すると

Windows フォーム コントロールへのアクセスは、本質的にスレッド セーフではありません。コントロールの状態を操作する複数のスレッドがある場合、コントロールが一貫性のない状態に陥る可能性があります。この他に競合状態やデッドロックなどのスレッド関連のバグが発生する可能性もあります。コントロールへのアクセスは、必ず、スレッド セーフな方法で行うようにすることが重要です。

とあります。ちょっとわかりにくい解説とサンプルですが、要するにInvoke()を使うことでサブスレッドからフォーム上のコントロールに安全にアクセスできるみたい。サンプルでは、メソッドをひとつ作成してメインスレッドと共通で使えるようなサンプルでしたがとりあえず冗長なのでスレッドから直に呼び出すようにしました。また、Invokeは、

コントロールの基になるウィンドウ ハンドルを所有するスレッド上で、指定した引数リストを使用して、指定したデリゲートを実行します。

となっているので、デリゲートも必要になります。幸いにも、プログレスバーへのアクセスはmin/max/value値の初期化とvalueのインクリメントだけで、これらはどれも高々引数int vだけがあれば事足りるので

private delegate void delegateProgressBar(int v);

とひとつだけ宣言して、これを使い回せばOK(最初気がつかなかったのですが、デリゲートは同じシグニチャであれば複数個の実装を定義できるのですね)。実際の初期化やインクリメントの実装は、どれもほとんど1,2行で済むのでいちいちメソッドを作成しないでラムダ式ですっきり書けます。

execute_heavy(string var)
{
  OleDbConnection conn = new OleDbConnection(
    "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=c:\\foo.mdb"
  );
  conn.Open();
  OleDbCommand     cmd = new OleDbCommand(var);
  OleDbDataAdapter da  = new OleDbDataAdapter(cmd);
  DataSet          ds  = new DataSet();
  da.Fill(ds, "foo");

  // プログレスバーに初期値を設定
  Invoke(new delegateProgressBar(
    (int v)=>{progressBar.Value=v; progressBar.Minimum=v;}),
    new Object[] { 0 }
  );

  // データベースで取得した件数を最大値に設定
  Invoke(new delegateProgressBar(
    (int v)=>{progressBar.Maximum=v;}),
    new Object[]{ ds.Tables[0].Rows.Count }
  );

  // インクリメント用デリゲート生成
  delegateProgressBar increment_bar = new delegateProgressBar(
    (int v)=>{Cursor.Current = Cursors.WaitCursor; progressBar.Value += v;}
  );

  // select 数分たっぷりループ
  foreach(DataRow r in ds.Tables[0].Rows)
  {
    :
    // なにか重たい処理
    :
    // プログレスバーをインクリメント
    Invoke(increment_bar,new Object[] { 1 });
  }
  :
  conn.Close();
}

インクリメントの実装中にウェイトカーソルの処理を入れたのは、メインスレッドでウェイトカーソルに変更してもスレッド起動後スコープを抜ければ元に戻ってしまうためでこれはちょっと汚いかも。あと、ここには書いていませんが終了のタイミングもInvokeを使っちゃいました。実際、サブスレッドが終了するときに"progressBar.Visible = false;"したかったという事情があるにせよこれももう少し考える必要が出てくるかも知れません。それにしても、C++/MFCでは面倒だった処理がデリゲートやラムダ式といった新機能を利用してすっきり書けて、今更ながら.net/C#に移行してよかったなぁとしみじみ思う秋の夜です。

この記事のトラックバックURL:

http://hippos-lab.com/blog/trackback/337

Comments