EternalWindows
コントロール基礎 / サブクラス化

ウインドウのサブクラス化とは、 ウインドウのウインドウプロシージャを別のものに書き換えることです。 これまで述べてきたように、ボタンなどのコントロールは全てウインドウであり、 独自のウインドウプロシージャを持っています。 このウインドウプロシージャをアプリケーションが用意した別のウインドウプロシージャに書き換えた場合、 アプリケーションはコントロールに対する全てのメッセージを取得できますから、 コントロールの動作を拡張したい場合は非常に便利です。 コントロールへの一部の操作は、WM_COMMANDという形で親ウインドウに通知されるため、 通知されない処理を検出したい場合にだけ、サブクラス化を使用することになります。

ウインドウプロシージャの書き換えは、SetWindowLongPtrで行うことができます。 この関数は、ウインドウスタイルなど様々な情報を設定することができる関数であり、 GWLP_WNDPROCを指定した場合はウインドウプロシージを設定する意味になります。

SetWindowLongPtr(hwndButton, GWLP_WNDPROC, (LONG_PTR)NewButtonProc);

これにより、第1引数のウインドウのウインドウプロシージャは、 第3引数のものに書き換えられたことになります。 よって、ウインドウへのメッセージはNewButtonProcに送られることになるのですが、 このNewButtonProcの実装は、具体的にはどのようになるのでしょうか。 検出したいメッセージを処理するのは当然のことですが、 それ以外のメッセージについてはデフォルトの処理を行わなければなりませんから、 この方法について考える必要があります。

g_lpfnDefButtonProc = (WNDPROC)GetWindowLongPtr(hwndButton, GWLP_WNDPROC);
SetWindowLongPtr(hwndButton, GWLP_WNDPROC, (LONG_PTR)NewButtonProc);

まず確かなのは、ウインドウプロシージャを書き換えたことによって、 ウインドウのデフォルトの処理が失われたということです。 ただし、これは言い換えれば、書き換える前のウインドウプロシージャはデフォルトの実装を持っていることも意味します。 よって、この元のウインドウプロシージャを予め保存しておき、 これを新しいウインドウプロシージャ内で呼び出せば、 デフォルトの処理を実行できることになります。 ウインドウプロシージャの取得は、GetWindowLongPtrで行います。

元のウインドウプロシージャを呼び出しは、CallWindowProcを通じて行います。

LRESULT CallWindowProc(
  WNDPROC lpPrevWndFunc,
  HWND hWnd,
  UINT Msg,
  WPARAM wParam,
  LPARAM lParam
);

lpPrevWndFuncは、元のウインドウプロシージャを指定します。 hWndは、ウインドウハンドルを指定します。 Msgは、メッセージを指定します。 wParamは、WPARAM値を指定します。 lParamは、LPARAM値を指定します。

今回のプログラムは、ボタンをサブクラス化し、ボタン上におけるマウスの右ボタンの押下を検出します。

#include <windows.h>

#define ID_BUTTON 100

WNDPROC g_lpfnDefButtonProc = NULL;

LRESULT CALLBACK NewButtonProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

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

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static HWND hwndButton = NULL;

	switch (uMsg) {
		
	case WM_CREATE:
		hwndButton = CreateWindowEx(0, TEXT("BUTTON"), TEXT("ボタン"), WS_CHILD | WS_VISIBLE, 30, 30, 80, 40, hwnd, (HMENU)ID_BUTTON, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		g_lpfnDefButtonProc = (WNDPROC)GetWindowLongPtr(hwndButton, GWLP_WNDPROC);
		SetWindowLongPtr(hwndButton, GWLP_WNDPROC, (LONG_PTR)NewButtonProc);
		return 0;

	case WM_DESTROY:
		SetWindowLongPtr(hwndButton, GWLP_WNDPROC, (LONG_PTR)g_lpfnDefButtonProc);
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

LRESULT CALLBACK NewButtonProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg) {

	case WM_RBUTTONDOWN:
		MessageBox(NULL, TEXT("右ボタンが押されました。"), TEXT("OK"), MB_OK);
		return 0;

	default:
		break;

	}

	return CallWindowProc(g_lpfnDefButtonProc, hwnd, uMsg, wParam, lParam);
}

WM_CREATEではボタンを作成するとともに、ボタンのサブクラス化も行います。 まず、GetWindowLongPtrで元のウインドウプロシージャのアドレスを取得し、 その後にSetWindowLongPtrで新しいウインドウプロシージャを設定します。 この関数はWM_DESTROYでも呼ばれていますが、 これはボタンが破棄される前に元のウインドウプロシージャへ戻すためです。 ただし、元に戻さなくても特に問題はありません。

NewButtonProcでは、マウスの右ボタンの押下を検出するためにWM_RBUTTONDOWNを処理しています。 また、それ以外のメッセージは、CallWindowProcにボタンの元のウインドウプロシージャを指定することによって、 デフォルトの処理を行えるようにしています。 なお、NewButtonProcでは、WM_LBUTTONDOWNを処理することもできますが、これには意味がないことに注意してください。 なぜなら、ボタンが選択された場合は親ウインドウにWM_COMMANDが通知されるため、 サブクラス化をしなくても左ボタンの押下は検出することができるからです。

