首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> Delphi

WinAPI 滑鼠鉤子回調函數 MouseProc 完整解析:攔截、監控與模擬滑鼠事件

     編輯:Delphi

MouseProc 回調函數概述

MouseProc 是 Windows 滑鼠鉤子 (WH_MOUSE 或 WH_MOUSE_LL) 的核心回調函數。當應用程式安裝滑鼠鉤子後,系統會在每次滑鼠事件 (移動、按鍵、滾輪、雙擊) 發生時呼叫此函數,讓開發者能夠攔截、監控、記錄或修改滑鼠訊息。廣泛應用於宏錄製、遊戲輔助、無障礙工具、UI 測試自動化等場景。

函數原型

LRESULT CALLBACK MouseProc(
  _In_ int    nCode,
  _In_ WPARAM wParam,
  _In_ LPARAM lParam
);

參數詳細說明

參數類型說明
nCodeint鉤子處理代碼:
HC_ACTION (0):正常處理,wParam/lParam 有效
HC_NOREMOVE (3):訊息已被 PeekMessage 移除但未從佇列刪除
若 nCode < 0:不得處理,必須呼叫 CallNextHookEx
wParamWPARAM滑鼠訊息識別碼:
WM_MOUSEMOVE、WM_LBUTTONDOWN/UP、WM_RBUTTONDOWN/UP、WM_MBUTTONDOWN/UP
WM_MOUSEWHEEL、WM_MOUSEHWHEEL、WM_LBUTTONDBLCLK 等
lParamLPARAM指向結構的指標:
WH_MOUSE:指向 MOUSEHOOKSTRUCT
WH_MOUSE_LL:指向 MSLLHOOKSTRUCT (含時間戳、注入標誌)

關鍵結構體定義

// 標準滑鼠鉤子 (WH_MOUSE)
typedef struct tagMOUSEHOOKSTRUCT {
  POINT  pt;           // 螢幕座標 (x, y)
  HWND   hwnd;         // 目標視窗控制代碼
  UINT   wHitTestCode; // Hit-test 代碼 (HTCLIENT, HTCAPTION 等)
  ULONG_PTR dwExtraInfo; // 額外資訊
} MOUSEHOOKSTRUCT, *PMOUSEHOOKSTRUCT;

// 低階滑鼠鉤子 (WH_MOUSE_LL) - 更豐富資訊
typedef struct tagMSLLHOOKSTRUCT {
  POINT  pt;           // 螢幕座標
  DWORD  mouseData;    // 滾輪增量 (WHEEL_DELTA=120) 或 XButton 資訊
  DWORD  flags;        // LLMHF_INJECTED (1) = 模擬輸入
  DWORD  time;         // 訊息時間戳 (GetMessageTime)
  ULONG_PTR dwExtraInfo;
} MSLLHOOKSTRUCT, *PMSLLHOOKSTRUCT;

返回值邏輯

  • nCode < 0:返回 CallNextHookEx(hHook, nCode, wParam, lParam)
  • 攔截訊息 (不傳遞給目標視窗):返回非零值 (通常為 1)
  • 繼續傳遞:返回 CallNextHookEx(...)

WH_MOUSE vs WH_MOUSE_LL 詳細比較

特性WH_MOUSE (標準)WH_MOUSE_LL (低階)
安裝需求需 DLL 注入 (SetWindowsHookEx 第4參數=0 時)同進程即可,無需 DLL (HInstance 可為 NULL)
攔截範圍特定執行緒 (第4參數=ThreadId) 或全系統 (0)全系統所有滑鼠輸入
座標系統螢幕座標螢幕座標
時間資訊無有 (time 欄位,精確到毫秒)
注入偵測無flags & LLMHF_INJECTED 偵測模擬輸入
效能影響高 (跨進程上下文切換)低 (同進程回調)
超時保護無有 (LowLevelHooksTimeout, 預設 300ms)
適用場景特定應用內部滑鼠監控全域熱鍵、宏錄製、螢幕錄製、防作弊

