4.5 ウィンドウプロシージャ
4.5.1 Windows メッセージ
コントロールのイベントは、対応するイベント発生用の On~() メソッドから呼び出されていました。では、On~() メソッドはどこから呼び出されているのでしょうか。そもそも、Control クラスはどのようにしてユーザー入力などの情報を得ているのでしょうか。この問題は Win32 API を C/C++ 言語を使って操作した経験がある人にとってはすでに承知のものです。
ネイティブ Windows は、ウィンドウに対してメッセージという方法でイベントを通知しています。メッセージは単なる数値型の定数で表現され、送られてきたメッセージの値を調べることで、ウィンドウ上で何が起こったのかを知ることができました。このメッセージに関しては Application クラスの Run() メソッドの解説で少しだけ説明しました。
Run() メソッドを起動すると、アプリケーションはメッセージループに入ります。メッセージループは、アプリケーションウィンドウに対して行われた操作や入力を監視し続けるループです。このループ処理は、アプリケーションの主ウィンドウが閉じられるまで続きます。メッセージループが新しいメッセージを発見すると、Windows に対して適切なウィンドウプロシージャにメッセージを送信するように要求します。
このウィンドウプロシージャと呼ばれる特殊な関数がすべてのイベントの源泉であると考えられます。.NET の場合、ウィンドウプロシージャは Control クラスの WndProc() メソッドがウィンドウプロシージャとしてメッセージを受け付けています。このメソッドの働きを理解する必要はありませんが、発生したイベントを意図的に隠蔽したり書き換えるなど、アプリケーション自身を騙すようなプログラムを作るときに使うことがあります。
protected virtual void WndProc(ref Message m)
このメソッドが受け取る System.Windows.Forms.Message 構造体への参照こそ、ネイティブシステムから受け取った Windows メッセージです。Windows はウィンドウプロシージャの結果として構造体から値を受け取ることがあるので、このパラメータは参照型となっています。この構造体オブジェクトの値を変更すると、システムにも影響を与える可能性があるので注意してください。
System.Object System.ValueType System.Windows.Forms.Message
public struct Message
Message 構造体は、メッセージ ID 値を設定・取得する Msg プロパティと、このメッセージに関連したデータを提供する LParam プロパティ及び WParam プロパティを公開しています。ウィンドウプロシージャの戻り値は Result プロパティに設定します。
public int Msg { get; set; }
public IntPtr LParam { get; set; }
public IntPtr WParam { get; set; }
public IntPtr Result { get; set; }
LParam プロパティ、WParam プロパティ、Result プロパティの内容は Msg プロパティに格納されている ID によって変化します。例えば、Msg プロパティの値が 0x0005(C/C++言語で使われていた定数でいうところの WM_SIZE) の場合はウィンドウのサイズ変更イベントを表し、 LParam プロパティの下位 16 ビットに新しい幅が、上位 16 ビットに新しい高さが格納されています。Result プロパティに設定する値もメッセージによって異なり、必要に応じて Windows に値を返す必要があります。
LParam プロパティ、WParam プロパティ、Result プロパティで使われているのは System.IntPtr 構造体型です。この構造体はポインタを渡すときに使われるプラットフォーム固有の型です。ウィンドウプロシージャのように、Windows システムと通信を行う処理に使われます。
System.Object System.ValueType System.IntPtr
[Serializable] public struct IntPtr : ISerializable
Win32 API のネイティブ Windows プログラミングの経験があれば見慣れたものだと思いますが、Java や .NET Framework のような仮想技術による開発に慣れている場合は、この複雑で不安定な仕様に戸惑うことでしょう。しかし、実際に Windows プログラマは長年ウィンドウプロシージャでこのメッセージを受け取ってイベントを処理していたのです。
個々の Windows メッセージについて、本書では解説はしません。これらは古い Win32 API で使われていたものであり、.NET Framework で積極的に使うものではありません。興味がある方は、MSDN と合わせて winuser.h ヘッダファイルを調べると良いでしょう。winuser.h ファイルには、メッセージの定数名と値が宣言されています。
using System.Drawing; using System.Windows.Forms; public class Test : Form { private string text; private const int WN_PAINT = 0x000F; private const int WM_ERASEBKGND = 0x0014; protected override void OnPaint(PaintEventArgs e) { base.OnPaint (e); Font font = new Font(Font.Name, 20); Brush brush = new SolidBrush(Color.Black); e.Graphics.DrawString(text, font, brush, 0, 0); } protected override void WndProc(ref Message m) { base.WndProc(ref m); if (m.Msg != WN_PAINT && m.Msg != WM_ERASEBKGND) { text = "Msg=" + m.Msg + ",LP=" + m.LParam + ",WP=" + m.WParam; Invalidate(); } } static void Main() { Application.Run(new Test()); } }
コード1は、WndProc() メソッドをオーバーライドして、ウィンドウプロシージャに渡されたメッセージの情報を描画するプログラムです。数値だけを見ると何か分かりませんが、これをメッセージ ID に基づいて解析するとウィンドウに対して何が行われたのかを知ることができます。
WndProc() メソッドの処理では、OnPaint() メソッドの呼び出しの前に発生する描画メッセージ WM_PAINT と WM_ERASEBKGND だけは無視するようにプログラムしてあります。どのようなイベントが発生しても、text フィールドに保存されているテキストを描画するには必ず描画メッセージが発生するためです。これをしなければ、常に描画メッセージである WM_ERASEBKGND が表示されてしまいます。