さっくりWINAPI_03//ゥィンドウプロシージャ
今回はWINAPIの中核、というか、一番面倒くさい部分と言うか、目新しい所というか、まあ、そんな感じの役割を果たすプロシージャについて解説していこうかと思います。
前回
1.メッセージループ
プログムは基本的にメイン関数を起点として、順次命令を読んでいきます。しかしこれでは困ったことが起ります。ウィンドウのアプリケーションでは、マウスの移動、クリック、閉じる、など何時どの様な要求が送られてくるのか分かりません。プログラムはこの要求が送られてくる瞬間を待ってたり、暇な時には別の処理をしたりする必要があるのです。(エッヘン)
ウィンドウでは、この要求をメッセージと呼びます。プログラムは常にメッセージを受け付けるループを持ち、ここで受け取ったメッセージを具体的に処理する関数に送り込む必要があります。このメッセ―ジを送り込む処理をディスパッチ(配車係)と呼びます。
勝手に思い込んでいるだけなので実際にそうなのかは分かりません。が、DispatchMessageという関数で使われているので、多分そうじゃないかなーと勝手に思っています。
細かい所はこの辺に書いてあります。
さて、ではそのメッセージループとやらを見てみましょう。
#include<windows.h>
int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd) {
WNDCLASS wc = {};
HWND hWnd = {};
MSG msg = {};
static const WCHAR AppName[] = TEXT("Sample Window");
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = DefWindowProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = AppName;
if(!RegisterClass(&wc)) return 0;
hWnd = CreateWindow(AppName, TEXT("Window"), WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
MessageBox(NULL, TEXT("CreateWindow"), TEXT("MessageBox"), MB_OK);
while (GetMessage(&msg, NULL, 0, 0)) {
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
ちなみにこのコードを右上のバツ印で消そうとしても、プログラムが終了しないので、親のコンパイラ側で削除するか、タスクマネージャーなどを使って頑張って削除してください。
前半の部分は前回までに見ているのでガンガン飛ばしていきます。一つ違う点は、MSGが定義されていることぐらいでしょうか。
後半では新しくGetMessage関数が出てきましたね。こいつがメッセージを取得する関数です。受け取ったメッセージはMSG構造体に送られます。では、順番に見ていきましょう。
まずは、MSGについてです。正確な事は次のドキュメントを参考にしてください。
MSG構造体は次のように定義されています。
typedef sturct tagMsg{
HWND hWnd;
UINT Message;
WPARAM wParam;
LPARAM lParam;
DOWRD time;
POINT pt;
DWORD lPrivate;
}MSG, *PMSG, *NPMSG, *LPMSG;
hWndは、メッセージを処理するウィンドウのハンドルが格納されています。このハンドルを通してウィンドウの制御するのはご存じと通りかと存じます。
Messageは発行されたメッセージの種類が格納されます。その実態は唯の数字ですが、マクロによってそれぞれの数字に意味が付与されています。具体的には次のドキュメントに記載されています。
例えばWM_DESTROYはウィンドウが破棄される際に、WM_QUITなどはアプリケーションが破棄される時にメッセ―ジが発行されます。基本的にはこのデータを使ってメッセージの種類は識別していきます。
一番厄介なのが、WPARAMとLPARAMです。これはメッセ―ジに付随するパラメータで、メッセ―ジの処理に必要な情報が含まれています。が、曲者なのはこの、必要な情報がメッセージによって違うことです。メッセ―ジ毎に良い感じにキャストして使ってく感じです。
イメージ的にはmalloc関数で取得したいデータ型に合わせて(int*)malloc(IntSize)や(char*)malloc(charSize)のように使い分けていく感じでしょうか。まあ、実際に使ってみた方が速いですね。
timeは文字通りメッセ―ジが発行された時間を格納しています。メッセ―ジが発行される瞬間と、実際にメッセージを処理する瞬間は微妙に違うので(といってもほぼ瞬間のことが多いと思います)ちょこっと注意が必要かもですね。
ptはメッセ―ジ発行時のカーソルの位置座標が格納されています。
lPrivateは、何なんでしょうね?説明も特に無くて、筆者も使った記憶がありません。誰かご存じの方がいらっしゃったら教えて頂ければ幸いです。
次に、GetMessage関数についてです。メッセージは発行された後、何処かに良い感じに保管されています。これを順繰りに取り出す関数がGetMessage関数です。GetMessage関数はメッセージを取得するまでその関数でプログラムが止まってしまうので注意が必要です。それが嫌な場合はPeekMessageを使いますが、まあ、今は気にしなくて大丈夫です。
はい、具体的なドキュメントです。
BOOL GetMessage(
LPMSG lpMsg,
HWND hWnd,
UINT wMsgFilterMin,
UINT wMsgFilterMax
);
lpMsgはMSG構造体のポインタです。ここで、取得したメッセージの情報を格納します。
hWndはメッセ―ジを発行したハンドル、つまり取得したいメッセージを発行したウィンドのハンドルを指定します。NULLを設定すると、プログラムで利用しているメッセージを勝手にいい感じに取得してくれます。
wMsgFilterMin,wMsgFilterMaxは文字通りフィルターの上限と下限を指定します。メッセ―ジはUINTのWM_で表現されますが、この実体はUINTです。つまり、0から2^32の数字で表現され、この数字の間に順繰りにデータが設定されています。この範囲をフィルターによって指定することができます。
つまり、受け取りたくないメッセージの指定をこのフィルターを通して、設定することができます。
まあ、特に問題なければ0Lとか、NULL、とかで大丈夫です。(超適当)
戻り値はWM_QUITの時は0、それ以外の時は0以外の数字となります。条件分岐に乗っけておくと、アプリケーションが終了した時にfalseに分岐します。while(GetMessage())でwhile文から抜ける際には、アプリケーションが終了していることになりますね。
なお、このときwParamの値は0になっています。WinMain関数の戻り値に指定されている理由がこれになります。もしも0以外の数字が格納されていれば、それは何らかの異常があったことを意味していますね。
最後にDispatchMessage関数です。この関数では、メッセージを具体的な処理を行うプロシージャに送り込んでくれます。ほぼ必須の処理なので、まあ、取り合えずおまじない感覚でも大丈夫です。
LRESULT DispatchMessage(
]const MSG *lpMsg
);
中々長かったですね。取り合えずこれで、ウィンドウの作成ができるようになりました。やったね!お疲れさまでした!
2.プロシージャ
と、そうは問屋が卸しません。ここからが本番でございます。
さて、ウィンドウプロシージャという単語がちょくちょく出て来たと思いますが、ここではその説明をサクッとやっていきましょう。
通常ウィンドウではユーザーの入力などに対してメッセージが送られ、そのメッセージに基づいて実際の処理が行われます。この実際の処理を行う部分がプロシージャです。プロシージャは通常手続きなどと翻訳されるそうですね。
実体は次のようなものです。
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
大体の引数はMSG構造体に含まれていたので、詳しい説明は端折ります。簡単にhWndが処理するウィンド、msgがメッセージのマクロ、wParam,lParamはメッセージ毎に付随するパラメータの事です。
で、このプロシージャ、関数のポインタとしてウィンドウ側に送られます。関数の引数に送られる関数の事をコールバック関数と呼び、このウィンドウプロシージャはコールバック関数です。
で、実はこのウィンドウプロシージャはすでに使用されていたりします。
wc.lpfnWndProc = DefWindowProc;
はい、こいつの事です。実はDefWindowProcとは、ウィンドウの基本的な処理を行ってくれるプロシージャとなっています。このプロシージャの関数を、WNDCLASS構造体に登録していますね。
では、DefWindowProcではなく、自前で作成したプロシージャを登録してみましょう。
#include<windows.h>
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}
int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd) {
WNDCLASS wc = {};
HWND hWnd = {};
MSG msg = {};
static const WCHAR AppName[] = TEXT("Sample Window");
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = AppName;
if(!RegisterClass(&wc)) return 0;
hWnd = CreateWindow(AppName, TEXT("Window"), WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
while (GetMessage(&msg, NULL, 0, 0)) {
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
これできちんとウィンドウが終了するようになりました。ではプロシージャの中身を見ていきましょう。
switch (msg) {
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
msgのデータごとによって処理を分ける必要があるため、switchで分岐しています。WM_DESTROYはウィンドウを破棄するときに送られるメッセージで、つまり、右上のバッテンを押したときに発行されます。
で、この具体的な処理はPostQuitMessage関数を使用しています。この関数では、ウィンドウにウィンドウを閉じるメッセージ(WM_QUIT)を発行しています。引数は整数値で、WM_QUITに付随するパラメーターを送り込みます。QUITでは、基本的に0を送り込みます。
それ以外の処理については、
return DefWindowProc(hWnd, msg, wParam, lParam);
で処理を行っています。このDefWindowProcもプロシージャなので、引数と戻り値は必ず一致します。そのため、上から流れて来た引数をそのまま送り込めば、自動的に下部で処理を行ってくれます。
DefWindowProcでは基本的な処理を自動的にやってくれます。
以上で本当に終了です、お疲れさまでした。
3.結び
これで基本的なウィンドウを作成することができるようになりました。この後の処理は基本的にこの土台の上で処理が行われて行く事になります。
次回は実際にWM_を使用して、クリックに対する処理などを行っていきます。