第165章 ジャーナルレコードとプレイバック その2


今回は、ジャーナル・レコードおよびプレイバックのプログラムを 実際に作ってみます。

まずは、dll側から作ります。

// hook04x.h #define EXPORT extern "C" __declspec(dllexport) EXPORT BOOL SetMainHWND(HWND); EXPORT BOOL StartRecord(void); EXPORT BOOL StartPlay(void); EXPORT BOOL EndHook(void); EXPORT void MenuCheck(void); EXPORT BOOL IsEndOK(void); EXPORT LRESULT CALLBACK MyHookProc(int, WPARAM, LPARAM); EXPORT LRESULT CALLBACK MyPlayProc(int, WPARAM, LPARAM); #define WM_PLAY_END WM_USER #define WM_RECORD_MAX (WM_USER + 1) #define WM_RECORD_END (WM_USER + 2) #define WM_END_HOOK (WM_USER + 3)

プログラムの関係上、ユーザー定義のメッセージをいくつか 作っておきます。このヘッダー・ファイルは呼び出し側 のプログラムにもインクルードされるのでdll側から 呼び出し側に合図を送るには好都合です。

// hook04x.cpp hook04x.dll #include <windows.h> #include "hook04x.h" #define MAX_RECORD 2000 HINSTANCE hInst; HHOOK hHook; HWND hWnd; BOOL bHook = FALSE; //フック中 BOOL bRecord = FALSE; //記録済かどうか int n; EVENTMSG MyEvent[MAX_RECORD]; DWORD dwStart; DWORD dwAdjust;

グローバル変数などです。

int WINAPI DllMain(HINSTANCE hInstance, DWORD fdReason, PVOID pvReserved) { hInst = hInstance; return TRUE; }

単にこのdllのインスタンスハンドルをグローバル変数にコピーしているだけです。

