「キーボードのボタンが押された」とか「マウスが移動した」等の、 コンピュータ上で発生するなんらかの事象のことをイベント(event)といい、 イベントが発生したときに行う処理のことをイベントハンドラ(event handler)と呼びます。 GUI アプリケーション等では、ユーザからの入力等のイベント発生を待ち、 それらに対して何らかの処理を行っていくことでプログラムが動作しています。 このように、イベントとそれに対する処理により動作するようなプログラムのことをイベントドリブン型(event driven:イベント駆動)プログラムと呼びます。
C# では、このようなイベントドリブン型のプログラム作成を容易にするため、 イベント処理用の構文 event が用意されています。 event は、デリゲートに対するプロパティのようなもので、 以下のような特徴を持っています。
まず始めに、イベントドリブン型のプログラム例を示します。 イベントドリブン型プログラムの典型例である GUI プログラムは、 (OS やライブラリによってプログラマの目からは隠されている場合が多いですが) 基本的にここで説明するような原理に基づいて動いています。 (GUI プログラムに関しては 「GUI アプリケーション」 や 「Windows Presentation Foundation」 参照。)
コンピュータ上で起こるイベントにはさまざまなものがありますが、 ここでは例としてキーボードからの入力を考えます。 簡単なサンプルとして、 1秒おきに時刻を表示するプログラムを作ります。 キーボードからの入力に応じて、 表示の一時停止や、表示形式の変更、プログラムの停止等を行います。
まず、始めに挙げるサンプルでは、 Main 関数内のループで時刻の表示を行い、 別のスレッドでイベント(ユーザからのキー入力)の発生を待ち続け、 同時にイベントの処理もこのスレッド内で行います。
using System; using System.Threading; class TestEvent { // 時刻の表示形式 const string FULL = "yyyy/dd/MM hh:mm:ss\n"; const string DATE = "yyyy/dd/MM\n"; const string TIME = "hh:mm:ss\n"; static bool isAlive = true; // プログラムの続行/終了フラグ。 static bool isSuspended = true; // プログラムの一時停止フラグ。 static string timeFormat = TIME; // 時刻の表示形式。 public static void Main() { Thread thread = new Thread(new ThreadStart(EventLoop)); thread.Start(); while(isAlive) { if(!isSuspended) { // 1秒おきに現在時刻を表示。 DateTime t = DateTime.Now; Console.Write(t.ToString(timeFormat)); } Thread.Sleep(1000); } thread.Join(); } static void EventLoop() { // イベントループ while(isAlive) { // 文字を読み込む // (「キーが押される」というイベントの発生を待つ) string line = Console.ReadLine(); char eventCode = line.Length == 0 ? '\0' : line[0]; // イベント処理 switch(eventCode) { case 'r': // run isSuspended = false; break; case 's': // suspend isSuspended = true; break; case 'f': // full timeFormat = FULL; break; case 'd': // date timeFormat = DATE; break; case 't': // time timeFormat = TIME; break; case 'q': // quit isAlive = false; break; default: // ヘルプ Console.Write( "使い方\n" + "r (run) : 時刻表示を開始します。\n" + "s (suspend): 時刻表示を一時停止します。\n" + "f (full) : 時刻の表示形式を“日付+時刻”にします。\n" + "d (date) : 時刻の表示形式を“日付のみ”にします。\n" + "t (time) : 時刻の表示形式を“時刻のみ”にします。\n" + "q (quit) : プログラムを終了します。\n" ); break; } }//while(isAlive) }//Main }
このプログラムでは、EventLoop というメソッドの中で、
イベント(ユーザのキー入力)発生を待ち、
その処理を行っています。
ここで、イベントの発生を待つ部分は他のプログラムでも利用可能な汎用的な処理です。
そのため、次のステップとして、
イベント発生待ちの部分を取り出して、汎用ルーチン化することを考えます。
すなわち、イベント発生待受け部(イベントループ)とイベント処理部(イベントハンドラ)を分けて実装することにします。
これまでで見てきたように、 イベントドリブン型のプログラムは大きく分けて 「イベント発生待受け部」(イベントループ)と「イベント処理部」(イベントハンドラ)の2つの部分からなります。 イベント処理部はプログラムごとに異なる処理を行うことになりますが、 イベント発生待受け部は汎用的な処理で、 どんなプログラムでも共通の処理になります。
そこで、イベント発生待受け部のみを独立させ、汎用ルーチン化することを考えます。 といっても、これの実現はそれほど難しい事ではなく、 単にデリゲートを用いてイベント処理を他のメソッドに譲り渡してしまえばいいだけのことです。 したがって、イベント発生待受け用クラスは以下のようになります。
using System; using System.Threading; // イベント処理用のデリゲート delegate void KeyboadEventHandler(char eventCode); /// <summary> /// キーボードからの入力イベント待受けクラス。 /// </summary> class KeyboardEventLoop { KeyboadEventHandler onKeyDown; Thread thread; public KeyboardEventLoop(KeyboadEventHandler onKeyDown) { this.onKeyDown = onKeyDown; } /// <summary> /// 待受け開始。 /// </summary> public void Start() { this.thread = new Thread(new ThreadStart(this.EventLoop)); this.thread.Start(); } /// <summary> /// 待受け終了。 /// </summary> public void End() { this.thread.Abort(); } /// <summary> /// イベント待受けスレッドの状態。 /// </summary> public bool IsAlive { get{return this.thread.IsAlive;} } /// <summary> /// イベント待受けループ。 /// </summary> void EventLoop() { try { // イベントループ while(true) { // 文字を読み込む // (「キーが押される」というイベントの発生を待つ) string line = Console.ReadLine(); char eventCode = (line == null || line.Length == 0) ? '\0' : line[0]; // イベント処理はデリゲートを通して他のメソッドに任せる。 this.onKeyDown(eventCode); } } catch(ThreadAbortException){} } }
このようにしてイベント待受け部から独立させたイベント処理部(この例においては onKeyDown デリゲート)のことをイベントハンドラと呼びます。
そして、このクラスを用いて先ほどのサンプルプログラムを書き換えると以下のようになります。
class TestEvent { // 時刻の表示形式 const string FULL = "yyyy/dd/MM hh:mm:ss\n"; const string DATE = "yyyy/dd/MM\n"; const string TIME = "hh:mm:ss\n"; static KeyboardEventLoop eventLoop; static bool isSuspended = true; // プログラムの一時停止フラグ。 static string timeFormat = TIME; // 時刻の表示形式。 public static void Main() { eventLoop = new KeyboardEventLoop(new KeyboadEventHandler(OnKeyDown)); eventLoop.Start(); while(eventLoop.IsAlive) { if(!isSuspended) { // 1秒おきに現在時刻を表示。 DateTime t = DateTime.Now; Console.Write(t.ToString(timeFormat)); } Thread.Sleep(1000); } } /// <summary> /// イベント処理部。 /// </summary> static void OnKeyDown(char eventCode) { // イベント処理 switch(eventCode) { case 'r': // run isSuspended = false; break; case 's': // suspend Console.Write("\n一時停止します\n"); isSuspended = true; break; case 'f': // full timeFormat = FULL; break; case 'd': // date timeFormat = DATE; break; case 't': // time timeFormat = TIME; break; case 'q': // quit eventLoop.End(); break; default: // ヘルプ Console.Write( "使い方\n" + "r (run) : 時刻表示を開始します。\n" + "s (suspend): 時刻表示を一時停止します。\n" + "f (full) : 時刻の表示形式を“日付+時刻”にします。\n" + "d (date) : 時刻の表示形式を“日付のみ”にします。\n" + "t (time) : 時刻の表示形式を“時刻のみ”にします。\n" + "q (quit) : プログラムを終了します。\n" ); break; } } }
ここまでの話をもう1歩推し進めて、 イベントハンドラの追加削除を自由にできるようにしたいと思います。 これはイベントハンドラ用のデリゲート変数を public にしてしまえば簡単にできたりもしますが、 「プロパティ」 で説明したように、 メンバー変数を外部から直接取得/書換え可能にすべきではありません。 デリゲート変数も例外ではなく、取得/書換えはアクセッサを介して行うべきです。
それならば、デリゲート型のプロパティを用意すればいいのではないかと思われるかもしれませんが、それではまだ不十分です。 なぜかといいますと、イベントハンドラ用のデリゲートには以下のような条件が求められるからです。
すなわち、クラス内部からは通常のデリゲート変数と同様に扱え、
外部からは +=、-= 演算子によるデリゲートの追加/削除のみを行えるような仕組みが欲しいわけです。
プロパティではこのような仕組みは提供できません。
そこで、C# にはこの仕組みを実現するために event というキーワードが用意されています。
利用方法は簡単で、イベントハンドラとして使用したいデリゲート型の変数宣言時に event という修飾子を付けるだけです。
event デリゲート型 イベントハンドラ名;
このようにして宣言した変数は“イベント”と呼ばれ、
前述のように内部からは普通のデリゲートと同じように利用でき、
外部からは +=、-= のみが利用できるようになります。
また、イベントはプロパティの get/set と同じように、
add/remove というキーワードを用いて、
追加/削除時の処理を明示的に指定することもできます。
(省略可能。)
event デリゲート型 イベントハンドラ名 { add { // addアクセサ // ここにイベントハンドラ追加時の処理を書く。 } remove { // removeアクセサ // ここにイベントハンドラ削除時の処理を書く。 } // add/remove アクセッサ共に、 // 追加/削除したいイベントハンドラは value という名前の変数に格納されている。 }
このように明示的に追加/削除時の処理を追加したものをイベントプロパティと呼びます。
それでは、先ほど作成したイベント発生待受けクラスを event を用いて書き換えてみましょう。 (event キーワードを足して public にしただけ。)
using System; using System.Threading; // イベント処理用のデリゲート delegate void KeyboadEventHandler(char eventCode); /// <summary> /// キーボードからの入力イベント待受けクラス。 /// </summary> class KeyboardEventLoop { Thread thread; public KeyboardEventLoop() : this(null){} public KeyboardEventLoop(KeyboadEventHandler onKeyDown) { this.OnKeyDown += OnKeyDown; } /// <summary> /// キー入力に対するイベントハンドラ。 /// </summary> public event KeyboadEventHandler OnKeyDown; /// <summary> /// 待受け開始。 /// </summary> public void Start() { this.thread = new Thread(new ThreadStart(this.EventLoop)); this.thread.Start(); } /// <summary> /// 待受け終了。 /// </summary> public void End() { this.thread.Abort(); } /// <summary> /// イベント待受けスレッドの状態。 /// </summary> public bool IsAlive { get{return this.thread.IsAlive;} } /// <summary> /// イベント待受けループ。 /// </summary> void EventLoop() { try { // イベントループ while(true) { // 文字を読み込む // (「キーが押される」というイベントの発生を待つ) string line = Console.ReadLine(); char eventCode = (line == null || line.Length == 0) ? '\0' : line[0]; // イベント処理はデリゲートを通して他のメソッドに任せる。 if(this.OnKeyDown != null) this.OnKeyDown(eventCode); } } catch(ThreadAbortException) { } } }