サブクラス

コントロールの拡張

Windowsが用意する各種コントロールは、作成するだけで最初からいろいろな機能が実装されています。
多くの場合はそのままで十分ですが、機能が物足りなかったり、既定の動作を変更したかったり、ということがあります。
コントロールの機能を拡張したいときはサブクラス化という方法を用います。

コントロールもウィンドウの一種であり、ウィンドウということはウィンドウプロシージャも存在しています。
コントロールの機能を決定しているのはこのウィンドウプロシージャです。
このウィンドウプロシージャの動作を変更できれば、コントロールの機能を変更できることになります。
コントロールのウィンドウプロシージャの中身を直接書き換えることはできないので、別のウィンドウプロシージャに差し替えるという形で動作を変更します。

SetWindowSubclass関数

ウィンドウプロシージャの差し替えはSetWindowSubclass関数を使用します。

BOOL SetWindowSubclass(
 HWND hWnd,
 SUBCLASSPROC pfnSubclass,
 UINT_PTR uIdSubclass,
 DWORD_PTR dwRefData
);
ウィンドウhWndのウィンドウプロシージャをpfnSubclassに置き換える。

第一引数hWndはサブクラス化したいコントロールのウィンドウプロシージャです。
第二引数pfnSubclassは新しいウィンドウプロシージャです。
これは自分でコールバック関数を自分で定義して指定します。
(後述)

第三引数uIdSubclassはサブクラスを識別するためのIDです。
これも自分で#defineなどで管理します。
第四引数dwRefDataは自作プロシージャに渡すことができる任意の整数値で、プログラマが自由に使うことができます。
DWORD_PTR型はポインタと同じサイズであることが保障されている型で、ポインタやウィンドウハンドルなどを渡すことができます。

Subclassprocコールバック関数

差し替えに使用するウィンドウプロシージャは以下の形式で定義します。

LRESULT CALLBACK Subclassproc(
 HWND hWnd,
 UINT uMsg,
 WPARAM wParam,
 LPARAM lParam,
 UINT_PTR uIdSubclass,
 DWORD_PTR dwRefData
){}
SetWindowSubclass関数、RemoveWindowSubclass関数が使用するコールバック関数の定義。

hWnduMsgwParamlParamは通常のウィンドウプロシージャと同じで、コントロールのウィンドウハンドル、メッセージ、ふたつの追加のパラメータが格納され送られてきます。
uIdSubclassdwRefDataSetWindowSubclass関数の引数に指定した値が送られてきます。

これらを使用したサンプルを以下に示します。

SetWindowSubclass関数などはcomctl32.libというライブラリに定義されていています。
これは標準ではリンクされていないので、ここではコードの先頭に#pragma comment(lib, "comctl32.lib")を記述してリンクします。
また、#include <commctrl.h>が必要です。


#pragma comment(lib, "comctl32.lib")

#include <windows.h>
#include <commctrl.h>

//関数プロトタイプ宣言
ATOM MyRegisterClass(HINSTANCE);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK MyEditProc(HWND, UINT, WPARAM, LPARAM, UINT_PTR, DWORD_PTR);

//ウィンドウの生成等は省略

#define IDC_EDIT1 100