EXPORT BOOL StartRecord() { memset(MyEvent, 0, sizeof(EVENTMSG));//構造体を0で初期化 hHook = SetWindowsHookEx(WH_JOURNALRECORD, (HOOKPROC)MyHookProc, hInst, 0); if (hHook == NULL) { MessageBox(hWnd, "フックに失敗しました", "Error", MB_OK); return FALSE; } dwStart = GetTickCount(); n = 0; bHook = TRUE; bRecord = TRUE; return TRUE; }

記録開始の関数です。記録を開始した時間をdwStartに書きこんでおきます。

DWORD GetTickCount(VOID)

システムが開始されてからのミリセコンドを返します。 戻り値がDWORD値なのでシステムが49.7日経つと0に戻ります。

EXPORT BOOL StartPlay() { hHook = SetWindowsHookEx(WH_JOURNALPLAYBACK, (HOOKPROC)MyPlayProc, hInst, 0); if (hHook == NULL) { MessageBox(hWnd, "フックに失敗しました", "Error", MB_OK); return FALSE; } dwAdjust = GetTickCount() - dwStart; n = 0; bHook = TRUE; return TRUE; }

再生を開始する関数です。記録開始から再生開始までの 時間差をdwAdjustに書きこんでおきます。

EXPORT BOOL EndHook() { if (UnhookWindowsHookEx(hHook) == 0) { MessageBox(hWnd, "フック解除に失敗しました", "Error", MB_OK); return FALSE; } else { MessageBox(hWnd, "フックの解除成功", "OK", MB_OK); bHook = FALSE; } return TRUE; }

フックを解除する関数です。記録と再生のフックはどちらもhHookにしてあるので この関数でどちらのフックも解除することができます。

EXPORT BOOL SetMainHWND(HWND hMyWnd) { if (hMyWnd == NULL) { MessageBox(NULL, "HWNDが正しくない", "Error", MB_OK); return FALSE; } else { MessageBox(NULL, "HWNDを受け取りました", "OK", MB_OK); } hWnd = hMyWnd; return TRUE; }

呼び出し側からウィンドウハンドルをもらって、グローバル変数に コピーしています。

EXPORT void MenuCheck() { HMENU hMenu, hSubMenu; hMenu = GetMenu(hWnd); hSubMenu = GetSubMenu(hMenu, 1); if (bHook == TRUE) { EnableMenuItem(hSubMenu, 0, MF_BYPOSITION | MF_GRAYED); EnableMenuItem(hSubMenu, 1, MF_BYPOSITION | MF_GRAYED); return; } if (bRecord == TRUE && bHook == FALSE) { EnableMenuItem(hSubMenu, 0, MF_BYPOSITION | MF_ENABLED); EnableMenuItem(hSubMenu, 1, MF_BYPOSITION | MF_ENABLED); } if (bRecord == FALSE && bHook == FALSE) { EnableMenuItem(hSubMenu, 0, MF_BYPOSITION | MF_ENABLED); EnableMenuItem(hSubMenu, 1, MF_BYPOSITION | MF_GRAYED); } return; }

呼び出し側のプログラムのメニュー項目を使用可能・不可にする関数です。 フック中は「記録開始」「再生」などの項目を使用不可にします。 記録済みでフックされていない時は、どちらも使用可能にします。 記録がされていない時は「再生」を使用不可にします。

EXPORT BOOL IsEndOK() { if (bHook) { return FALSE; } else { return TRUE; } }

呼び出し側のプログラムをやめてよいかどうかを調べる関数です。 フック中ならFALSEをそうでない時はTRUEを返します。

EXPORT LRESULT CALLBACK MyHookProc(int nCode, WPARAM wp, LPARAM lp) { EVENTMSG *lpEM; if (nCode == HC_ACTION) { lpEM = (EVENTMSG *)lp; if (lpEM->message == WM_KEYDOWN && LOBYTE(lpEM->paramL) == VK_CANCEL) { PostMessage(hWnd, WM_RECORD_END, 0, 0); PostMessage(hWnd, WM_END_HOOK, 0, 0); return TRUE; } MyEvent[n].hwnd = lpEM->hwnd; MyEvent[n].message = lpEM->message; MyEvent[n].paramH = lpEM->paramH; MyEvent[n].paramL = lpEM->paramL; MyEvent[n].time = lpEM->time; n++; if (n >= MAX_RECORD-1) { PostMessage(hWnd, WM_RECORD_MAX, 0, 0); PostMessage(hWnd, WM_END_HOOK, 0, 0); return TRUE; } return TRUE; } return CallNextHookEx(hHook, nCode, wp, lp); }

ジャーナルレコードのフックプロシージャです。nCodeがHC_ACTIONの時、 バッファなどにイベントを記録します。イベントはlpの指すEVENTMSG構造体 を調べるとわかります。(前章参照)

さて、記録を中止する合図(Ctrl + STOP)が来たかどうかは、EVENTMSG構造体の messageメンバを調べるとわかります。まずキーが押されるとmessageメンバに WM_KEYDOWNが入ります。どのキーが押されたかはparamLメンバのLOBYTEを 調べるとわかります。これがVK_CANCELなら記録を中止します。 ここではPostMessage関数で呼び出し側にWM_RECORD_ENDメッセージを 送ります。呼び出し側ではこのメッセージが来たら単にメッセージボックスを 出してユーザーに記録を止めることを知らせます。そのあと、再度呼び出し側に WM_END_HOOKメッセージを出します。呼び出し側ではこのメッセージが来たら dll側のEndHook関数を呼びます。これで、フックが解除されます。 どうしてこんなめんどうなことをするかというと、 UnhookWindowsHookEx関数が成功したかどうかを調べて そのことをメッセージボックスでユーザーに知らせたいからです。 UnhookWindowsHookEx関数を実行するとフックプロシージャは無効になります。 その無効なプロシージャの中で、関数(MessageBoxなど)を呼ぶのは 大変おかしなことです。また、WM_RECORD_END、WM_END_HOOKメッセージを 送る時にSendMessage関数ではなくPostMessage関数を使っています。 後者はキューにメッセージを入れると応答を待たずに復帰します。 前者は直ちに目的のプロシージャにメッセージを送り、そのメッセージが 処理されてから復帰します。実際にやってみるとわかりますが SendMessageを使うとおかしな結果になります。

BOOL PostMessage( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam );

使い方そのものは、SendMessage関数と同じです。

次にnが配列(MyEvent)の最後まで行ったら、その旨を ユーザーに知らせて、フックを解除します。

EXPORT LRESULT CALLBACK MyPlayProc(int nCode, WPARAM wp, LPARAM lp) { EVENTMSG *lpem; DWORD dwReturnTime; if (nCode == HC_GETNEXT) { lpem = (EVENTMSG *)lp; lpem->hwnd = MyEvent[n].hwnd; lpem->message = MyEvent[n].message; lpem->paramH = MyEvent[n].paramH; lpem->paramL = MyEvent[n].paramL; lpem->time = MyEvent[n].time + dwAdjust; dwReturnTime = lpem->time - GetTickCount(); if ((int)dwReturnTime < 0) { dwReturnTime = 0; lpem->time = GetTickCount(); } return (dwReturnTime); } if (nCode == HC_SKIP) { n++; if (MyEvent[n].hwnd == 0) { PostMessage(hWnd, WM_PLAY_END, 0, 0); PostMessage(hWnd, WM_END_HOOK, 0, 0); return FALSE; } } return CallNextHookEx(hHook, nCode, wp, lp); }

ジャーナルプレイバックのフックプロシージャです。 nCodeがHC_GETNEXTの時、次に実行すべきメッセージの情報を lpの指しているEVENTMSG構造体にセットします。 この時、timeメンバはこのメッセージが実行される時間でなくてはいけません。 記録されているtimeメンバは記録された時間です。これが実行される 時間は記録を開始した時間と再生を開始した時間の差だけ加えてやらなくては いけません。また、戻り値は次のメッセージを開始するまでの時間で なくてはいけません。誤差が生じて次のメッセージを開始するまでの時間が マイナスになることもよくあります。この場合は戻り値を0にして timeメンバを現在の時間にする必要があります。 前章でも書きましたが これをチェックする時dwReturnTimeをint型にキャストしないと大変わかりにくい バグとなります。

呼び出し側のプログラムは今までとほとんど同じなので一気に表示します。

// hook04.rcの一部 ///////////////////////////////////////////////////////////////////////////// // // Menu // MYMENU MENU DISCARDABLE BEGIN POPUP "ファイル(&F)" BEGIN MENUITEM "終了(&X)", IDM_END END POPUP "フック(&H)" BEGIN MENUITEM "記録開始(&S)", IDM_RECORDSTART MENUITEM "再生(&P)", IDM_PLAYSTART END END

単なるメニューのリソース・スクリプトです。

// hook04.cpp #ifndef STRICT #define STRICT #endif #include <windows.h> #include "resource.h" #include "hook04x.h" LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); ATOM InitApp(HINSTANCE); BOOL InitInstance(HINSTANCE, int); char szClassName[] = "hook04"; //ウィンドウクラス HINSTANCE hInst; int WINAPI WinMain(HINSTANCE hCurInst, HINSTANCE hPrevInst, LPSTR lpsCmdLine, int nCmdShow) { MSG msg; hInst = hCurInst; if (!InitApp(hCurInst)) return FALSE; if (!InitInstance(hCurInst, nCmdShow)) return FALSE; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } //ウィンドウ・クラスの登録 ATOM InitApp(HINSTANCE hInst) { WNDCLASSEX wc; wc.cbSize = sizeof(WNDCLASSEX); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = WndProc; //プロシージャ名 wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInst; //インスタンス wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wc.lpszMenuName = "MYMENU"; //メニュー名 wc.lpszClassName = (LPCSTR)szClassName; wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION); return (RegisterClassEx(&wc)); } //ウィンドウの生成 BOOL InitInstance(HINSTANCE hInst, int nCmdShow) { HWND hWnd; hWnd = CreateWindow(szClassName, "猫でもわかるフック",//タイトルバーにこの名前が表示されます WS_OVERLAPPEDWINDOW, //ウィンドウの種類 CW_USEDEFAULT, //X座標 CW_USEDEFAULT, //Y座標 190,//幅 75, //高さ NULL, //親ウィンドウのハンドル、親を作るときはNULL NULL, //メニューハンドル、クラスメニューを使うときはNULL hInst, //インスタンスハンドル NULL); if (!hWnd) return FALSE; ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); return TRUE; } //ウィンドウプロシージャ LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) { int id; switch (msg) { case WM_END_HOOK: EndHook(); break; case WM_RECORD_MAX: MessageBox(hWnd, "これ以上記録できません", "記録限界", MB_OK | MB_SYSTEMMODAL); break; case WM_PLAY_END: MessageBox(hWnd, "再生が終了しました", "再生終了", MB_OK | MB_SYSTEMMODAL); break; case WM_RECORD_END: MessageBox(hWnd, "記録を終了しました", "記録終了", MB_OK | MB_SYSTEMMODAL); break; case WM_INITMENU: MenuCheck(); break; case WM_CREATE: SetMainHWND(hWnd); break; case WM_COMMAND: switch (LOWORD(wp)) { case IDM_END: SendMessage(hWnd, WM_CLOSE, 0, 0); break; case IDM_RECORDSTART: StartRecord(); break; case IDM_PLAYSTART: StartPlay(); break; case IDM_HOOKEND: EndHook(); break; } break; case WM_CLOSE: if (!IsEndOK()) { MessageBox(hWnd, "フックが解除されていません", "注意", MB_OK); break; } id = MessageBox(hWnd, "終了してもよいですか", "終了確認", MB_YESNO | MB_ICONQUESTION); if (id == IDYES) { DestroyWindow(hWnd); } break; case WM_DESTROY: PostQuitMessage(0); break; default: return (DefWindowProc(hWnd, msg, wp, lp)); } return 0; }

呼び出し側のプログラムの説明はあまりいらないですね。当たり前ですが dllを作った時にできるlibをリンクするのを忘れないでください。 できあがったプログラムで遊んでみてください。結構面白いですよ。 MyEventをファイルに記録して、あとから再生できるように改良してみて下さい。 また、このプログラムは重複起動すると困ったことが起こります。 重複起動できないように改良してみて下さい。
[SDK第2部 Index] [総合Index] [Previous Chapter] [Next Chapter]

Update 24/Dec/1998 By Y.Kumei
当ホーム・ページの一部または全部を無断で複写、複製、 転載あるいはコンピュータ等のファイルに保存することを禁じます。