スーパークラス化について

今回は、ウインドウプロシージャの書き換えをサブクラス化によって実現しましたが、 これはスーパークラス化によっても実現することができます。 スーパークラス化とは、既存のウインドウクラスを基に新しいウインドウクラスを作ることですが、 この新しいウインドウクラスのウインドウプロシージャを独自に指定することで、 新しいウインドウクラスから作成されたウインドウのメッセージは、 独自のウインドウプロシージャに送られるようになります。 次に例を示します。

#include <windows.h>

#define ID_BUTTON 100

LRESULT CALLBACK NewButtonProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

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

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg) {
		
	case WM_CREATE: {
		TCHAR      szNewClassName[] = TEXT("BUTTONEX");
		WNDPROC    lpfnDefButtonProc;
		WNDCLASSEX wc;

		wc.cbSize = sizeof(WNDCLASSEX);
		GetClassInfoEx(((LPCREATESTRUCT)lParam)->hInstance, TEXT("BUTTON"), &wc);
		
		lpfnDefButtonProc = wc.lpfnWndProc;

		wc.lpfnWndProc   = NewButtonProc;
		wc.lpszClassName = szNewClassName;
		if (RegisterClassEx(&wc) == 0)
			return -1;

		CreateWindowEx(0, szNewClassName, TEXT("ボタン"), WS_CHILD | WS_VISIBLE, 30, 30, 80, 40, hwnd, (HMENU)ID_BUTTON, ((LPCREATESTRUCT)lParam)->hInstance, lpfnDefButtonProc);

		return 0;
	}

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

LRESULT CALLBACK NewButtonProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static WNDPROC lpfnDefButtonProc = NULL;

	switch (uMsg) {

	case WM_NCCREATE:
		lpfnDefButtonProc = (WNDPROC)((LPCREATESTRUCT)lParam)->lpCreateParams;
		break;

	case WM_RBUTTONDOWN:
		MessageBox(NULL, TEXT("右ボタンが押されました。"), TEXT("OK"), MB_OK);
		return 0;

	default:
		break;

	}

	return CallWindowProc(lpfnDefButtonProc, hwnd, uMsg, wParam, lParam);
}

まず、既存のウインドウクラスの情報を取得するためにGetClassInfoExを呼び出して、WNDCLASSEX構造体を初期化します。 第1引数にはインスタンスハンドルを指定し、第2引数には既存のウインドウクラスの名前を指定します。 次に、取得した既存のウインドウプロシージャのアドレスを変数に保存しておきます。 これは、独自のウインドウプロシージャでCallWindowProcを呼び出す際に必要になります。 そして、既存のウインドウクラスを基に新しいウインドウクラスを作成することになります。

wc.lpfnWndProc   = NewButtonProc;
wc.lpszClassName = szNewClassName;
if (RegisterClassEx(&wc) == 0)
	return -1;

CreateWindowEx(0, szNewClassName, TEXT("ボタン"), WS_CHILD | WS_VISIBLE, 30, 30, 80, 40, hwnd, (HMENU)1, ((LPCREATESTRUCT)lParam)->hInstance, lpfnDefButtonProc);

lpfnWndProcに独自のウインドウプロシージャのアドレスを指定し、 lpszClassNameに独自のウインドウクラスの名前を指定します。 szNewClassNameはBUTTONEXという文字列で初期化されていたため、 RegisterClassExが成功した場合はBUTTONEXというウインドウクラスが登録されたことを意味します。 この名前をCreateWindowExの第2引数に指定した場合は、BUTTONEXというウインドウクラスを基にウインドウが作成されますから、 ウインドウへのメッセージはNewButtonProcで処理できるようになります。

先のCreateWindowExの最終引数には、元のウインドウプロシージャのアドレスを指定しています。 ここに指定した値は、WM_NCCREATE及びWM_CREATEに渡されるCREATESTRUCT構造体のlParamに格納されるため、 これを通じてNewButtonProcに元のウインドウプロシージャのアドレスを渡すようにしています。 実際にどちらのメッセージでアドレスを受け取るかですが、 これは必ずWM_NCCREATEでなければなりません。 なぜなら、このメッセージはウインドウが作成されたときに送られる最初のメッセージであるからであり、 WM_CREATEでアドレスを受け取ってしまうと、それ以前のメッセージ(WM_NCCREATEなど)が送られた場合にCallWindowProcの第1引数がNULLになってしまうからです。

スーパークラス化はサブクラス化と比べて手順が複雑ですが、 ウインドウプロシージャを書き換えるコントロールが多い場合は、 スーパークラス化を使用したほうがよいと思われます。 サブクラス化の場合は、ウインドウの数だけウインドウプロシージャの書き換えを行う必要がありますが、 スーパークラス化の場合は、一度ウインドウクラスを登録すれば、 後はCreateWindowExにウインドウクラスの名前を指定するだけで済みますから、 コントロールの数によって処理が増えることはなくなります。



戻る