//エディットボックスの新しいウィンドウプロシージャ
LRESULT CALLBACK MyEditProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
{
	static BOOL b;
	switch (uMsg)
	{
	case WM_LBUTTONUP: //左クリック時の動作を変更する
		b = !b;
		SendMessage(hWnd, EM_SETREADONLY, b, 0);
		break;
	}
	
	//コントロールの既定の動作をさせる
	return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}

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

	switch (message)
	{
	case WM_CREATE: //ウィンドウ作成
		hEdit = CreateWindowEx(
			WS_EX_CLIENTEDGE,
			L"EDIT", 0,
			WS_CHILD | WS_VISIBLE |
			ES_MULTILINE | WS_VSCROLL | ES_NOHIDESEL,
			10, 10, 250, 150,
			hWnd, (HMENU)IDC_EDIT1, hInst, NULL);

		//ウィンドウプロシージャの差し替え
		SetWindowSubclass(hEdit, MyEditProc, IDC_EDIT1, 0);
		break;

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

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

このコードは、エディットコントロールをクリックするたびに読み取り専用状態の設定/解除が行われます。
サブクラス化のサンプル

メインウィンドウのプロシージャでは、マウスに対する処理は何も行っていません。
マウス処理は、自前で用意したエディットコントロール用のウィンドウプロシージャ(MyEditProc関数)で行っています。
(差し替えたウィンドウプロシージャはここでは新ウィンドウプロシージャと呼ぶことにします)

新ウィンドウプロシージャでの処理は基本的にいつものウィンドウプロシージャと同じです。
サンプルコードではWM_LBUTTONUPメッセージを処理しています。
この処理はサブクラス化したコントロールでのみ有効になります。

DefSubclassProc関数

新ウィンドウプロシージャの処理の最後はDefSubclassProc関数を実行します。

LRESULT DefSubclassProc(
 HWND hWnd,
 UINT uMsg,
 WPARAM wParam,
 LPARAM lParam
);
SetWindowSubclass関数でサブクラス化している場合に、既定のウィンドウプロシージャを呼び出す。

この関数はサブクラス化前の既定のウィンドウプロシージャを実行します。
通常のウィンドウプロシージャで使用するDefWindowProc関数と同じようなもので、引数も同じです。
ただし、DefWindowProc関数は自前でメッセージを処理した場合には実行しませんが、サブクラス化した場合の新ウィンドウプロシージャでは基本的に最後にDefSubclassProc関数を実行するようにします。
これにより既定の動作を保ったまま、任意の動作をコントロールに追加することができます。

逆に言えば、DefSubclassProc関数を実行しない場合はコントロールは既定の動作をしません。
これを利用して特定のメッセージに対する動作を無効化したり丸々変更することもできます。

GetWindowSubclass関数

ウィンドウのサブクラス化の状態はGetWindowSubclass関数で取得できます。

BOOL GetWindowSubclass(
 HWND hWnd,
 SUBCLASSPROC pfnSubclass,
 UINT_PTR uIdSubclass,
 DWORD_PTR *pdwRefData
);
ウィンドウhWndが、コールバック関数pfnSubclass、サブクラスID uIdSubclassによりサブクラス化されているかをTRUE/FALSEで返す。

サブクラスは、コールバック関数のアドレスとサブクラスIDの組み合わせによって識別されており、GetWindowSubclass関数はその状態を返します。
引数pdwRefDataにはDWORD_PTR型のポインタを指定します。
ここにはサブクラス化時に引数に指定した任意の値が格納されます。

すでにサブクラス化されているコントロールでも、別のコールバック関数を指定することでさらにサブクラス化することもできます。
(最後に登録したコールバック関数から順に実行されます)
サブクラスIDを変えることで同じコールバック関数を複数回実行することもできます。

RemoveWindowSubclass関数

ウィンドウのサブクラス化を解除するにはRemoveWindowSubclass関数を使用します。

BOOL RemoveWindowSubclass(
 HWND hWnd,
 SUBCLASSPROC pfnSubclass,
 UINT_PTR uIdSubclass
);
ウィンドウhWndが、コールバック関数pfnSubclass、サブクラスID uIdSubclassによりサブクラス化されている場合に、サブクラス化を解除する。
成功した場合はTRUE、失敗した場合はFALSEを返す。

引数の意味は(第四引数を除いて)GetWindowSubclass関数と同じです。

Windows XPより前のバージョンでのサブクラス化

SetWindowSubclass関数によるサブクラス化は、Windows XP以降で使用可能な方法です。
それよりも前のバージョンではSetWindowLongPtr関数を使用します。

LONG_PTR SetWindowLongPtrW(
 HWND hWnd,
 int nIndex,
 LONG_PTR dwNewLong
);
ウィンドウhWndの属性nIndexをdwNewLongに置き換える。
成功した場合は設定する以前の値を返す。
失敗した場合は0を返す。

これはウィンドウに対する様々な設定を変更するための関数です。
この関数の第二引数にGWLP_WNDPROCという定数を指定することで、ウィンドウに関連づけられているウィンドウプロシージャのポインタを置き換えることができます。

変更はせずに現在の設定を取得するGetWindowLongPtr関数もあります。

LONG_PTR GetWindowLongPtrW(
 HWND hWnd,
 int nIndex
);
ウィンドウhWndの属性nIndexを取得する。

詳しくはSetWindowLongPtr関数GetWindowLongPtr関数の項で改めて説明します。

置き換え用の新しいウィンドウプロシージャでは、処理の最後に既定のウィンドウプロシージャを呼び出す必要があります。
(呼び出さない場合はメッセージが無効化されます)
既定のウィンドウプロシージャはCallWindowProc関数で呼び出すことができます。

LRESULT CallWindowProcW(
 WNDPROC lpPrevWndFunc,
 HWND hWnd,
 UINT Msg,
 WPARAM wParam,
 LPARAM lParam
);
ウィンドウプロシージャlpPrevWndFuncを実行する。

第一引数には実行するウィンドウプロシージャを指定します。
残りの引数は通常のウィンドウプロシージャと同じです。

これらを使用してサブクラス化する例です。


#include <windows.h>

//関数プロトタイプ宣言
ATOM MyRegisterClass(HINSTANCE);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK MyEditProc(HWND, UINT, WPARAM, LPARAM);

#define IDC_EDIT1 100

//既定のウィンドウプロシージャを格納する変数
WNDPROC g_DefEditProc;

//エディット用ウィンドウプロシージャ
LRESULT CALLBACK MyEditProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg)
	{
	case WM_KEYDOWN:
		//Ctrl + A
		if (wParam == 'A' && GetKeyState(VK_CONTROL) < 0) {
			//テキストを全選択
			SendMessage(hWnd, EM_SETSEL, 0, SendMessage(hWnd, EM_GETLIMITTEXT, 0, 0));
		}
		break;
	}
	return CallWindowProc(g_DefEditProc, hWnd, uMsg, wParam, lParam);
}

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

	switch (message)
	{
	case WM_CREATE: //ウィンドウ作成
		hEdit = CreateWindowEx(
			WS_EX_CLIENTEDGE,
			L"EDIT", 0,
			WS_CHILD | WS_VISIBLE |
			ES_MULTILINE | WS_VSCROLL | ES_NOHIDESEL,
			0, 0, 250, 100,
			hWnd, (HMENU)IDC_EDIT1, hInst, NULL);

		//既定のウィンドウプロシージャのポインタを取得
		g_DefEditProc = GetWindowLongPtr(hEdit, GWLP_WNDPROC);
		//新しいウィンドウプロシージャに差し替え
		SetWindowLongPtr(hEdit, GWLP_WNDPROC, MyEditProc);
		break;

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

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

このコードはエディットコントロールに「Ctrl + A」キーの押下でテキストを全選択する機能を追加します。

まずGetWindowLongPtr関数で、既定のウィンドウプロシージャのポインタを取得します。
これは新しいウィンドウプロシージャ内で、処理の最後にCallWindowProc関数で呼び出すために使用します。
関数間をまたぐのでグローバル変数に保存しておきます。
次にSetWindowLongPtr関数でウィンドウプロシージャを置き換えます。

新ウィンドウプロシージャでの処理は、引数の数が異なるのと、既定のプロシージャ呼び出しに使用する関数が異なる点以外はSetWindowSubclass関数の時と同じです。

サブクラス化の解除

サブクラス化の解除はSetWindowLongPtr関数で、保存しておいた既定のウィンドウプロシージャに戻します。

SetWindowSubclass関数によるサブクラス化は、同じコントロールに複数回サブクラス化をすると、最初にサブクラス化したウィンドウプロシージャのポインタは次のサブクラス化の処理の中に組み込まれます。
サブクラス化の解除は、最後にサブクラス化したものから順に行う必要があります。
特定のサブクラス化だけを一発で解除することはできないので、処理が煩雑になります。

SetWindowLongPtr関数と似たものにSetWindowLong関数があります。
基本的にどちらも同じ動作をしますが、SetWindowLong関数は64ビットアプリケーションには対応していません。
SetWindowLongPtr関数は、32ビット/64ビットのどちらにも対応しています。