實戰範例 1:低階滑鼠鉤子記錄器 (Delphi)

unit MouseHookUnit;

interface

uses Windows, Messages, Classes, SysUtils;

type
  TMouseEventType = (meMove, meLDown, meLUp, meRDown, meRUp, 
                     meMDown, meMUp, meWheel, meHWheel, meLDouble, meRDouble, meMDouble);
  
  TMouseEvent = record
    EventType: TMouseEventType;
    X, Y: Integer;      // 螢幕座標
    WheelDelta: Integer; // 滾輪增量
    Injected: Boolean;  // 是否為模擬輸入
    TimeStamp: DWORD;   // 系統時間戳
  end;
  
  TMouseEventProc = procedure(const Event: TMouseEvent) of object;

  TLowLevelMouseHook = class
  private
    FHook: HHOOK;
    FOnMouseEvent: TMouseEventProc;
    class var FInstance: TLowLevelMouseHook;
    class function HookProc(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
    function GetEventType(Msg: UINT): TMouseEventType;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Install;
    procedure Uninstall;
    property OnMouseEvent: TMouseEventProc read FOnMouseEvent write FOnMouseEvent;
  end;

implementation

{ TLowLevelMouseHook }

constructor TLowLevelMouseHook.Create;
begin
  inherited;
  FInstance := Self;
end;

destructor TLowLevelMouseHook.Destroy;
begin
  Uninstall;
  FInstance := nil;
  inherited;
end;

class function TLowLevelMouseHook.HookProc(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
  HookStruct: PMSLLHOOKSTRUCT;
  Event: TMouseEvent;
begin
  Result := 0;
  if (nCode = HC_ACTION) and Assigned(FInstance) and Assigned(FInstance.FOnMouseEvent) then
  begin
    HookStruct := PMSLLHOOKSTRUCT(lParam);
    Event.X := HookStruct.pt.x;
    Event.Y := HookStruct.pt.y;
    Event.WheelDelta := 0;
    Event.Injected := (HookStruct.flags and LLMHF_INJECTED) <> 0;
    Event.TimeStamp := HookStruct.time;
    
    Event.EventType := FInstance.GetEventType(wParam);
    
    if wParam = WM_MOUSEWHEEL then
      Event.WheelDelta := SmallInt(HookStruct.mouseData shr 16)  // HIWORD
    else if wParam = WM_MOUSEHWHEEL then
      Event.WheelDelta := SmallInt(HookStruct.mouseData shr 16);
    
    // 同步呼叫事件處理 (注意:回調中避免耗時操作!)
    FInstance.FOnMouseEvent(Event);
  end;
  Result := CallNextHookEx(FInstance.FHook, nCode, wParam, lParam);
end;

function TLowLevelMouseHook.GetEventType(Msg: UINT): TMouseEventType;
begin
  case Msg of
    WM_MOUSEMOVE: Result := meMove;
    WM_LBUTTONDOWN: Result := meLDown;
    WM_LBUTTONUP: Result := meLUp;
    WM_RBUTTONDOWN: Result := meRDown;
    WM_RBUTTONUP: Result := meRUp;
    WM_MBUTTONDOWN: Result := meMDown;
    WM_MBUTTONUP: Result := meMUp;
    WM_MOUSEWHEEL: Result := meWheel;
    WM_MOUSEHWHEEL: Result := meHWheel;
    WM_LBUTTONDBLCLK: Result := meLDouble;
    WM_RBUTTONDBLCLK: Result := meRDouble;
    WM_MBUTTONDBLCLK: Result := meMDouble;
  else Result := meMove;
  end;
end;

procedure TLowLevelMouseHook.Install;
begin
  if FHook = 0 then
    FHook := SetWindowsHookEx(WH_MOUSE_LL, @HookProc, HInstance, 0);
end;

procedure TLowLevelMouseHook.Uninstall;
begin
  if FHook <> 0 then
  begin
    UnhookWindowsHookEx(FHook);
    FHook := 0;
  end;
end;

end.

實戰範例 2:主程式使用記錄器

unit MainUnit;

interface

uses Windows, Forms, StdCtrls, Controls, Classes, MouseHookUnit;

type
  TForm1 = class(TForm)
    btnInstall: TButton;
    btnUninstall: TButton;
    memoLog: TMemo;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure btnInstallClick(Sender: TObject);
    procedure btnUninstallClick(Sender: TObject);
  private
    MouseHook: TLowLevelMouseHook;
    procedure OnMouseEvent(const Event: TMouseEvent);
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  MouseHook := TLowLevelMouseHook.Create;
  MouseHook.OnMouseEvent := OnMouseEvent;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  MouseHook.Free;
end;

procedure TForm1.btnInstallClick(Sender: TObject);
begin
  MouseHook.Install;
  memoLog.Lines.Add('[Hook Installed]');
end;

procedure TForm1.btnUninstallClick(Sender: TObject);
begin
  MouseHook.Uninstall;
  memoLog.Lines.Add('[Hook Uninstalled]');
end;

procedure TForm1.OnMouseEvent(const Event: TMouseEvent);
const
  EventNames: array[TMouseEventType] of string = (
    'Move', 'LDown', 'LUp', 'RDown', 'RUp', 
    'MDown', 'MUp', 'Wheel', 'HWheel', 'LDouble', 'RDouble', 'MDouble'
  );
var
  Line: string;
begin
  // 限制日誌行數避免記憶體耗盡
  if memoLog.Lines.Count > 1000 then
    memoLog.Lines.Delete(0);
  
  Line := Format('[%s] X=%d Y=%d Injected=%s',
    [EventNames[Event.EventType], Event.X, Event.Y, 
     BoolToStr(Event.Injected, True)]);
  
  if Event.EventType in [meWheel, meHWheel] then
    Line := Line + Format(' Delta=%d', [Event.WheelDelta]);
  
  memoLog.Lines.Add(Line);
end;

end.

進階應用:滑鼠手勢識別

type
  TGesturePoint = record X, Y: Integer; end;
  TGestureEngine = class
  private
    FPoints: array of TGesturePoint;
    FMinPoints: Integer;
    procedure AnalyzeGesture;
  public
    constructor Create;
    procedure AddPoint(X, Y: Integer);
    procedure Clear;
    property OnGestureRecognized: TNotifyEvent;
  end;

procedure TGestureEngine.AddPoint(X, Y: Integer);
begin
  SetLength(FPoints, Length(FPoints) + 1);
  FPoints[High(FPoints)] := TGesturePoint.Create(X, Y);
  
  // 簡單策略:超過 20 點且移動距離夠大才分析
  if Length(FPoints) >= 20 then
    AnalyzeGesture;
end;

procedure TGestureEngine.AnalyzeGesture;
var
  StartPt, EndPt: TGesturePoint;
  DX, DY: Integer;
  Angle: Single;
begin
  StartPt := FPoints[0];
  EndPt := FPoints[High(FPoints)];
  DX := EndPt.X - StartPt.X;
  DY := EndPt.Y - StartPt.Y;
  
  // 過濾太小移動
  if (Abs(DX) < 50) and (Abs(DY) < 50) then Exit;
  
  Angle := ArcTan2(DY, DX) * 180 / Pi;  // -180 到 180
  
  // 簡單 8 方向識別
  case Round(Angle / 45) * 45 of
    0:    ; // 右
    45:   ; // 右下
    90:   ; // 下
    135:  ; // 左下
    180, -180: ; // 左
    -135: ; // 左上
    -90:  ; // 上
    -45:  ; // 右上
  end;
  
  Clear; // 重置
  if Assigned(OnGestureRecognized) then OnGestureRecognized(Self);
end;

模擬滑鼠輸入 (配合鉤子測試)

uses Windows;

procedure SimulateMouseClick(X, Y: Integer; RightButton: Boolean = False);
var
  Input: TInput;
begin
  ZeroMemory(@Input, SizeOf(TInput));
  Input._type := INPUT_MOUSE;
  Input.mi.dx := X * (65535 div GetSystemMetrics(SM_CXSCREEN));
  Input.mi.dy := Y * (65535 div GetSystemMetrics(SM_CYSCREEN));
  Input.mi.dwFlags := MOUSEEVENTF_ABSOLUTE or MOUSEEVENTF_MOVE;
  SendInput(1, Input, SizeOf(TInput));
  
  ZeroMemory(@Input, SizeOf(TInput));
  Input._type := INPUT_MOUSE;
  if RightButton then
    Input.mi.dwFlags := MOUSEEVENTF_RIGHTDOWN
  else
    Input.mi.dwFlags := MOUSEEVENTF_LEFTDOWN;
  SendInput(1, Input, SizeOf(TInput));
  
  ZeroMemory(@Input, SizeOf(TInput));
  Input._type := INPUT_MOUSE;
  if RightButton then
    Input.mi.dwFlags := MOUSEEVENTF_RIGHTUP
  else
    Input.mi.dwFlags := MOUSEEVENTF_LEFTUP;
  SendInput(1, Input, SizeOf(TInput));
end;

// 注意:SendInput 產生的輸入在 WH_MOUSE_LL 中 flags 會帶有 LLMHF_INJECTED
// 可用於區分真實用戶操作 vs 程式模擬

注意事項與最佳實踐

  1. 回調執行時間限制:WH_MOUSE_LL 回調必須在 300ms 內返回 (可修改註冊表 HKEY_CURRENT_USER\Control Panel\Desktop\LowLevelHooksTimeout),否則系統會自動移除鉤子。嚴禁在回調中執行:檔案 I/O、MessageBox、資料庫操作、同步等待、複雜計算。
  2. 執行緒安全:回調在系統執行緒中執行,若需更新 UI,請使用 PostMessage、TThread.Queue 或執行緒安全佇列。
  3. 座標轉換:鉤子提供螢幕座標,若需客戶區座標,使用 ScreenToClient(TargetWnd, Pt)。
  4. DLL 注入 (WH_MOUSE):32位 DLL 注入 32位進程,64位注入 64位。跨位數需兩套 DLL。
  5. UAC 完整性等級:低完整性進程無法鉤住高完整性進程 (如以管理員執行的程式)。
  6. 防作繞過:惡意軟體可能用 UnhookWindowsHookEx、SetWindowsHookEx 覆蓋、直接修改鏈結指標。防禦需定期驗證鉤子完整性。

相關 API 快速查閱

  • SetWindowsHookEx(WH_MOUSE_LL, ...) - 安裝低階滑鼠鉤子
  • SetWindowsHookEx(WH_MOUSE, ...) - 安裝標準滑鼠鉤子
  • CallNextHookEx - 傳遞給下一個鉤子
  • UnhookWindowsHookEx - 移除鉤子
  • GetAsyncKeyState - 即時查詢按鍵狀態 (配合鉤子判斷修飾鍵)
  • GetCursorPos / SetCursorPos - 獲取/設定滑鼠位置
  • SendInput - 模擬輸入 (推薦)
  • mouse_event - 舊版模擬輸入 (已棄用)
  • WindowFromPoint / ChildWindowFromPoint - 座標轉視窗控制代碼

總結

MouseProc 配合 WH_MOUSE_LL 是實現全域滑鼠監控、宏錄製、手勢識別、自動化測試的核心技術。關鍵掌握:低階鉤子無需 DLL、MSLLHOOKSTRUCT 提供豐富資訊、300ms 超時鐵律、LLMHF_INJECTED 偵測模擬輸入。正確處理執行緒切換、資料佇列、座標轉換,即可構建健壯的滑鼠事件處理系統。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved