伊勢的新常識  Index  Search  Changes  RSS  Login

Windows Mobile Tips - サブクラス化でイベントを取得する

サブクラス化でイベントを取得

イベントが足りない!Compact Framework

.NET Compact Framework は軽量化のために、各コントロールのメソッドやプロパティ、イベントが削られています。

メソッドやプロパティは独自実装でなんとかできることも多いのですが、イベントはなかなか他で代替することができません。たとえば、EbIRC を作成しているとき、以下のようなことがありました。

  • 画面の向きを変えたときにテキストボックスの最終行に移動したい
  • テキストボックスには Resize イベントがない。
    • フォームの Resize イベントをとっても、その段階ではテキストボックスはリサイズしていない
    • フォームの Resize イベントでテキストボックスを最終行に移動しても、テキストボックスのサイズがその後で変わってしまって意味なし。

いにしえの技術、サブクラス化

.NET Framework では、コントロールがイベントを持っていないウィンドウメッセージ*1でも、対象のコントロールを継承して、WndProc メソッドをオーバーライドし、そこでウィンドウメッセージを直接受け取ることができます。しかし、この方法も Compact Framework に WndProc メソッドがないため使用することができません。

そこで出てくるのが「サブクラス化」という手法です。サブクラス化は対象コントロールのウィンドウハンドラ*2を乗っ取り、ウィンドウメッセージを受け取り、処理をします。その後元のウィンドウハンドラには受信したウィンドウメッセージをそのまま流して、元のハンドラからすれば何もなかったかのようにします。

この手法は、同じようにウィンドウメッセージを直接受信できなかった VisualBasic 6 の時代に多用されました。.NET Framework で WndProc メソッドが実装され、(自分の中では)すっかりいにしえの技術となっていました。

サブクラス化のキモ、SetWindowLong

サブクラス化の基本的な手順は以下の通りです。

  1. 新しいウィンドウハンドラへのデリゲートを定義
  2. 新しいウィンドウハンドラを定義
  3. デリゲートのポインタを得て、SetWindowLong API で対象のコントロールのウィンドウハンドラを書き換える。戻り値で元のウィンドウハンドラのポインタを得る。
  4. 新しいウィンドウハンドラで、処理を行う。行った後は CallWindowProc API で元のウィンドウハンドラにデータを流す。
  5. 対象のコントロールがDisposeされる前に、SetWindowLong API で元のウィンドウハンドラを書き戻しておく。

新しいウィンドウハンドラへのデリゲート

ウィンドウハンドラのデリゲートは以下の通りです。

private delegate int WndProcDelegate(IntPtr hwnd, uint msg, uint wParam, int lParam);

それぞれ引数は、受信するウィンドウのハンドル、ウィンドウメッセージ、メッセージのパラメータ2つです。

新しいウィンドウハンドラを定義

新しいウィンドウハンドラを定義します。引数はデリゲートと同じにします*3

protected virtual int WndProc(IntPtr hwnd, uint msg, uint wParam, int lParam)
{
    // (ここに処理を書く)

    // デフォルトのプロシージャへ
    return CallWindowProc(oldWndProc, hwnd, msg, wParam, lParam);
}

CallWindowProc API がでてきました。このAPIはウィンドウハンドラを呼び出す関数で、元のウィンドウハンドラを呼び出します。詳しくは後述します。

デリゲートへのポインタを得る

さて、ここからがサブクラス化の手順です。まず、デリゲートのインスタンスを作成して、プライベートフィールドに格納します。GCに回収されてメッセージが受信できなくなるので、メソッド内変数にしてはいけません。(ここ重要)

デリゲートのポインタは、System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate で得ることができます。

// このWndProcはクラス内で private WndProcDelegate wndProc; と定義されています。
wndProc = new WndProcDelegate(WndProc); 
// WndProcPtrも同様に、 private IntPtr wndProcPtr; と定義されています。
wndProcPtr = Marshal.GetFunctionPointerForDelegate(wndProc);

ウィンドウハンドラの書き換え

いよいよウィンドウハンドラの書き換えです。SetWindowLong API を使用します。このAPIの P/Invoke の定義は以下の通りです。

