實作一個鍵盤監聽器,能在背景捕獲所有進程的按鍵釋放事件 (WM_KEYUP),並透過自訂訊息通知主視窗顯示虛擬鍵碼。採用 WH_JOURNALRECORD Hook 類型,搭配 DLL 注入達成全域監聽。
| Hook 類型 | 用途 | 是否需 DLL | 權限需求 |
|---|---|---|---|
| WH_KEYBOARD | 攔截鍵盤訊息 (WM_KEYDOWN/UP) | 是 (全域時) | 標準 |
| WH_KEYBOARD_LL | 低層級鍵盤 Hook (原始輸入) | 否 (可在 EXE) | 需 UI 權限、Vista+ 需簽署/提權 |
| WH_JOURNALRECORD | 記錄輸入事件 (滑鼠/鍵盤) 供回放 | 是 | 標準 |
WH_JOURNALRECORD 歷史用於巨集錄製,能捕獲 EVENTMSG 結構 (含訊息、參數、時間、座標)。雖然屬「記錄」性質,但同樣能攔截全域鍵盤事件,且不需低層級權限,適合教學與舊版相容專案。
DllEntryPoint、SetHook 匯出函式、HookProc 回呼。安裝 WH_JOURNALRECORD,收到 HC_ACTION + WM_KEYUP 時 PostMessage 通知主視窗。LoadLibrary 載入 DLL,GetProcAddress 取得 SetHook,呼叫安裝/移除。視窗程序 WndProc (或 TApplicationEvents) 處理自訂訊息顯示按鍵碼。
// Project1.cpp - DLL 專案
#include
#include
#pragma hdrstop
// 共用常數
const UINT MY_HOOK_MSG = 2000; // 自訂通知訊息
const LPARAM MY_HOOK_LPARAM = 1; // 識別標記
// 全域變數 (每個進程各自一份,但 HookProc 執行於注入進程中)
static HINSTANCE g_hInst = NULL;
static HWND g_hTargetWnd = NULL; // 主程式視窗句柄
static HHOOK g_hHook = NULL;
// 標準 DLL 入口點
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*) {
if (reason == DLL_PROCESS_ATTACH) g_hInst = hinst;
return 1;
}
// 匯出函式:安裝/移除 Hook
extern "C" __declspec(dllexport) void __stdcall SetHook(HWND hTargetWnd, BOOL bInstall) {
if (bInstall) {
g_hTargetWnd = hTargetWnd;
// WH_JOURNALRECORD: nCode=HC_ACTION 時 lParam 指向 EVENTMSG
g_hHook = SetWindowsHookEx(WH_JOURNALRECORD, (HOOKPROC)HookProc, g_hInst, 0);
} else {
if (g_hHook) { UnhookWindowsHookEx(g_hHook); g_hHook = NULL; }
}
}
// Hook 回呼函式
LRESULT CALLBACK HookProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode == HC_ACTION) {
EVENTMSG* pMsg = (EVENTMSG*)lParam;
if (pMsg->message == WM_KEYUP) {
// pMsg->paramL 低位元組為虛擬鍵碼
BYTE vk = LOBYTE(pMsg->paramL);
if (g_hTargetWnd)
PostMessage(g_hTargetWnd, MY_HOOK_MSG, vk, MY_HOOK_LPARAM);
}
}
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
// Main.cpp - EXE 專案
#include
#pragma hdrstop
#include "Main.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
// DLL 函式指標型別
typedef void (__stdcall *SET_HOOK_PROC)(HWND, BOOL);
static HINSTANCE g_hDll = NULL;
static SET_HOOK_PROC g_pSetHook = NULL;
const UINT MY_HOOK_MSG = 2000;
const LPARAM MY_HOOK_LPARAM = 1;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) {}
// 表單建立:載入 DLL、安裝 Hook
void __fastcall TForm1::FormCreate(TObject *Sender) {
g_hDll = LoadLibrary("Project1.dll");
if (!g_hDll) { ShowMessage("無法載入 Project1.dll"); Application->Terminate(); return; }
g_pSetHook = (SET_HOOK_PROC)GetProcAddress(g_hDll, "SetHook");
if (!g_pSetHook) { ShowMessage("找不到 SetHook 函式"); FreeLibrary(g_hDll); Application->Terminate(); return; }
g_pSetHook(Handle, TRUE); // 安裝 Hook
}
// 表單關閉:移除 Hook、卸載 DLL
void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action) {
if (g_pSetHook) g_pSetHook(NULL, FALSE);
if (g_hDll) FreeLibrary(g_hDll);
}
// 使用 TApplicationEvents 元件處理自訂訊息 (OnMessage 事件)
void __fastcall TForm1::ApplicationEvents1Message(tagMSG &Msg, bool &Handled) {
if (Msg.message == MY_HOOK_MSG && Msg.lParam == MY_HOOK_LPARAM) {
ShowMessage("收到 Hook 按鍵訊息!\n\n虛擬鍵碼: " + IntToStr(Msg.wParam));
Handled = true;
}
}
typedef struct tagEVENTMSG {
UINT message; // 訊息代碼 (WM_KEYDOWN, WM_KEYUP, WM_MOUSEMOVE...)
UINT paramL; // 訊息參數 wParam (鍵盤: 虛擬鍵碼在 LOBYTE)
UINT paramH; // 訊息參數 lParam (鍵盤: 掃描碼、擴充旗標等)
DWORD time; // 事件時間戳 (GetTickCount)
DWORD dwExtraInfo; // 額外資訊
} EVENTMSG;
鍵盤事件中:
- paramL 低位元組 = 虛擬鍵碼 (VK_A, VK_RETURN 等)。
- paramH 低位元組 = 掃描碼,高位元組含擴充鍵、Alt 鍵狀態等旗標。
回呼**必須**呼叫 CallNextHookEx(g_hHook, nCode, wParam, lParam) 將事件傳遞給下一個 Hook 或系統預設處理,否則會導致鍵盤輸入系統癱瘓 (無法輸入字元、熱鍵失效)。
DLL 被注入每個進程時,g_hInst, g_hTargetWnd, g_hHook 在各進程各有一份副本。但 SetHook 只在主程式進程呼叫一次,因此只有主程式進程的 g_hTargetWnd 被設定。Hook 回呼在**目標進程**執行時,其 g_hTargetWnd 為 0。
解決方案: 本範例利用 SetHook 只在主程式呼叫的特性,僅在主程式進程建立 Hook。但 WH_JOURNALRECORD 本質是全域 Hook,系統會將 DLL 映射到所有進程,並在每個進程內呼叫 HookProc。此時目標進程的 g_hTargetWnd 為 0,PostMessage 會失敗。
正確做法: 應將 g_hTargetWnd 存入**共享記憶體** (如 CreateFileMapping + PAGE_READWRITE),或使用 WH_KEYBOARD 搭配共享資料段 (#pragma data_seg) 讓所有進程共享同一份 Hook 句柄與目標視窗句柄。參考文章 7842 第 3、5 節共享記憶體實作。
建議將自訂訊息改用 RegisterWindowMessage("MyUniqueHookMsg_2024") 取得系統唯一訊息代碼,避免與其他應用程式衝突。
LoadLibrary 成功;SetWindowsHookEx 回傳非 NULL (失敗可呼叫 GetLastError,常見錯誤 1428/1429 為 Hook 程序無效/權限不足)。CallNextHookEx、在回呼執行阻塞操作 (MessageBox、檔案 I/O、Sleep)。回呼必須極速返回,僅 PostMessage 通知主程式。LowLevelHooksTimeout (預設 300ms) 系統會自動移除 Hook;需在獨立執行緒處理耗時邏輯;UAC 高權限進程需簽署驅動或關閉 UAC。現代專案建議優先評估 WH_KEYBOARD_LL + 執行緒池架構;舊版相容或教學專案 WH_JOURNALRECORD + 共享記憶體 DLL 仍具參考價值。