メッセージクラッカ
はじめに
MFC を使わないで C/C++ の WinMain コードを書いている人は、メッセージクラッカを使うべきです。メッセージクラッカは、メッセージハンドラをわかりやすく書くためのマクロで、 windowsx.h で定義されています。メッセージクラッカを使用するときは、 WindowProc と DialogProc で書き方が異なるので注意が必要です。
1. WindowProc とメッセージクラッカ
はじめにメッセージクラッカの使い方を説明してから、次にその仕組みについて説明します。
1.1 使い方
Platform SDK をインストールしているならば、include パスに windowsx.h があるはずです。それを開いてみてください。そのヘッダーには 英語で 「windowsx.h マクロ API, ウィンドウメッセージ クラッカー 及び コントロール API 」 と記載されています。
マクロ API については今回は説明しませんが、よく使うコードがマクロになっていますので一通り眺めて便利そうなのをピックアップされてお くと良いと思います。
さて、ウィンドウメッセージクラッカーですが、これは 「メッセージハンドラをわかりやすく書くためのマクロ群」 です。通常、WindowProc は次のような形を取ります。
LRESULT CALLBACK WindowProc
( HWND hwnd, UINT uMsg, WPARAM
wParam, LPARAM lParam ) {
switch ( uMsg ) {
case WM_CREATE:
// WM_CREATE メッセージの処理
....
case WM_COMMAND:
// WM_COMMAND メッセージの処理
....
case ....
}
return DefWindowProc (hwnd,
uMsg, wParam, lParam);
}
つまり、何も工夫をしないとこのように、複数の機能を持った巨大な switch 文が出来上がってしまいます。複数の機能を持つので、一つの関数の中で大量の変数を宣言することになりますし、wParam や lParam の意味をメッセージの種類ごとに読み替えて使わねばならないので煩雑になりがちです。
そこで、メッセージクラッカの登場です。
例えば上記のコードは、メッセージクラッカを使うと次のようにかけます。
LRESULT CALLBACK WindowProc
( HWND hwnd, UINT uMsg, WPARAM
wParam, LPARAM lParam ) {
switch ( uMsg ) {
HANDLE_MSG
(hwnd, WM_CREATE, OnCreate);
HANDLE_MSG (hwnd, WM_COMMAND,
OnCommand);
}
return DefWindowProc (hwnd,
uMsg, wParam, lParam);
}
BOOL Cls_OnCreate (HWND
hwnd, LPCREATESTRUCT lpCreateStruct) {
// WM_CREATE の処理
return TRUE;
}
void Cls_OnCommand (HWND
hwnd, int id, HWND hwndCtl, UINT codeNotify) {
// WM_CREATE の処理
}
注目するところは、(1) WindowProc の switch 文が HANDLE_MSG の並びに変わっているところ、もうひとつはそれぞれのメッセージ (ここでは WM_CREATE と WM_COMMAND) の処理がそれぞれ異なるプロシージャで行われていることです。
特にこれが便利なのは、メッセージハンドラのパラメータが、例えば LPCREATESTRUCT のように、実際に渡されるデータに解釈されていることです。これにより、毎回毎回 lParam を LPCREATESTRUCT にキャストするような煩雑さが消えます。
ここで登場する HANDLE_MSG、Cls_OnCreate あるいは Cls_OnCommand の宣言は windowsx.h に書いてあります。
例えば、WM_CREATE のメッセージハンドラを書こうと思ったときは、
- windowsx.h を開く
- WM_CREATE を検索
- HANDLE_WM_CREATE
マクロ定義の直前にコメントとして書いてあるプロトタイプを使う
(自分のコードの中で宣言する)
という手順でプロトタイプを見つけてくれば良いのです。メッセージクラッカの使い方はこれだけです。
尚、windowsx.h をいちいち開いて確認するのが面倒ならば、メッセージクラッカー ウィザード ツールを使っても良いと思います。
1.2 仕組み
では、その仕組みについて見てみましょう。マクロを展開するだけです。
WM_CREATE を例にとって見てみましょう。まず HANDLE_MSG は windowsx.h にて次のように定義されています。
#define HANDLE_MSG(hwnd, message,
fn) \
case (message): return
HANDLE_##message((hwnd), (wParam), (lParam), (fn))
また WM_CREATE は次の通りです。
/* BOOL Cls_OnCreate(HWND hwnd,
LPCREATESTRUCT lpCreateStruct) */
#define HANDLE_WM_CREATE(hwnd, wParam, lParam, fn) \
((fn)((hwnd), (LPCREATESTRUCT)(lParam))
? 0L : (LRESULT)-1L)
ですから、これを
switch ( uMsg ) {
HANDLE_MSG (hwnd, WM_CREATE, Cls_OnCreate);
}
として使った場合は、HANDLE_MSG を展開すると
switch (uMsg) {
case (WM_CREATE): return HANDLE_WM_CREATE((hwnd), (wParam),(lParam),
(Cls_OnCreate));
}
となります。さらに HANDLE_WM_CREATE を展開すると、次のようになります。
switch (uMsg) {
case (WM_CREATE): return ((Cls_OnCreate)((hwnd),
(LPCREATESTRUCT)(lParam)) ? 0L : (LRESULT)-1L);
}
括弧が多くてごちゃごちゃしているので、改行したりして少し読みやすくすると次になります。
switch (uMsg) {
case (WM_CREATE):
return (
Cls_OnCreate ( hwnd,
(LPCREATESTRUCT) lParam)
? 0L : (LRESULT) -1L
);
}
つまり、Cls_OnCreate (hwnd, (LPCREATESTRUCT) lParam) を実行して、それが TRUE を返せば 0 を、FALSE を返せば -1 を返すようになります。
そもそも WM_CREATE メッセージは MSDN にこのように書いてあります。
If an application processes this message, it should return zero to continue creation of the window. If the application returns –1, the window is destroyed and the CreateWindowEx or CreateWindow function returns a NULL handle.
(もしアプリケーションがこのメッセージを処理したら、ゼロを返してそのウィンドウの生成を続ける。もしアプリケーションが -1 を返したらそのウィンドウは破棄され、CreateWindoweEx または CreateWindow 関数は NULL ハンドルを返します)
まさに Cls_OnCreate プロシージャの成否 (TRUE or FALSE) を、WindowProc に正しく反映しています。
マクロがこのようにうまく展開されるように出来ていますので、windowx.h のコメントに書いてあるプロトタイプを使えばよいの です。
ちなみに、 マクロの形式は大きく分けて二種類あります。 ひとつは上記 WM_CREATE のように明示的に TRUE や FALSE を返して、それを元に WindowProc に返す値を判断するタイプ。もうひとつは、(a, b, c) 式に処理を行って c の値を返すタイプです。WM_COMMAND が後者の例なので見てみましょう。WM_COMMAND は次のように宣言されています。
/* void Cls_OnCommand(HWND hwnd, int id,
HWND hwndCtl, UINT codeNotify) */
#define HANDLE_WM_COMMAND(hwnd, wParam, lParam, fn) \
((fn)((hwnd), (int)(LOWORD(wParam)),
(HWND)(lParam), (UINT)HIWORD(wParam)), 0L)
Cls_OnCommand の戻り値は void です。しかし、もともとウィンドウプロシージャは LPARAM を返します。ただし、WM_COMMAND の場合は MSDN に
If an application processes this message, it should return zero.
(アプリケーションがこのメッセージを処理したら、ゼロを返す)
とありますので、ゼロを返せばよいのです。それが上記の 0L の部分です。つまりこれは、
case WM_COMMAND:
return ( OnCommand式の評価, 0L)
という形になっており、結局 0 が返ります。マクロはそのようにメッセージにあわせて戻り値も調整してくれるのでコーディングがとても楽になります。
2. DialogProc への適用
冒頭で DialogProc では HANDLE_MSG の使い方に注意が必要であると書きました。はじめに、なぜ注意が必要なのかを説明します。それから、使い方について述べます。
2.1 なぜ注意が必要なのか
上述したようにメッセージクラッカーはウィンドウプロシージャ (WindowProc) に対して使用するものです。ダイアログプロシージャは、ウィンドウプロシージャと戻り値が異なります。ですから、 windowsx.h に書いてあるままでは使えないのです。
WindowProc は処理したメッセージ応じて、適切な LPARAM 値を返します。一方、DialogProc は MSDN に次のように書かれています (私の参考訳です。正確には英文をお読みください)。
通常、メッセージを処理したらダイアログボックスプロシージャは TRUE を返し、処理しなかったら FALSE を返してください。ダイアログボックスプロシージャが FALSE を返したら、ダイアログマネージャはそのメッセージへの応答にデフォルトの操作を行います。
もしダイアログボックスプロシージャが、特定の戻り値を必要とするメッセージを処理するのならば、そのダイアログボックスプロシージャは TRUE を返す直前に SetWindowLong (hwndDlg, DWL_MSGRESULT, lResult) を呼び出すことによって返したい戻り値をセットしてください。TRUE を返す直前に行わなければならないことに注意してください。早く呼び出してしまうと DWL_MSGRESULT の値が入れ子になったダイアログボックスメッセージによって上書きされてしまうかもしれません。
以下のメッセージは上述の一般的なルールの例外です。戻り値の意味に関する詳細についてはドキュメントを参照してください。
WM_CHARTOITEM
WM_COMPAREITEM
WM_CTLCOLORBTN
WM_CTLCOLORDLG
WM_CTLCOLOREDIT
WM_CTLCOLORLISTBOX
WM_CTLCOLORSCROLLBAR
WM_CTLCOLORSTATIC
WM_INITDIALOG
WM_QUERYDRAGICON
WM_VKEYTOITEM
大雑把に言えば、処理したら TRUE、処理しなかったら FALSE です。ですから、上に列挙されている WM_CHARTOITEM など以外は ハンドラではいつも TRUE を返せばよいのです。TRUE は 1 です。WindowProc では、処理したらゼロを返すことが多いのに、DialogProc は処理したら 1 を返すのです。正反対です。ですから、DialogProc ではメッセージクラッカはそのままでは使えないのです。
2.2 使い方
結局どうすればよいかというと、次のようなマクロを定義して使えばよいのです。
#define HANDLE_DLG_MSG(hwnd, msg, fn) \
case(msg): \
return SetDlgMsgResult(hwnd, msg, HANDLE_##msg(hwnd, wParam, lParam,fn))
そして DialogProc は次のようになります。
BOOL CALLBACK DialogProc
(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch(uMsg) {
HANDLE_DLG_MSG (hwnd,
WM_INITDIALOG, OnInitDialog);
HANDLE_DLG_MSG (hwnd,
WM_COMMAND, OnCommand);
}
return FALSE;
}
尚、ここで出てくる SetDlgMsgResult は windowsx.h で定義されているマクロです。
#define SetDlgMsgResult(hwnd,
msg, result) (( \
(msg) ==
WM_CTLCOLORMSGBOX || \
(msg) ==
WM_CTLCOLOREDIT
|| \
(msg) == WM_CTLCOLORLISTBOX || \
(msg) ==
WM_CTLCOLORBTN
|| \
(msg) ==
WM_CTLCOLORDLG
|| \
(msg) == WM_CTLCOLORSCROLLBAR || \
(msg) ==
WM_CTLCOLORSTATIC || \
(msg) ==
WM_COMPAREITEM
|| \
(msg) ==
WM_VKEYTOITEM
|| \
(msg) ==
WM_CHARTOITEM
|| \
(msg) ==
WM_QUERYDRAGICON
|| \
(msg) ==
WM_INITDIALOG
\
) ? (BOOL)(result) :
(SetWindowLongPtr((hwnd), DWLP_MSGRESULT, (LPARAM)(LRESULT)(result)),
TRUE))
つまり、WM_CTLCOLORMSGBOX ~ WM_INITDIALOG までのメッセージだったら、HANDLE_MSG を通してメッセージハンドラを処理した戻り値をそのまま返します。一方、それ以外の場合 (大半の場合) は、戻り値を DWLP_MSGRESULT にセットしてから、いつも TRUE を返します。特別な値を必要としない場合、DWLP_MSGRESULT にセットされた値は単に無視されるだけです。