ウィンドウプロシージャ

メッセージの処理

前ページではGetMessage関数でメッセージを取得し、DispatchMessage関数で取得したメッセージを送信するコードを書きました。
このメッセージが送信される先はウィンドウプロシージャという特殊な関数です。

ウィンドウプロシージャはウィンドウに関連付けられています。
関連付けはWNDCLASS(WNDCLASSEX)構造体のlpfnWndProcメンバで設定します。
今まではDefWindowProcというあらかじめ用意されている関数をウィンドウプロシージャとして設定していました。


ATOM MyRegisterClass(HINSTANCE hInstance)
{
	WNDCLASSEX wcex;
	wcex.cbSize = sizeof(WNDCLASSEX);
	wcex.style = CS_HREDRAW | CS_VREDRAW;
	wcex.lpfnWndProc = DefWindowProc;
	//↑これ

DefWindowProc関数はウィンドウに対する基本的な動作を提供します。
(ウィンドウの移動やサイズの変更など)
これだけではアプリケーション毎に異なる動作をさせることができないので、独自のウィンドウプロシージャを定義してメッセージを適切に処理します。

ウィンドウプロシージャの定義

まずは全体のサンプルコードを示します。


#include <windows.h>

//グローバル変数
HINSTANCE hInst;	//インスタンス
WCHAR szTitle[] = L"テストアプリ";		//タイトル
WCHAR szWindowClass[] = L"MyTestApp";	//ウィンドウクラス名

//関数プロトタイプ宣言
ATOM MyRegisterClass(HINSTANCE);
BOOL InitInstance(HINSTANCE, int);

//ウィンドウプロシージャのプロトタイプ宣言
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int APIENTRY wWinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPWSTR lpCmdLine, 
	int nCmdShow)
{
	MyRegisterClass(hInstance);
	if (!InitInstance(hInstance, nCmdShow))
	{
		return 0;
	}

	MSG msg;
	BOOL ret;

	//メッセージループ
	while ((ret = GetMessage(&msg, NULL, 0, 0)) != 0)
	{
		if (ret == -1)
		{
			//GetMessage関数の実行失敗
			return -1;
		}
		//if (msg.message == WM_LBUTTONUP)
		//	break;
		DispatchMessage(&msg);
	}

	//WM_QUITメッセージを取得するとここに処理が移る
	return (int)msg.wParam;
}

//ウィンドウクラスの登録
ATOM MyRegisterClass(HINSTANCE hInstance)
{
	WNDCLASSEX wcex;
	wcex.cbSize = sizeof(WNDCLASSEX);
	wcex.style = CS_HREDRAW | CS_VREDRAW;
	//wcex.lpfnWndProc = DefWindowProc;
	wcex.lpfnWndProc = WndProc; //変更
	wcex.cbClsExtra = 0;
	wcex.cbWndExtra = 0;
	wcex.hInstance = hInstance;
	wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
	wcex.lpszMenuName = NULL;
	wcex.lpszClassName = szWindowClass;
	wcex.hIconSm = NULL;

	return RegisterClassEx(&wcex);
}

//ウィンドウの作成
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
	hInst = hInstance;

	HWND hWnd = CreateWindow(
		szWindowClass, szTitle,
		WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, CW_USEDEFAULT,
		CW_USEDEFAULT, CW_USEDEFAULT,
		NULL,
		NULL,
		hInstance,
		NULL
	);

	if (!hWnd) {
		return FALSE;
	}
	ShowWindow(hWnd, nCmdShow);
	return TRUE;
}

