EternalWindows
メッセージ管理 / メッセージフック

メッセージフックとは、ウインドウプロシージャに送られるメッセージを、 アプリケーションがウインドウプロシージャよりも先にフック(横取りや検出)する方法です。 メッセージをフックすると、アプリケーションが用意したフックプロシージャという関数が呼ばれ、 そこでメッセージの情報を変更したり、フィルタしたりすることができます。 フィルタするというのは、メッセージをウインドウプロシージャに送られないようにすることであり、 ウインドウの既定の動作を変更したい場合に利用できます。

メッセージフックは非常に強力な機能です。 たとえば、GetMessageは呼び出し側スレッドのメッセージキューからメッセージを取得するため、 他のスレッド宛てのメッセージを考慮できませんでしたが、 メッセージフックならばこのようなことは可能です。 なぜなら、フック対象とするスレッドのIDを指定できるからです。 メッセージをフックするには、SetWindowsHookExを呼び出します。

HHOOK SetWindowsHookEx(
  int idHook,
  HOOKPROC lpfn,
  HINSTANCE hMod,
  DWORD dwThreadId
);

idHookは、インストールしたいフックプロシージャのタイプを指定します。 lpfnは、フックプロシージャのアドレスを指定します。 hModは、フックプロシージャを実装するDLLのハンドルを指定します。 フックプロシージャがプログラム内で実装されている場合は、NULLを指定します。 dwThreadIdは、フックプロシージャを関連付けるスレッドのIDを指定します。 0を指定した場合は、システムに存在する全てのスレッドにフックプロシージャが関連付けられます。 戻り値は、フックプロシージャのハンドルが返ります。

メッセージフックが不要になったアプリケーションは、 インストールしたフックプロシージャを削除するためにUnhookWindowsHookExを呼び出します。

BOOL UnhookWindowsHookEx(
  HHOOK hhk
);

hhkは、フックプロシージャのハンドルを指定します。 関数が成功した場合は、これ以上フックプロシージャが呼ばれることはなくなります。

フックプロシージャは、特定のフックタイプに関連するフックチェーンの中にインストールされます。 フックチェーンとはフックプロシージャのリストであり、 現在のフックプロシージャは次のフックプロシージャをCallNextHookExで呼び出す義務が生じます。 そうしなければ、処理されないフックプロシージャが出てしまい、 そのフックプロシージャの動作が失われてしまうからです。 CallNextHookExは、次のように定義されています。

LRESULT CallNextHookEx(
  HHOOK hhk,
  int nCode,
  WPARAM wParam,
  LPARAM lParam
);

hhkは、フックハンドルを指定します。 Windows XP以降では、この引数は無視されます。 nCodeは、フックプロシージャに渡されたフックコードを指定します。 wParamは、フックプロシージャに渡されたWPARAMを指定します。 lParamは、フックプロシージャに渡されたLPARAMを指定します。 戻り値は、次のフックプロシージャの戻り値が返ります。 通常、フックプロシージャはこの値を返します。

今回のプログラムは、エディットコントロールにアルファベットのみを入力可能とします。 この動作を実現するために、フックがどのように機能しているかを考えていきます。

#include <windows.h>

