WisdomSoft - for your serial experiences.

ウィンドウプロシージャ

ユーザーの入力などに応じてウィンドウが独自の処理を実行するには、ウィンドウプロシージャと呼ばれる関数を記述し、ポインタをウィンドウクラスに登録します。アプリケーションに送られたメッセージは、登録したウィンドウプロシージャに転送されます。

新しいウィンドウプロシージャを作成する

Windows API では、ウィンドウに送られたメッセージを処理する関数のことをウィンドウプロシージャと呼んでいます。ウィンドウプロシージャは WNDCLASS 構造体の lpfnWndProc メンバに指定することでウィンドウと関連付けられます。ウィンドウプロシージャは WNDPROC 型として次のように宣言されています。

WNDPROC 宣言
typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);

この関数型に一致する関数は、ウィンドウプロシージャとしてウィンドウクラスに登録できます。

一般に、ウィンドウプロシージャは以下のような関数として宣言されます。前述した DefWindowProc() 関数もウィンドウプロシージャなので、以下の宣言と同じ関数型であることが確認できます。

ウィンドウプロシージャ
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

関数名は自由に命名できますが、戻り値とパラメータ型はこれを守らなければなりません。

ウィンドウプロシージャの hwnd パラメータにメッセージが送られたウィンドウのハンドル、uMsg パラメータにメッセージ、そして wParam パラメータと lParam パラメータにメッセージのパラメータが渡されます。これは MSG 構造体の先頭 4 つのメンバと同じものです。

戻り値の LRESULT 型はメッセージを処理した結果を表す整数型です。ウィンドウプロシージャの戻り値は メッセージによって異なりますが、一部のメッセージを除いて、そのほとんどが意味のある値ではありません。

開発者は、アプリケーションのウィンドウに固有のウィンドウプロシージャを用意し、受け取った uMsg パラメータの値を WM_ から始まる定数値と比較し、処理したいメッセージかどうかを調べます。if 文でも比較できますが、メッセージの数が非常に多いため switch 文を使ってメッセージごとの case ラベルを作成する書き方が推奨されます。

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch(uMsg) 
	{
	case メッセージ1:
		/*メッセージ処理*/;

	case メッセージ2:
		/*メッセージ処理*/;

	default:
		return DefWindowProc(hwnd , uMsg , wParam , lParam);
	}

	return 0;
}

一般的なウィンドウプロシージャは上記のように switch 文で uMsg パラメータの値を定数と比較し、一致した定数のコードを実行します。

新しいウィンドウプロシージャを作るときは、基本的な動作を処理することを忘れてはいけません。ウィンドウの移動やサイズ変更などの基本動作も、ウィンドウプロシージャで処理しなければならないのです。メッセージを解析して、必要なすべての処理を独自に実現するとすれば、かなりの負担になってしまいます。そこで、アプリケーションにとって興味のないメッセージを DefWindowProc() 関数に送り、基本的なウィンドウの動作を実現します。

ウィンドウプロシージャはウィンドウクラスの lpfnWndProc メンバに設定することで、ウィンドウの実体と関連付けられます。ウィンドウはメッセージが送られると、ウィンドウクラスに登録されているウィンドウプロシージャにメッセージを転送します。

NDCLASSEX wcx;
wcx.lpfnWndProc = WindowProc;

通常、メッセージをウィンドウプロシージャに送るには、直接呼び出すのではなく DispatchMessage() 関数を使います。ウィンドウに設定されているウィンドウプロシージャは、ウィンドウクラスによって異なりますが DispatchMessage() 関数はメッセージを適切なウィンドウプロシージャに送出してくれます。

DispatchMessage() 関数
LONG DispatchMessage(CONST MSG *lpmsg);

lpmsg パラメータに処理するメッセージのポインタを指定します。関数はメッセージに含まれているウィンドウのハンドルから、メッセージを適切なウィンドウプロシージャに送ります。DispatchMessage() 関数の戻り値は、メッセージが送出されたウィンドウプロシージャの戻り値が返されます。通常、この関数の戻り値は無視します。

ウィンドウプロシージャは、ウィンドウの破棄を適切に行わなければなりません。ウィンドウの終了ボタンが押されると WM_CLOSE メッセージが発行されます。このメッセージはウィンドウを閉じることを要求するもので、DefWindowProc() 関数がこのメッセージを受け取ると DestoryWindow() 関数を用いてウィンドウを破棄します。WM_CLOSE メッセージのパラメータはありません。

DestroyWindow() 関数によってウィンドウが破棄されると WM_DESTROY メッセージが送られます。WM_DESTROY メッセージはウィンドウが破棄されたことを通知するメッセージで、パラメータはなく、常に 0 を返します。

もし、アプリケーションウィンドウが破棄された場合は、メッセージループから抜け出しアプリケーションを終了させる必要があります。これを行うために、アプリケーションウィンドウが閉じられた場合は PostQuitMessage() 関数を呼び出して、アプリケーションの終了を通知します。

PostQuitMessage() 関数
VOID PostQuitMessage(int nExitCode);

