ローカルフック

 今回はフックの実践編ということで、ローカルフックを使ってみましょう。実際にシステムフックを使いたい場合でも、デバッグ等の問題からローカルフックでチェックしてから使用した方が楽なので、まずはこちらをマスターしておいてください。

フッククラスの作製
 今回、「フックをコントロールするクラス」を作製します。実際にはクラスを作る必要はないのですが、クラスとして作製すると、システムフックとして使う時にDLLに移行するのが簡単にできてしまうので、フック用のクラスを作製してしまいましょう。
 また、今回使用するフックはキーボードフックにします。標準的なフックで、使い道も多いでしょう。今回は「キーをインクリメントする」フックを作製します。Aを入力したらBが出てくる、みたいな感じです。

 ではさっそく作製しましょう。今回作製するプロジェクトはダイアログにします。で、そのダイアログにエディットボックスをひとつ貼り付けてください。IDなんかはほったらかしで構いません。ただあればいいんです。
 そのあと、「ワークスペース」の「クラスビュー」を表示し、「* クラス」(*はプロジェクト名)で右クリックし、出てきたメニューの「クラスの新規作製」を選んでください。
 表示されたダイアログの一番上、「クラスの種類」で「Generic クラス」を選んでください。こちらに選択しないと、MFCからの継承クラスを作ることになってしまいます。
 そのあと「クラス名」CKeyboardHookと入力して、「OK」ボタンを押してください。これで新しくクラスが作製されるはずです。

フックを使用する手順
 これからプログラムを組んでいくわけですが、その前に「フックの掛け方」を見ておきましょう。

 まず、SetWindowsHookEx()を使用してフックをセットします。このとき、フックのタイプと、横取りしたメッセージを受け取るフックプロシージャへのポインタを指定します。この関数はフックハンドルを返すのでこれを取っておきます。
 実は、フックのセットそのものはこれだけです。簡単ですね。
 そのあと、フックプロシージャにメッセージが送られてきますから、フックプロシージャでしたい処理を行えばOKです。
 最後に、UnhookWindowsHookEx()を使用してフックを外します。このとき、取っておいたフックハンドルを使用します。この処理は、アプリケーションを終了する前に必ず行ってください。

 この3つをクラスにインプリメントすれば、クラスの完成ということになります。では、順を追って説明していきましょう。

フックのセット
 ではセットするメンバ関数を作製しましょう。クラスビューのCKeyboardHookで右クリック、「メンバ関数の追加」ダイアログを開き、次の関数を作製してください。

関数の型 関数の宣言 アクセス制御
BOOL Set() Public

 このメンバ関数は外から呼び出すもの(いわゆる「メソッド」)なのでPublicにします。引数がないのは心配でしょうが、大丈夫です。
 さて、作製した関数に次のようなコードを書き込んでください。


BOOL CKeyboardHook::Set()
{
	m_hHook = ::SetWindowsHookEx( WH_KEYBOARD
			, (HOOKPROC)CKeyboardHook::KeyboardProc
			, NULL, 0 );

	if( !m_hHook )
		return FALSE;

	return TRUE;
}
	

 SetWindowsHookEx()はフックをセットするAPIです。たったこれだけでフックをセットできます。
 第1引数にはフックのタイプを指定します。今回はキーボードフックを使用するのでそのフラグを使っています。他のフラグについては前回とこのAPIのリファレンスを読んでください。
 第2引数にはフックプロシージャへのポインタを指定します。これはあとで作るものです。
 第3引数には、ローカルフックではNULLを渡します。システムフックを使う時に改めて説明します。
 第4引数にはセットするスレッドのIDを指定しますが、マルチスレッドを使用していないのならカレントスレッドだけなので、0を指定しちゃって構いません。
(注: Win NT / 2K でローカルフックをセットする場合には、第4引数にカレントのスレッドIDを指定する必要があるみたいです。 API の場合には GetWindowThreadProcessId() の戻り値を渡せばOK。 MFC の場合には AfxGetApp()->m_nThreadID を渡せばOK。 SetWindowsHookEx() が失敗する、という方はお試しあれ。)

フックハンドルの保存
 で、このAPIの戻り値はフックハンドルと呼ばれるものです。まぁお察しの通り、フックを操作するためのハンドルです。これがNULLのときには失敗なので、FALSEを返すようにしています。
 このm_hHookをまだ作っていなかったので、今作りましょう。メンバ関数を作ったときと同じようにクラスビューのCKeyboardHookで右クリック、「メンバ変数の追加」ダイアログを開き、次の変数を作製してください。

