今回はフックの実践編ということで、ローカルフックを使ってみましょう。実際にシステムフックを使いたい場合でも、デバッグ等の問題からローカルフックでチェックしてから使用した方が楽なので、まずはこちらをマスターしておいてください。
|
フッククラスの作製
今回、「フックをコントロールするクラス」を作製します。実際にはクラスを作る必要はないのですが、クラスとして作製すると、システムフックとして使う時にDLLに移行するのが簡単にできてしまうので、フック用のクラスを作製してしまいましょう。 また、今回使用するフックはキーボードフックにします。標準的なフックで、使い道も多いでしょう。今回は「キーをインクリメントする」フックを作製します。Aを入力したらBが出てくる、みたいな感じです。
ではさっそく作製しましょう。今回作製するプロジェクトはダイアログにします。で、そのダイアログにエディットボックスをひとつ貼り付けてください。IDなんかはほったらかしで構いません。ただあればいいんです。 |
フックを使用する手順
これからプログラムを組んでいくわけですが、その前に「フックの掛け方」を見ておきましょう。
まず、SetWindowsHookEx()を使用してフックをセットします。このとき、フックのタイプと、横取りしたメッセージを受け取るフックプロシージャへのポインタを指定します。この関数はフックハンドルを返すのでこれを取っておきます。
この3つをクラスにインプリメントすれば、クラスの完成ということになります。では、順を追って説明していきましょう。 |
フックのセット
ではセットするメンバ関数を作製しましょう。クラスビューのCKeyboardHookで右クリック、「メンバ関数の追加」ダイアログを開き、次の関数を作製してください。 |
|
このメンバ関数は外から呼び出すもの(いわゆる「メソッド」)なので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で右クリック、「メンバ変数の追加」ダイアログを開き、次の変数を作製してください。 |
|
フックハンドルへはメソッドを介してのみ操作できるようにするため、Privateにしておきます。無理矢理フックハンドルをNULLになんてされたらたまったものじゃないですから。
で、実はこれだけではありません。CKeyboardHookクラスの宣言部を、次のように書き換えてください。 |
static HHOOK m_hHook; //static を書き加えてください。
さらに、KeyboardHook.cppの最初の方(コンストラクタの手前くらい)に、次のコードを書き込んでください。
|
HHOOK CKeyboardHook::m_hHook = NULL;
このフックハンドルを格納する変数はスタティック変数として宣言します。次に作製するフックプロシージャをスタティック関数として作製しなければならないため、このような形で宣言します。この実装方法だと、同じクラスで複数のフックをセットできないので注意してください。
さて、以上でセットする部分は終わり。次はフックプロシージャを作製しましょう。 |
フックプロシージャ
フックプロシージャは、横取りしたメッセージを受け取るための関数で、ウィンドウプロシージャのような書式にする必要があります。また、フックプロシージャはフックのタイプによって引数の解釈が違ってきます。その辺は各フックプロシージャのリファレンスを読んでください。
今回はキーボードプロシージャを作製します。キーボードプロシージャの場合、WM_KEYDOWNかWM_KEYUPがメッセージキューの先頭にあるときにGetMessage()かPeekMessage()が呼び出される直前にウィンドウズから呼び出されます(ただ、「実際にキーを押したとき」に限るらしく、PostMessage()で送ったものはフックプロシージャが呼ばれません)。
この辺の状態は、フックプロシージャによって完全に違います。上の状況はSDKベースでメッセージループを作ってチェックしたものです。なかなかテストは大変だと思いますのでご注意を。
では、例によって「メンバ関数の追加」ダイアログを開き、次の関数を作製してください。 |
|
ひとつずつ順に追っていきましょう。
まず戻り値の型ですが、LRESULTはウィンドウプロシージャやフックプロシージャが返す戻り値の型で、単なる32ビット整数(DWORD)です。CALLBACKもプロシージャ系には必要なもので、ウィンドウズから呼び出される関数に付けます。といってもこれも単に__stdcallのことです(「DLLを作ろう!(関数編)」の装飾名を参考にしてください)。 関数名は別に決まってませんが、キーボードフックプロシージャのリファレンスを簡単に見るために、同名にしておきました。あと引数の数と型は一致していなければなりません。でもフックプロシージャはすべて同じです。 この関数はCKeyboardHook::Set()からしか呼び出されないのでPrivateにしておきます(ウィンドウズから呼び出される関数がPrivateっていうのも変な話ですが)。
重要なのは、この関数もスタティックだということです。通常、クラスのメンバ関数は宣言された変数ごとに違ってくるため、メンバ関数のアドレスは特定されておらず、当然SetWindowsHookEx()へと渡すこともできません。
さて、ではフックプロシージャにコードを書き込んでみましょう。 |
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()を読んでフックプロシージャからすぐ抜けてしまいます。つまりこれは何もしないということです。
この値が負の場合、送られてきたメッセージは「このフックプロシージャでは処理してはいけません」という意味です。そのため、素通りさせます。 |
|
さて、このふたつの場合にはCallNextHookEx()を呼び出します。これは次のフックを呼び出す関数で、このフックをすぐ呼び出すということは、すなわち「何もしない」ということです。
複数のフックがセットされた場合、フックプロシージャはそれぞれが個別に呼ばれるわけではなく、フックプロシージャ上を橋渡ししていく形で呼ばれることになっています。この仕組みはよくSCSIのデイジーチェーンに似ていると言われます。 そしてSCSIと同じように、どこかが途切れてしまうと、そのあとのフックプロシージャは呼ばれなくなってしまいます。ローカルフックの場合には自分のアプリが動かなくなるだけで済みますが、システムフックの場合には他のフックを殺してしまうという問題が発生します。そのためにも、このCallNextHookEx()の呼び出しは必ずする必要があります。 また、このとき第1引数にフックハンドルを渡すということが重要です。フックプロシージャがreturnで終了したときに、ひとつ前のフックプロシージャのCallNextHookEx()へと戻る必要があります。ここでフックハンドルを渡していないと戻れなくなってしまいます。これも他のフックを殺す原因になってしまうので注意してください。 (「うちかぶ1.0」および「たすかぶ2.0」のバグはこれが原因でした(汗)。このページを参考にしてた方、ご注意を)
このあと、p_lParamの値もチェックします。KeyboardProc()での値の意味は「キー入力がどういう状況か」というものです。この値の32ビット目が1(つまり4バイト目が0x80)ならキーを離したときという意味です。このフラグによって、あとで送信するメッセージを変更します。
「このプログラムでの処理」を終えたら、こちらのメッセージも次のフックプロシージャに送っておきます。
以上をまとめましょう。 |
フックを外す
最後に後始末をしましょう。例によって「メンバ関数の追加」ダイアログで次の関数を作製してください。 |
|
ほとんど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派生クラスを右クリックして「メンバ変数の作製」を選び、次の変数を作製してください。 |
|
そのあと、ダイアログクラスの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. |