LRESULT CALLBACK KeyboardProc(int code, 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 HHOOK hhk = NULL;
	
	switch (uMsg) {
		
	case WM_CREATE:
		CreateWindowEx(0, TEXT("EDIT"), TEXT(""), WS_CHILD | WS_VISIBLE | WS_BORDER, 30, 30, 150, 30, hwnd, (HMENU)1, ((LPCREATESTRUCT)lParam)->hInstance, NULL);		
		hhk = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, NULL, GetCurrentThreadId());
		return 0;

	case WM_DESTROY:
		if (hhk != NULL)
			UnhookWindowsHookEx(hhk);
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

LRESULT CALLBACK KeyboardProc(int code, WPARAM wParam, LPARAM lParam)
{
	WORD wType;
	
	if (code < 0)
		return CallNextHookEx(NULL, code, wParam, lParam);

	if (wParam == VK_BACK || wParam == VK_SHIFT)
		return CallNextHookEx(NULL, code, wParam, lParam);
	
	GetStringTypeEx(LOCALE_SYSTEM_DEFAULT, CT_CTYPE3, (LPTSTR)&wParam, sizeof(TCHAR), &wType);
	if (!(wType & C3_HALFWIDTH) || !(wType & C3_ALPHA))
		return 1;

	return CallNextHookEx(NULL, code, wParam, lParam);
}

SetWindowsHookExの第1引数にWH_KEYBOARDを指定した場合は、 キーボードメッセージが生成された際に、第2引数のKeyboardProcが呼び出されます。 このフックプロシージャは、プログラム内で実装されているので第3引数はNULLを指定します。 第4引数は、フックプロシージャを関連付けるスレッドのIDであり、 今回は現在のスレッド(メインスレッド)のIDを指定しています。 これにより、現在のスレッド宛てに生成されたキーボードメッセージをフックできることになります。

今回のKeyboardProcで行うことは、アルファベット以外の文字の入力を防ぐことです。 wParamには押下されたキーの仮想キーコードが格納されているので、 これを確認することになります。

LRESULT CALLBACK KeyboardProc(int code, WPARAM wParam, LPARAM lParam)
{
	WORD wType;
	
	if (code < 0)
		return CallNextHookEx(NULL, code, wParam, lParam);

	if (wParam == VK_BACK || wParam == VK_SHIFT)
		return CallNextHookEx(NULL, code, wParam, lParam);
	
	GetStringTypeEx(LOCALE_SYSTEM_DEFAULT, CT_CTYPE3, (LPTSTR)&wParam, sizeof(TCHAR), &wType);
	if (!(wType & C3_HALFWIDTH) || !(wType & C3_ALPHA))
		return 1;

	return CallNextHookEx(NULL, code, wParam, lParam);
}

フックプロシージャにおける1つの決まりとして、codeが0より低い場合はCallNextHookExを呼ばなければならないとことになっています。 よって、そのようなコードを最初に記述しておきます。 次の処理では、押下されたキーがBackやShiftでないかを確認しています。 先ほどアルファベット以外の入力を許可しないと述べましたが、 これでは入力した文字を消すこともできなくなりますから、 最低限必要なキーの入力は許可しておく必要があります。 これはいわば、デフォルトの処理が行われるようにするということですから、 CallNextHookExで次のフックプロシージャを呼び出せばよいことになります。 GetStringTypeExが返した値に、C3_HALFWIDTH(半角)が含まれていない、 またはC3_ALPHA(アルファベット)が含まれていない場合は、 入力された文字がアルファベット以外の文字であることを意味するため、 CallNextHookExを呼ばずに0以外の値を返します。 0以外の値を返すと、次のフックプロシージャが呼ばれなくなり、 デフォルトの処理が行われなくなります。 アルファベットが入力されたことを分かった場合は、 CallNextHookExを呼び出すことで次のフックプロシージャを呼び出します

WH_KEYBOARDにおけるデフォルト処理というのは、ウインドウに対してWM_KEYDOWNなどのキーメッセージを送ることです。 たとえば、親ウインドウに対してフォーカスがある場合は、 親ウインドウにWM_KEYDOWNが送られますし、 エディットコントロールにフォーカスがある場合は、エディットコントロールにWM_KEYDOWNが送られます。 フックプロシージャでフィルタ処理(0以外の値を返す)をした場合は、 入力した文字を表すWM_KEYDOWNが送られないことになりますから、 エディットコントロールは文字の入力を検出することができず、 入力した文字は表示されないことになります。

メッセージフックは、ウインドウのサブクラス化と少し似ている部分がありますが、 実際には大きく異なるものです。 たとえば、エディットコントロールをサブクラス化すると、 エディットコントロールに対するWM_KEYDOWNを検出することができますが、 このメッセージをフィルタすることはできません。 なぜなら、WM_KEYDOWNは入力されたキーを知らせるための通知に過ぎないからです。 また、メッセージフックはウインドウ単位ではなくスレッド単位で機能します。 つまり、1つのスレッドによって作成された全てのウインドウへのメッセージをフックできます。 これにより、今回のような入力制限は、エディットコントロールに限らず他に作成したウインドウにも適応されます。


戻る