//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message) 
	{
	case WM_LBUTTONUP: //左クリック
		MessageBox(hWnd, L"左クリックしました。", szTitle, MB_OK);
		break;
	case WM_DESTROY: //ウィンドウの破棄
		MessageBox(hWnd, L"終了します。", szTitle, MB_OK);
		PostQuitMessage(0);
		break;
	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

このコードの一番最後に定義されている関数がウィンドウプロシージャです。

LRESULT CALLBACK WndProc(
 HWND,
 UINT,
 WPARAM,
 LPARAM
){}
ウィンドウプロシージャの関数定義。

ウィンドウプロシージャは引数と戻り値が決められていて、これに従う必要があります。
また、関数名の前にCALLBACKというキーワードが必要です。
関数名は自由です。

CALLBACKが付けられた関数はコールバック関数と言い、システムが呼び出す関数です。
呼び出されるタイミングはシステムが自動的に決定します。

プログラマがウィンドウプロシージャを明示的に実行することはありません。
ウィンドウにメッセージが送られてきたときに自動的に実行されます。

WNDCLASSEX構造体のlpfnWndProcメンバを、DefWindowProc関数から自作のWndProc関数に変更します。
これでDispatchMessage関数は自作のWndProc関数にメッセージを送信するようになります。

ちなみに、前ページのサンプルコードでは、DispatchMessage関数の手前にマウスクリックを検知してプログラムを終了するコードを書いていましたが、 終了の処理はウィンドウプロシージャ内で行うように変更するので削除しておいてください。


//メッセージループ
while ((ret = GetMessage(&msg, NULL, 0, 0)) != 0)
{
	if (ret == -1)
	{
		return -1;
	}
	//↓いらない
	//if (msg.message == WM_LBUTTONUP)
	//	break;

	DispatchMessage(&msg);
}

ウィンドウプロシージャ内の処理

ウィンドウプロシージャ内では、送られてきたメッセージに対する処理を記述します。


LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message) 
	{
	case WM_LBUTTONUP: //左クリック
		MessageBox(hWnd, L"左クリックしました。", szTitle, MB_OK);
		break;
	case WM_DESTROY: //ウィンドウの破棄
		MessageBox(hWnd, L"終了します。", szTitle, MB_OK);
		PostQuitMessage(0);
		break;
	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

引数

引数hWndは、メッセージが送られてきたウィンドウのウィンドウハンドルです。

引数messageは送られてきたメッセージです。
整数値ですが、各メッセージに対応した定数が定義されているのでそれを使用します。
この値をswitch文に指定して処理を振り分けます。
サンプルコードでは二種類のメッセージを処理しています。

引数wParamlParamはメッセージに対する追加の情報で、メッセージの種類によって意味が変わります。

引数の数とデータ型、および戻り値の型は固定ですが、引数名は自由です。

WM_LBUTTONUPメッセージ

WM_LBUTTONUPメッセージは前ページでも登場しましたが、マウス左クリック時に送られるメッセージです。
ここではメッセージボックスを表示しています。
(この処理はメッセージの説明のために書いているだけなので、削除してもかまいません)

MessageBox関数の第一引数をNULLからhWndに変更していることに注目してください。
第一引数にウィンドウハンドルを指定すると、そのウィンドウがメッセージボックスのオーナーウィンドウとなります。

このメッセージボックスは常にオーナーウィンドウよりも手前に表示され、オーナーウィンドウはメッセージボックスを閉じるまで操作できなくなります。

WM_DESTROYメッセージ

WM_DESTROYメッセージはウィンドウの破棄です。
これはウィンドウが閉じられ、破棄されたに送られてくるメッセージです。
WM_DESTROYメッセージにが送られてきた場合、通常はPostQuitMessage関数を実行します。

PostQuitMessage関数

一般的なウィンドウアプリでは、メインのウィンドウが閉じられるとプログラムは終了します。
しかし内部的には「メインウィンドウの破棄」と「プログラムの終了」は別の動作です。
メインウィンドウを閉じただけではプログラムは終了しないので、適切に終了処理を行わないと「ウィンドウが無いのにプログラムは動作し続ける」という状態になってしまいます。
(このことを利用したプログラムにすることもできます)

ウィンドウプロシージャ内でプログラムを終了するにはPostQuitMessage関数を実行します。

void PostQuitMessage(
 int nExitCode
);
スレッドのメッセージキューにWM_QUITメッセージを送信する。

PostQuitMessage関数は即座にプログラムを終了するのではなく、メッセージキューにWM_QUITメッセージを追加します。
GetMessage関数がWM_QUITメッセージを取得すると、メッセージループを抜けてプログラムは終了します。

引数nExitCodeはGetMessage関数が取得するMSG構造体のwParamメンバにセットされる値です。
これは通常はプログラムの終了コードとして使用されます。
(0は正常終了を意味します)

return文

ウィンドウプロシージャの戻り値はLRESULT型となっているので、return文で値を返す必要があります。
戻り値は基本的にメッセージに対して何か処理をした場合は0を指定するという決まりがあります。
そして、何も処理をしなかった場合はDefWindowProc関数を実行し、その戻り値をreturn文に指定するという決まりがあります。

サンプルコードでは、switch文のdefault句でDefWindowProc関数を実行し、その戻り値を直接return文に指定しています。
DefWindowProc関数の引数はウィンドウプロシージャの引数をそのまま指定するだけです。

何かメッセージを処理した場合はbreak文が実行され、switch文を抜けて最後の「return 0;」に到達します。

上に書いた二つの決まり通りならウィンドウプロシージャの書き方は自由です。
自分がわかりやすいと思う方法で構いません。


//switch文中でbreakし、最後に0を返すタイプ
//switch文に引っ掛からないメッセージはdefault句でDefWindowProc関数を実行
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message) 
	{
	case WM_DESTROY: //ウィンドウの破棄
		MessageBox(hWnd, L"終了します。", szTitle, MB_OK);
		PostQuitMessage(0);
		break;
	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

//switch文中で0を返すタイプ
//switch文に引っ掛からないメッセージは関数末尾でDefWindowProc関数を実行
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message) 
	{
	case WM_DESTROY: //ウィンドウの破棄
		MessageBox(hWnd, L"終了します。", szTitle, MB_OK);
		PostQuitMessage(0);
		return 0;
	}
	return DefWindowProc(hWnd, message, wParam, lParam);
}