[DllImport("coredll.dll")]
private extern static IntPtr SetWindowLong(IntPtr hwnd, int nIndex, IntPtr dwNewLong);
private const int GWL_WNDPROC = -4;

SetWindowLongは、ウィンドウに関するいろいろな情報を書き換えることができます。書き換える情報を nIndex のパラメータに指定しますが、今回は、ウィンドウハンドラを書き換えますので、その定数も定義しています。

以下のように呼び出します。第1引数に書き換えたいのコントロールの Handle プロパティ、第2引数に GWL_WNDPROC、第3引数に先ほど作成したデリゲートのポインタを渡します。

oldWndProc = SetWindowLong(targetHandle, GWL_WNDPROC, wndProcPtr);

このとき必ず、戻り値を取得しておいてください。この戻り値が元のウィンドウハンドラのポインタとなります。

これで、新しいウィンドウハンドラへメッセージが届くようになります。

新しいウィンドウハンドラで、元のウィンドウハンドラを呼び出す

新しいウィンドウハンドラで受信したメッセージは、基本的にすべて元のウィンドウハンドラに渡してやる必要があります。本来.NET FrameworkやWindows側でやる処理にもイベントが起こったことを通知してやる必要があるからです*4

ここで使用するのが先ほどの CallWindowProc API になります。

[DllImport("coredll.dll")]
private extern static int CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hwnd, uint msg, uint wParam, int lParam);

第1引数に元のウィンドウハンドラを渡し、第2引数以降に、受信したウィンドウハンドラ

忘れちゃいけない、ウィンドウハンドラ書き戻し。

さて、書き換えたウィンドウハンドラですが、書き換えたコントロールがDisposeが呼ばれてなくなる前に元通りに書き戻す必要があります。SetWindowLong の第3引数に元のウィンドウハンドラのポインタを渡して呼び出します。

result = SetWindowLong(targetHandle, GWL_WNDPROC, oldWndProc);

対象のコントロールの Disposing イベントを受信すれば確実なのですが、Compact Frameworkにはそれすらないので*5、意識的にコントロールのDisposeより先に呼ばれるように調整しなくてはなりません。

リモートスパイをつかおう

さて、ウィンドウメッセージを受信する方法はわかりましたが、どんなときにどのようなウィンドウメッセージが来てるかを調べるのはなかなか骨が折れます。

そこで活躍するのが VisualStudio2005 についてくる「リモートスパイ」というツールです*6。スタートメニュー「Microsoft Visual Studio 2005」の「Visual Studio Remote Tools」というところにあります。

このツールでW-ZERO3へ接続すると、現在開いているウィンドウの一覧が現れますので、取得したいウィンドウを選び、ツールバーの双眼鏡のマークを押すことで、そのウィンドウで受信したメッセージをリアルタイムに確認することができます。この受信メッセージ一覧を開いた状態で操作すれば、その操作で送られるウィンドウメッセージを確認することができます。

RemoteSpy.png

あとは、ウィンドウメッセージの名前(WM_MOVE)などで検索すれば、たいていは呼び出しの方法がかかりますので、それを参考に受信したときのアクションを書くことになります。

TextBoxInputFilter

以上の手順をまとめたのが、EbIRCなどで使用している TextBoxInputFilter です。このクラスはテキストボックスのイベントを処理する専門ですが、コントロールさえ書き換えれば他にも応用できます。


Last modified:2007/10/07 17:49:18
Keyword(s):
References:[SideMenu] [技術ドキュメント]

*1 ウィンドウでイベントが起こったときに、ウィンドウが受け取るデータのこと

*2 ウィンドウでイベントが起こったときに、OSから呼ばれる関数。ウィンドウメッセージを受信するところです。ここにきたデータを元に各イベントに仕分けされます。先ほどのWndProcメソッドはこのウィンドウハンドラにあたります。

*3 デリゲートの基本です。

*4 そうしないと、本来コントロールがあるべき場所に穴が空いたり、面倒なことになります。

*5 Disposedはあるけど、なくなった後では遅い

*6 「スマート デバイス プログラマビリティ」のオプション付きでインストールとかしないといけないかも