変数の型 変数の宣言 アクセス制御
HHOOK m_hHook Private

 フックハンドルへはメソッドを介してのみ操作できるようにするため、Privateにしておきます。無理矢理フックハンドルをNULLになんてされたらたまったものじゃないですから。
 で、実はこれだけではありません。CKeyboardHookクラスの宣言部を、次のように書き換えてください。


	static HHOOK m_hHook;	//static を書き加えてください。
	

 さらに、KeyboardHook.cppの最初の方(コンストラクタの手前くらい)に、次のコードを書き込んでください。


HHOOK	CKeyboardHook::m_hHook = NULL;
	

 このフックハンドルを格納する変数はスタティック変数として宣言します。次に作製するフックプロシージャをスタティック関数として作製しなければならないため、このような形で宣言します。この実装方法だと、同じクラスで複数のフックをセットできないので注意してください。
 さて、以上でセットする部分は終わり。次はフックプロシージャを作製しましょう。

フックプロシージャ
 フックプロシージャは、横取りしたメッセージを受け取るための関数で、ウィンドウプロシージャのような書式にする必要があります。また、フックプロシージャはフックのタイプによって引数の解釈が違ってきます。その辺は各フックプロシージャのリファレンスを読んでください。

キーボードプロシージャ  今回はキーボードプロシージャを作製します。キーボードプロシージャの場合、WM_KEYDOWNWM_KEYUPがメッセージキューの先頭にあるときにGetMessage()PeekMessage()が呼び出される直前にウィンドウズから呼び出されます(ただ、「実際にキーを押したとき」に限るらしく、PostMessage()で送ったものはフックプロシージャが呼ばれません)。
 フックプロシージャには「どんなキーを押したか」などの情報が送られてくるため、その情報を元に適当な処理をします。また、キューからそのメッセージを削除するか、それとも残しておくかを戻り値によって変えることができます。
 フックプロシージャから抜けると、GetMessage()等は思い出したようにキューからメッセージを取り出して、そのままプログラムが進んでいきます。

 この辺の状態は、フックプロシージャによって完全に違います。上の状況はSDKベースでメッセージループを作ってチェックしたものです。なかなかテストは大変だと思いますのでご注意を。

 では、例によって「メンバ関数の追加」ダイアログを開き、次の関数を作製してください。

関数の型 関数の宣言 アクセス制御 その他
LRESULT CALLBACK KeyboardProc( int p_nCode, WPARAM p_wParam, LPARAM p_lParam ) Private Static

 ひとつずつ順に追っていきましょう。
 まず戻り値の型ですが、LRESULTはウィンドウプロシージャやフックプロシージャが返す戻り値の型で、単なる32ビット整数(DWORD)です。CALLBACKもプロシージャ系には必要なもので、ウィンドウズから呼び出される関数に付けます。といってもこれも単に__stdcallのことです(「DLLを作ろう!(関数編)」の装飾名を参考にしてください)。
 関数名は別に決まってませんが、キーボードフックプロシージャのリファレンスを簡単に見るために、同名にしておきました。あと引数の数と型は一致していなければなりません。でもフックプロシージャはすべて同じです。
 この関数はCKeyboardHook::Set()からしか呼び出されないのでPrivateにしておきます(ウィンドウズから呼び出される関数がPrivateっていうのも変な話ですが)。

 重要なのは、この関数もスタティックだということです。通常、クラスのメンバ関数は宣言された変数ごとに違ってくるため、メンバ関数のアドレスは特定されておらず、当然SetWindowsHookEx()へと渡すこともできません。
 ですが、スタティック関数の場合にはそのクラスの変数すべてが同じコードを使用するので、アドレスが決まっています。というわけで、スタティック変数ならSetWindowsHookEx()というわけです。
 この関係でフックハンドルもスタティック変数になったというわけですが、この辺はシステムフックにしたときにいい方に転がるので、気にしないでください。
 ちなみにこの辺がイヤな方は、フックハンドルはグローバル変数に、フックプロシージャは普通のC言語タイプの関数にしてください。

 さて、ではフックプロシージャにコードを書き込んでみましょう。


