GUIスレッド以外で動くコンポーネント側からコントロール操作したい!
ポッキーの日でありながらバイナリデーですね!! (前フリ)
今回はふつーのスレッドでウゴウゴするコンポーネントから、コントロールを操作したいよー!
ってあれです。
GUIスレッド?
ウィジェット、WinForms でいうコントロールは GUI スレッドというスレッドの中で生まれ、GUI スレッドの中で死んでいきます。
しかし、それ以外のコンポーネントは別のスレッドに跨ったりもできるわけです。
例えば、何らかの情報を受信し窓に通知するコンポーネント HogeReceiver
を作るとします。
MonoDevelop を使って途中まで書いてみました:
/// <summary> /// Hoge receiver. /// </summary> public class HogeReceiver : Component { /// <summary> /// Initializes a new instance of the <see cref="Demo.Components.HogeReceiver"/> class. /// </summary> public HogeReceiver() { } /// <summary> /// Occurs when receive. /// </summary> public event EventHandler<ReceiveEventArgs> Receive; /// <summary> /// Start this instance. /// </summary> public bool Start() { } /// <summary> /// Stop this instance. /// </summary> public bool Stop() { } }
使い方
HogeReceiver
をフォームに貼り付けて、Start()
で受信用スレッドを開始します:
private void Form1_Load(object sender, EventArgs e) { hogeReceiver1.Start(); }
Stop()
で受信用スレッドを停止します:
private void Form1_FormClosed(object sender, FormClosedEventArgs e) { hogeReceiver1.Stop(); }
受信したデータを使ってうにゃうにゃする場合はこんな風にします:
private void hogeReceiver1_Receive(object sender, ReceiveEventArgs e) { listBox1.Items.Add( string.Format( "{0} に {1} が {2} しました。", Date.Now.ToString(), e.Value1, e.Value2 ) ); }
問題
しかし、ここで問題があります。
listBox1.Items.Add( ... );
という処理は hogeReceiver1
の受信用スレッドで実行されるため、hogeReceiver1_Receive
で例外が送出されます。
何の例外だったか忘れましたが、GUI スレッドでコントロール使えよ!ヽ(`Д´)ノというものだった気がします。
private void hogeReceiver1_Receive(object sender, ReceiveEventArgs e) { listBox1.Invoke( delegate (object sender, ) { listBox1.Items.Add( string.Format( "{0} に {1} が {2} しました。", Date.Now.ToString(), e.Value1, e.Value2 ) ); } ); }
ということで、こんなめんどくさいことをしないといけません:
private void hogeReceiver1_Receive(object sender, ReceiveEventArgs e) { listBox1.Invoke( delegate (object sender, ReceiveEventArgs e) { listBox1.Items.Add( string.Format( "{0} に {1} が {2} しました。", Date.Now.ToString(), e.Value1, e.Value2 ) ); }, listBox1, e ); }
Receive
イベントに設定したイベントハンドラを HogeReceiver
側でなおかつ Invoke()
したい!
というのが今回のお題です。
じゃあさ、コントロールはどーやって持ってくるの?
いい質問ですね。
HogeReceiver
側で Invoke()
するには、イベントハンドラを所有しているコントロールが分からないことには二進も三進もいきませんよね。
ですが、その心配は無用です。
EventHandler は Delegate なので、所有しているオブジェクトを取得できるプロパティ、Target
というのがあるのです。
これを遣いましょう。
追加されたイベントハンドラのコレクションはどこから取得するの?
ええ、これが一番難しかったです。
System.Reflection.EventInfo というのがあったんですが、これは全く違いました。
EventInfo
じゃイベントハンドラのコレクションを取得することはできません。
じゃあ、どーすればいいんでしょう?
イベントに追加したイベントハンドラのリストが取得できないって?
ジョジョ、逆に考えるんだ。
イベントハンドラが追加された時にコレクションに追加すればいいんだと考えるんだ。
ありがとう、ジョースター卿。
イベントにはプロパティみたく add
と remove
というのを書くところがあります。
これはつまり、イベントハンドラが追加された時と追加されたイベントハンドラを削除する時に呼ばれる処理を書くことができるということです。
ヽ(゚∀゚)ノ ワー
System.ComponentModel
名前空間に System.ComponentModel.EventHandlerList というのがあるのでそれをフィールドとして持った後は:
/// <summary> /// Occurs when receive. /// </summary> public event EventHandler<ReceiveEventArgs> Receive { add { lock ( this.eventLock_ ) { this.eventHandlerList_.AddHandler( ReceiveEventKey, value ); } } remove { lock ( this.eventLock_ ) { this.eventHandlerList_.RemoveHandler( ReceiveEventKey, value ); } } }
こんな感じで value
を追加したり削除したりしましょう。
フィールドはこんな感じになってます:
/// <summary> /// The event lock. /// </summary> private object eventLock_ = new object(); /// <summary> /// The event handler list. /// </summary> private EventHandlerList eventHandlerList_ = new EventHandlerList(); /// <summary> /// The receive event key. /// </summary> private static readonly object ReceiveEventKey = new object();
イベントハンドラの実行だと思われる
後は OnReceive()
の中でこんな風に呼べばいいんじゃないかな…。
private void OnReceive(ReceiveEventArgs e) { EventHandler<ReceiveEventArgs> handler = (EventHandler<ReceiveEventArgs>)this.eventHandlerList_.Item( ReceiveEventKey ); if ( handler.Target is Control ) { Control target = handler.Target as Control; if ( target != null || target.InvokeRequired ) { target.Invoke( handler, target, e ); } } else { handler( handler.Target, e ); } }
あー、疲れた。
そうそう、VB で書いてたのを無理やり C# で書いてるから、いくらか間違ってるかもしれない。
そんときはさーせん。