nExitCode には、終了コードとなる整数値を指定します。この関数が呼び出されると WM_QUIT メッセージがキューに送られます。WM_QUIA メッセージはアプリケーションの終了を表し、wParam パラメータに終了コードを格納しています。WM_QUIA メッセージはウィンドウプロシージャで処理するメッセージではなく GetMessage() 関数にメッセージループの終了を通知するためのメッセージです。

GetMessage() 関数は WM_QUIA メッセージを受け取ると 0 を返します。そこで、メッセージループでは GetMessage() 関数の戻り値を調べ、その結果が 0 より上ならばウィンドウプロシージャにメッセージを送り、そうでなければメッセージループを終了するようにプログラムします。

ここまでの内容はウィンドウプログラミングのベースとなる部分で、多くのプログラマは開発環境が生成するコードを再利用したり、他のコードからコピー&ペーストして組み立てます。しかし、厳密な入出力を求められるゲームプログラミングでは、プログラムから意図的に入出力のメッセージを再現するなどの処理が求められることもあるかもしれないため、その原理を知り尽くすことは重要です。

コード1
#include <windows.h>
#define WINDOWS_CLASS_NAME TEXT("WisdomSoft.Sample.Window")

LRESULT CALLBACK SampleWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch(uMsg) 
	{
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	default:
		return DefWindowProc(hwnd , uMsg , wParam , lParam);
	}

	return 0;
}


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
	HWND window;
	WNDCLASSEX wcx;
	int returnCode = 0;

	wcx.cbSize = sizeof(WNDCLASSEX);
	wcx.style = CS_HREDRAW | CS_VREDRAW;
	wcx.lpfnWndProc = SampleWindowProc;
	wcx.cbClsExtra = 0;
	wcx.cbWndExtra = 0;
	wcx.hInstance = hInstance;
	wcx.hIcon =  NULL;
	wcx.hCursor = NULL;
	wcx.hbrBackground = (HBRUSH)COLOR_BACKGROUND + 1;
	wcx.lpszMenuName = NULL;
	wcx.lpszClassName = WINDOWS_CLASS_NAME;
	wcx.hIconSm = NULL;

	if (!RegisterClassEx(&wcx)) 
	{
		OutputDebugString(TEXT("Error: ウィンドウクラスの登録ができません。\n"));
		return 0;
	}

	window = CreateWindowEx(
		WS_EX_LEFT, WINDOWS_CLASS_NAME, TEXT("Window Title"),
		WS_OVERLAPPEDWINDOW | WS_VISIBLE,
		CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
		NULL, NULL, hInstance ,NULL
	);
	if (!window)
	{
		OutputDebugString(TEXT("Error: ウィンドウが作成できません。\n"));
		return 0;
	}
	
	while(TRUE)
	{
		MSG msg;
		int r = GetMessage(&msg, NULL, 0, 0);
		if (r > 0) DispatchMessage(&msg);
		else if(r == -1)
		{
			OutputDebugString(TEXT("Error: メッセージの取得に失敗しました。\n"));
			break;
		}
		else
		{
			returnCode = msg.wParam;
			break;
		}
	}

	return returnCode;
}
実行結果
コード2 実行結果

コード1は、何もしない単純なウィンドウを表示するだけのプログラムです。SampleWindowProc() 関数をウィンドウプロシージャとしてウィンドウクラスに設定することで、DispatchMessage() 関数を通してメッセージが送られてきます。

SampleWindowProc() 関数では、ウィンドウが削除されたことを表す WM_DESTROY メッセージだけを処理しています。WM_DESTROY が送られてくると PostQuitMessage() 関数を呼び出してアプリケーションを終了させることを通知します。これによって WM_QUIT メッセージが発生し、GetMessage() 関数が 0 を返します。メッセージループを抜け出してプログラムを終了するとき、MSG 構造体の wParam メンバの値を返していますが、このメンバには PostQuitMessage() 関数の引数に指定した値が格納されています。

SampleWindowProc() 関数では終章処理しか記述していませんが、プログラムはウィンドウの移動やサイズ変更などの基本的な動作をサポートしています。ウィンドウの移動処理などは記述していませんが、WM_DESTROY 以外のメッセージが送られてくると、メッセージをそのまま DefWindowProc() 関数に渡しているためです。

コード1の SampleWindowProc() 関数を以下のように書き換え、送られてくるメッセージを Visual Studio の出力ウィンドウに表示すれば、ウィンドウが多くのメッセージを瞬時に処理していることが視覚的に確認できます。

LRESULT CALLBACK SampleWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
#ifdef _DEBUG
	TCHAR debugText[1024];
	wsprintf(debugText, TEXT("uMsg=%d, wParam=%d, lParam=%d\n"), uMsg, wParam, lParam);
	OutputDebugString(debugText);
#endif

	switch(uMsg) 
	{
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	default:
		return DefWindowProc(hwnd , uMsg , wParam , lParam);
	}

	return 0;
}
図1 ウィンドウプロシージャに送られたメッセージ
図1 ウィンドウプロシージャに送られたメッセージ

特にマウスカーソルをウィンドウ上で動かすと、マウスカーソルの動きが検知されるたびにメッセージが送られてきます。