LRESULT CALLBACK CKeyboardHook::KeyboardProc(int p_nCode, WPARAM p_wParam, LPARAM p_lParam)
{
	if( p_nCode < 0 || p_nCode == HC_NOREMOVE )
		return ::CallNextHookEx( m_hHook, p_nCode, p_wParam, p_lParam );

	UINT	uiMsg;

	if( p_lParam & 0x80000000 )
		uiMsg = WM_KEYUP;
	else
		uiMsg = WM_KEYDOWN;

	++p_wParam;
	::PostMessage( ::GetFocus(), uiMsg, p_wParam, p_lParam );
	::CallNextHookEx( m_hHook, p_nCode, p_wParam, p_lParam );

	return TRUE;
}
	

 このフックプロシージャの処理はとても重要です。順を追って見ていきましょう。
 まずp_nCodeの値をチェックします。この場合は、CallNextHookEx()を読んでフックプロシージャからすぐ抜けてしまいます。つまりこれは何もしないということです。

 この値が負の場合、送られてきたメッセージは「このフックプロシージャでは処理してはいけません」という意味です。そのため、素通りさせます。
 また、HC_NOREMOVEは、このフックプロシージャがPeekMessage()から呼ばれ、かつPM_NOREMOVEのフラグが立てられていることを意味します。
 キーボードフックは、GetMessage()PeekMessage()が呼ばれ、メッセージをキューから取り込んだときにそのメッセージを横取りするフックです。
 PeekMessage()でメッセージを取り出したとき、この第5引数にPM_NOREMOVEを渡すと、キューからメッセージを取り除かず、単に今どのメッセージがあるのかチェックするだけになります。
 これは「メッセージがないとき」の処理をするためのシステムで、普通にメッセージがある場合なら必ずそのあとでGetMessage()等が呼ばれ、ちゃんとメッセージが処理されるはずです。というわけで、HC_NOREMOVEの立ったメッセージを処理してしまうと、同じメッセージを何度も処理してしまう可能性が出てくるので、この場合にはメッセージを素通りさせるというわけです。まとめると……

判定 意味
p_nCode < 0 次のフックプロシージャ
に渡す。
p_nCode == HC_NOREMOVE PeekMessage()がPM_NOREMOVE
付きで呼ばれたとき

フックチェーン  さて、このふたつの場合にはCallNextHookEx()を呼び出します。これは次のフックを呼び出す関数で、このフックをすぐ呼び出すということは、すなわち「何もしない」ということです。
 複数のフックがセットされた場合、フックプロシージャはそれぞれが個別に呼ばれるわけではなく、フックプロシージャ上を橋渡ししていく形で呼ばれることになっています。この仕組みはよくSCSIのデイジーチェーンに似ていると言われます。
 そしてSCSIと同じように、どこかが途切れてしまうと、そのあとのフックプロシージャは呼ばれなくなってしまいます。ローカルフックの場合には自分のアプリが動かなくなるだけで済みますが、システムフックの場合には他のフックを殺してしまうという問題が発生します。そのためにも、このCallNextHookEx()の呼び出しは必ずする必要があります
 また、このとき第1引数にフックハンドルを渡すということが重要です。フックプロシージャがreturnで終了したときに、ひとつ前のフックプロシージャのCallNextHookEx()へと戻る必要があります。ここでフックハンドルを渡していないと戻れなくなってしまいます。これも他のフックを殺す原因になってしまうので注意してください。
(「うちかぶ1.0」および「たすかぶ2.0」のバグはこれが原因でした(汗)。このページを参考にしてた方、ご注意を)

 このあと、p_lParamの値もチェックします。KeyboardProc()での値の意味は「キー入力がどういう状況か」というものです。この値の32ビット目が1(つまり4バイト目が0x80)ならキーを離したときという意味です。このフラグによって、あとで送信するメッセージを変更します。
 今回は「ひとつキーを進める」ということだったので、キーが入っているp_wParamをインクリメントします。
 そのあとPostMessage()でキーを送ります。一見「永久ループになるんとちゃう?」と思われますが、どうやら「実際にキー入力した時のメッセージ」でないとフックプロシージャは呼ばれないので大丈夫です。それに、メッセージキューが空の場合には、フックプロシージャが呼ばれたあとにGetMessage()等が呼ばれますので、これは当然メッセージプロシージャに送られません。

 「このプログラムでの処理」を終えたら、こちらのメッセージも次のフックプロシージャに送っておきます。
 最後に戻り値です。キーボードプロシージャの場合、この値をゼロにすると、このとき取得したメッセージをメッセージキューから削除します。今回は先ほど代わりのメッセージを送ってあるので、ゼロにして値を返さないようにします。
 削除しない場合には、基本的にCallNextHookEx()の戻り値を返します。このAPIの戻り値は呼び出したフックプロシージャの戻り値なので、この値をさらに返していくことで、戻り値もフックプロシージャを橋渡しさせていくことになります。

 以上をまとめましょう。
 重要なのは必ずCallNextHookEx()を呼ぶことです。このときフックハンドルを渡すことを忘れずに。
 それ以外は基本的に好きにしていいでしょう。今回はくだらないことをしていますが、アイディア次第で色々なことができるでしょう。
 ただ、実際に「したいこと」があったとしても、そのためにどういうプログラムを組めばいいのかというのが結構難しいと思います。デバッガを駆使してがんばってください。