ウィンドウプロシージャは通常の関数と同じように、関数の先頭から処理を行い、関数の末尾に到達して処理を終了します。
この処理をメッセージが送られてくる度に何度も繰り返します。
ひとつのメッセージの処理毎に関数の実行と終了を繰り返すので、ローカル変数はその度に生成と破棄が行われます。
なので、あるメッセージの処理で得たデータを別のメッセージの処理で使いたい場合はグローバル変数やstatic変数などに保存しておく必要があります。

ウィンドウアプリの基本形の完成

ここまでのコードで、ウィンドウアプリの基本的な形は完成しました。
ウィンドウクラスの登録ウィンドウの作成メッセージループウィンドウプロシージャの四つがWindows APIによるウィンドウアプリ開発に共通の手順です。

ウィンドウアプリはコンソールアプリに比べて出来ることが多いので、「ウィンドウを表示して、アプリを終了する」だけのコードでもこれだけの分量が必要になります。
これを一気に理解するのは大変ですが、それぞれの処理自体はそこまで難しいものではないと思います。

まだウィンドウを作っただけで実用的なアプリは作れません。
次ページからはもう少し実用的なアプリを作るための機能を解説します。

複数のウィンドウを持つアプリ

以下は余談的な話です。
とりあえずは必要のない情報なので読み飛ばしてもかまいません。

アプリケーションは複数のウィンドウを同時に持つことができます。
二つのウィンドウを使用する場合、ウィンドウクラスとウィンドウプロシージャをもう一つ別に用意します。
(CreateWindow関数も二回実行します)


#include <windows.h>

HINSTANCE hInst;	//インスタンス
WCHAR szTitle[] = L"テストアプリ";		//タイトル
WCHAR szWindowClass[] = L"MyTestApp";	//ウィンドウクラス名
WCHAR szWindowClass2[] = L"MyTestApp_Window2";	//ウィンドウクラス名2

