EternalWindows
ウインドウ管理 / メッセージループ

ウインドウを作成してそれを表示すれば、ユーザーはウインドウに対して マウスやキーボードなどを使ってイベントを発生させることができます。 ご存知の通り、このイベントはメッセージという形でウインドウプロシージャに送られる ことになるのですが、正確にはそれに至るまでいくつかの手順を踏むことになります。 1つの例を挙げて考えてみましょう。

今、ユーザーがウインドウ上でマウスの左ボタンを押したとして、 ウインドウプロシージャにWM_LBUTTONDWONが送られたとします。 このウインドウは、WM_LBUTTONDWONで非常に時間の掛かる処理を行うものとし、 その処理を終える前にユーザーは、今度はマウスの右ボタンを押したとします。 このとき、システムはウインドウにWM_RBUTTONDWONを送らねばなりませんが、 現在このウインドウはWM_LBUTTONDWONを処理しているため、ウインドウプロシージャの呼び出しで、 WM_RBUTTONDWONの処理が割り込まれるようなことがあってはいけません。 このような問題を回避するために、システムはアプリケーション(正確にはスレッド)の メッセージキューと呼ばれる領域にメッセージを格納し、 それをアプリケーション自身が取得することで、 アプリケーションにメッセージ処理のタイミングを決定できるようにさせています。 メッセージを取得するには、GetMessageを呼び出します。

BOOL GetMessage(
  LPMSG lpMsg,
  HWND hWnd,
  UINT wMsgFilterMin,
  UINT wMsgFilterMax
);

lpMsgは、MSG構造体のアドレスを指定します。 関数から制御が返ると、この構造体に取得したメッセージの情報が格納されます。 hWndは、メッセージを取得するウインドウのハンドルを指定します。 NULLを指定すると、呼び出し側スレッドに関連付けられている全ての ウインドウへのメッセージを取得できます。 wMsgFilterMinとwMsgFilterMaxは、取得対象のメッセージの範囲を指定します。 両方の引数に0を指定した場合、全てのメッセージが取得されることになります。 戻り値は、WM_QUIT以外のメッセージを取得した場合は0以外の値が返り、 WM_QUITを取得した場合は0が返ります。 引数のアドレスが無効である場合などは、-1が返ることもあります。

メッセージを取得したら、次はそれをウインドウプロシージャに送らなければなりません。 このことをメッセージのディスパッチと呼び、DispatchMessageで行うことになります。 DispatchMessageは、内部でMSG構造体のhwndメンバを用いて ウインドウプロシージャのアドレスを取得する関数を呼び出し、 その後、MSG構造体のメンバを引数としてウインドウプロシージャを呼び出します。

LRESULT DispatchMessage(
  CONST MSG *lpmsg
);

lpmsgは、MSG構造体のアドレスを指定します。 一般には、GetMessageで初期化したMSG構造体をそのまま指定することになるでしょう。 戻り値は、ウインドウプロシージャが返した値となります。

メッセージの取得とディスパッチの方法を見てきたところで、 この2つの動作とメッセージキューの仕組みを今一度確認してみましょう。 今、ウインドウでマウスの左ボタンが押されたとして、 システムはそのウインドウが属するアプリケーションの メッセージキューにWM_LBUTTONDOWNを格納します。 アプリケーションは、この格納されたメッセージをGetMessageで取得し、 それをDispatchMessageでウインドウプロシージャに送ることになります。 このとき、たとえウインドウに新たなイベントが発生してメッセージが生成されたとしても、 システムはそれをメッセージキューに格納するだけですから、このメッセージが処理されるのは、 再びアプリケーションがGetMessageとDispatchMessageを呼び出すときとなります。

これらのことから分かるように、アプリケーションがメッセージを処理するためには、 絶えずGetMessageとDispatchMessageを呼び出さなければなりませんから、 この2つの処理はループ文として実行することになります。

while (GetMessage(&msg, NULL, 0, 0) > 0) {
	TranslateMessage(&msg); // この関数の呼び出しは必須ではない
	DispatchMessage(&msg);
}

このようなループ文は特別にメッセージループと呼ばれ、GetMessageがWM_QUITを取得したとき、 またはGetMessageがエラーを発生させるまでループが続くよう設計されています。 WM_QUITは、いわばアプリケーションの終了を促すためのメッセージで、 PostQuitMessageという関数を呼び出すことでメッセージキューに格納されます。 この関数は一般にWM_DESTROYで送られることになるため、 ウインドウが破棄されたときにはメッセージループから抜けることになります。 なお、WM_QUITがメッセージループの終了を意味することから、 このメッセージがウインドウプロシージャに送られることはありません。

さて、残る問題はこのメッセージループのコードを何処に書くべきであるかです。 GetMessageがメッセージキューからメッセージを取得する設計であることを考えると、 それを呼び出すべきタイミングはメッセージキューにメッセージがある場合のみに 限られるように思えますが、そうではありません。 GetMessageは、メッセージキューにメッセージが存在しない場合は、 メッセージがメッセージキューに格納されるまで制御を返さない設計になっているため、 アプリケーションがGetMessageを呼び出すタイミングを特定する必要はないのです。 しかし、ウインドウをまだ表示していない状態ではユーザーはウインドウを操作できず、 メッセージは生成されませんから、表示を終えた後に記述するとよいでしょう。

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	・
	・
	・

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

このように、WinMainにメッセージループのコードを書くことは、 単にメッセージを取得してディスパッチするというだけでなく、 アプリケーションの終了を防いでいるという見方もできるでしょう。 WinMainから制御を返せばアプリケーションは終了してしまいますから、 何らかの仕組みでコードの進行を止めなければなりません。 この仕組みというのがメッセージループであり、それはコードの進行を止めるだけでなく、 ウインドウへのメッセージを処理するための中継にもなっているのです。

WinMainの戻り値は、アプリケーションの終了コードになります。 この終了コードはWM_DESTROYで実行するPostQuitMessageに指定することになっており、 GetMessageがWM_QUITを取得して0を返した場合は、 MSG構造体のwParamに終了コードが格納されることになっています。 よって、この値をWinMainで返すことになります。 終了コードの値は自由に決定でき、 特定の値を返したからといってシステムが何らかの動作を行うことはありません。

TranslateMessageについて

メッセージループで呼ばれているTranslateMessageは、キーボードメッセージから文字メッセージを生成するために存在します。 通常、アプリケーションがキーを押すと、WM_KEYDOWNというキーボードメッセージが送られ、 たとえばAキーならwParamにはAキーの仮想キーコードである0x41が格納されます。 しかし、仮想キーコードだけは、押したAが大文字のAなのか小文字のaなのか分かりませんから、 このような場合は文字コードをwParamとして持つ文字メッセージが必要になります。 TranslateMessageがWM_KEYDOWNを検出すると、文字メッセージであるWM_CHARがメッセージキューに格納され、 次にGetMessageを呼び出した際に取得されることになります。



戻る