フックを外す
 最後に後始末をしましょう。例によって「メンバ関数の追加」ダイアログで次の関数を作製してください。

関数の型 関数の宣言 アクセス制御
BOOL Release() Public

 ほとんどCKeyboardHook::Set()と同じなので問題ないでしょう。では、作製した関数に次のコードを書き加えてください。


BOOL CKeyboardHook::Release()
{
	BOOL bRes;

	if( m_hHook )
		bRes = ::UnhookWindowsHookEx( m_hHook );

	m_hHook = NULL;
	return bRes;
}
	

 さらに、デストラクタに次のコードを書き込んでください。


CKeyboardHook::~CKeyboardHook()
{
	Release();
}
	

 UnhookWindowsHookEx()を使用して、フックを外します。クラスを便利にするために、デストラクタで自動的に呼ばれるようにしました。この辺は難しい部分はないでしょう。

フッククラスの使用方法
 では実際に使ってみましょう。今回はダイアログベースのアプリケーションを作製しているので、それを踏まえて書き換えていってください。
 まずもともとあるCWinApp派生クラスCDialog派生クラスソースファイル(拡張子がcppのファイル)の最初の方に、次の1行を追加してください。


#include "stdafx.h"		//この次の行にね。
#include "CKeyboardHook.h"	//この行を追加。
	

 ダイアログクラスのヘッダーファイルを読み込んでいるソースファイルはすべて書き加えてください。
 次にそのダイアログクラスのメンバ変数として、先ほど作製したCKeyboardHookクラスの変数を作製します。例によってCDialog派生クラスを右クリックして「メンバ変数の作製」を選び、次の変数を作製してください。

変数の型 変数の宣言 アクセス制御
CKeyboardHook m_cKeyHook Private

 そのあと、ダイアログクラスのOnInitDialog()が最初からあると思うので、その後半部分に次のコードを書き込んでください。


BOOL CMyDlg::OnInitDialog()
{
	CDialog::OnInitDialog();

	// このダイアログ用のアイコンを設定します。フレームワークはアプリケーションのメイン
	// ウィンドウがダイアログでない時は自動的に設定しません。
	SetIcon(m_hIcon, TRUE);			// 大きいアイコンを設定
	SetIcon(m_hIcon, FALSE);		// 小さいアイコンを設定
	
	//////////////////////////////////
	// フックのセット。
	m_cKeyHook.Set();	//この行を追加してください。

	return TRUE;  // TRUE を返すとコントロールに設定したフォーカスは失われません。
}
	

 はい、お疲れさま。コーディングはここまでです。ビルドしたあと実行して、エディットボックスで文字を入力してみてください。ちゃんとフックが効いているのが分かると思います。

まとめ
 で、実際に使ってみると思いの外難しいということが分かると思います。
 フックを使うだけならなんの問題もないんですが、それをどう処理するか、ということがやっかいになってきます。例えば今回の場合には、デリートキー等が効かなくなってしまうので、その辺の処理をしなければなりません。こういった細かな処理を組み込んでやらないと、うまく働かなくなってしまいます。
 また、今回以外の処理や、他のフックを使用した場合には、その都度、色々とテストしなければならないでしょう。どんな時にどういうメッセージが送られてくるのか、どのメッセージを削除して、どんなメッセージをどのウィンドウへと送ればいいのか……そういったテストを入念に繰り返す必要が出てくると思います。
 それでも楽な方です。自分の作ったアプリケーションに使用するんですから。

 というわけで次回は、茨の道を突き進むあなたに送る「システムフック」です。

(C)KAB-studio 1998 ALL RIGHTS RESERVED.