//関数プロトタイプ宣言
ATOM MyRegisterClass(HINSTANCE, WNDPROC, LPCWSTR);
BOOL InitInstance(HINSTANCE, int);

//ウィンドウプロシージャのプロトタイプ宣言
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK WndProc2(HWND, UINT, WPARAM, LPARAM); //ウィンドウ2

int APIENTRY wWinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPWSTR lpCmdLine,
	int nCmdShow)
{
	//メインのウィンドウクラス登録
	MyRegisterClass(hInstance, WndProc, szWindowClass);
	//ウィンドウ2も登録
	MyRegisterClass(hInstance, WndProc2, szWindowClass2);

	//メインウィンドウ作成
	if (!InitInstance(hInstance, nCmdShow))
	{
		return 0;
	}

	MSG msg;
	BOOL ret;

	//メッセージループ
	while ((ret = GetMessage(&msg, NULL, 0, 0)) != 0)
	{
		if (ret == -1)
		{
			//GetMessage関数の実行失敗
			return -1;
		}
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

//ウィンドウクラスの登録
ATOM MyRegisterClass(HINSTANCE hInstance, WNDPROC wndProc, LPCWSTR windowClass) //引数の追加
{
	WNDCLASSEX wcex;
	wcex.cbSize = sizeof(WNDCLASSEX);
	wcex.style = CS_HREDRAW | CS_VREDRAW;
	//変更
	wcex.lpfnWndProc = wndProc;
	wcex.cbClsExtra = 0;
	wcex.cbWndExtra = 0;
	wcex.hInstance = hInstance;
	wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
	wcex.lpszMenuName = NULL;
	//変更
	wcex.lpszClassName = windowClass;
	wcex.hIconSm = NULL;

	return RegisterClassEx(&wcex);
}

//メインウィンドウの作成
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
	hInst = hInstance;

	HWND hWnd = CreateWindow(
		szWindowClass, szTitle,
		WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, CW_USEDEFAULT,
		CW_USEDEFAULT, CW_USEDEFAULT,
		NULL,
		NULL,
		hInstance,
		NULL
	);

	if (!hWnd) {
		return FALSE;
	}
	ShowWindow(hWnd, nCmdShow);
	return TRUE;
}

//ウィンドウプロシージャ1
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	HWND h;

	switch (message)
	{
	case WM_LBUTTONUP: //左クリック
		//ウィンドウ2を作る
		h = CreateWindow(
			szWindowClass2, L"ウィンドウ2",
			WS_OVERLAPPEDWINDOW,
			CW_USEDEFAULT, CW_USEDEFAULT,
			CW_USEDEFAULT, CW_USEDEFAULT,
			NULL,
			NULL,
			hInst,
			NULL
		);
		ShowWindow(h, SW_SHOW);
		break;

	case WM_DESTROY: //ウィンドウの破棄
		PostQuitMessage(0);
		break;

	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

//ウィンドウプロシージャ2
LRESULT CALLBACK WndProc2(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message)
	{
	case WM_LBUTTONUP: //左クリック
		MessageBox(hWnd, L"ウィンドウ2上で左クリックしました。", szTitle, MB_OK);
		break;

	//WM_DESTROYメッセージは処理しない
	//case WM_DESTROY: //ウィンドウの破棄
	//	PostQuitMessage(0);
	//	break;

	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

複数のウィンドウを持つアプリ

ウィンドウクラスの登録は最初にWinMain関数内で行っておきます。
MyRegisterClass関数は引数を変更してウィンドウプロシージャとウィンドウクラス名を任意のものを指定できるようにしています。
(あまり自由度は高くないので「MyRegisterClass2」などの関数を別に作っても良いです)

二枚目のウィンドウの表示タイミングは任意ですが、ここでは最初はメインウィンドウだけを表示することにします。
メインウィンドウ上を左クリック(WM_LBUTTONUPメッセージ)したときに、CreateWindow関数を実行して二枚目のウィンドウを作成し、表示します。

二枚目のウィンドウはメインウィンドウとは別のウィンドウプロシージャでメッセージを処理します。
このウィンドウプロシージャ「WndProc2」ではWM_DESTROYメッセージは処理しません。
ここでメインウィンドウと同じようにPostQuitMessage関数を実行すると、ウィンドウ2を閉じたときにもアプリケーション全体が終了してしまいます。
(それで構わない場合は残しておいても良い)

ウィンドウ2を複数開かないようにする

上のコードは複数ウィンドウの起動チェック処理はしていないので、メインウィンドウをクリックする度に同じウィンドウが何枚も表示されます。
それで不都合な場合は何らかの方法でウィンドウ2が存在するかをチェックする必要があります。


//ウィンドウプロシージャ1
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	//static変数に変更
	static HWND hWnd2;

	switch (message)
	{
	case WM_LBUTTONUP: //左クリック
		//ウィンドウ2が開かれているかチェック
		if (FindWindow(szWindowClass2, NULL))
			break;

		//ウィンドウ2を作る
		hWnd2 = CreateWindow(
			szWindowClass2, L"ウィンドウ2",
			WS_OVERLAPPEDWINDOW,
			CW_USEDEFAULT, CW_USEDEFAULT,
			CW_USEDEFAULT, CW_USEDEFAULT,
			NULL,
			NULL,
			hInst,
			NULL
		);
		ShowWindow(hWnd2, SW_SHOW);
		break;

	case WM_DESTROY: //ウィンドウの破棄
		PostQuitMessage(0);
		break;

	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

ここではFindWindowという関数を使用してウィンドウ2が存在するか否かをチェックしています。

もっと簡単な方法としては、ウィンドウ2のウィンドウハンドルを格納する変数をグローバル変数にし、ウィンドウ作成時に格納、ウィンドウ破棄時に空にする、という手順でも実現できます。


//グローバル変数
HWND hWnd2;

//ウィンドウプロシージャ1
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message)
	{
	case WM_LBUTTONUP: //左クリック
		//空じゃなければ処理しない
		if (hWnd2 != NULL)
			break;

		//ウィンドウ2を作る
		hWnd2 = CreateWindow(
			szWindowClass2, L"ウィンドウ2",
			WS_OVERLAPPEDWINDOW,
			CW_USEDEFAULT, CW_USEDEFAULT,
			CW_USEDEFAULT, CW_USEDEFAULT,
			NULL,
			NULL,
			hInst,
			NULL
		);
		ShowWindow(hWnd2, SW_SHOW);
		break;

	case WM_DESTROY: //ウィンドウの破棄
		PostQuitMessage(0);
		break;

	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

//ウィンドウプロシージャ2
LRESULT CALLBACK WndProc2(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message)
	{
	case WM_LBUTTONUP: //左クリック
		MessageBox(hWnd, L"ウィンドウ2上で左クリックしました。", szTitle, MB_OK);
		break;

	case WM_DESTROY:
		//空にしておく
		hWnd2 = NULL;
		break;

	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

この方法はウィンドウハンドルをグローバル変数に格納しなければならないのが欠点です。

ひとつのウィンドウクラスで複数のウィンドウを開く

上記の例ではウィンドウクラスを複数用意していますが、ひとつのウィンドウクラスで複数のウィンドウを開くこともできます。
しかしその場合、ウィンドウプロシージャもひとつになるので、そのままではどのウィンドウでも同じ処理しかできません。
ウィンドウ毎に別の処理をするには、ウィンドウプロシージャで現在処理しているウィンドウを何らかの方法で識別し、処理を振り分ける必要があります。

これはMDI子ウィンドウの操作#ウィンドウ毎に独立したデータを扱うの項で改めて説明します。