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

WinAPI 訊息過濾鉤子 MsgFilterProc 完整解析:模態迴圈訊息攔截與處理

     編輯:Delphi

MsgFilterProc 回調函數概述

MsgFilterProc 是 Windows 訊息過濾鉤子 (WH_MSGFILTER) 的回調函數。與全域的 WH_SYSMSGFILTER 不同,WH_MSGFILTER 是執行緒局部的鉤子,僅監控安裝它的執行緒中的模態迴圈訊息 (對話方塊、訊息方塊、功能表、捲軸)。它無需 DLL,可直接在應用程式中安裝,適合為應用程式自身的模態 UI 添加熱鍵、自定義導航、攔截特定按鍵等功能。

函數原型

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

參數詳細說明

參數類型說明
nCodeint模態迴圈類型代碼:
MSGF_DIALOGBOX (0):對話方塊 (DialogBox/DialogBoxParam/DialogBoxIndirect)
MSGF_MESSAGEBOX (1):訊息方塊 (MessageBox/MessageBoxEx)
MSGF_MENU (2):功能表 (TrackPopupMenu/TrackPopupMenuEx)
MSGF_SCROLLBAR (5):捲軸操作
MSGF_NEXTWINDOW (6):視窗切換 (Alt+Tab/Alt+Esc)
MSGF_MAX (8):最大值標記
wParamWPARAM訊息 ID (如 WM_KEYDOWN、WM_COMMAND、WM_MOUSEWHEEL)
lParamLPARAM指向 MSG 結構的指標,包含完整訊息資訊 (hwnd, message, wParam, lParam, time, pt)

返回值邏輯

  • 0:訊息未被處理,繼續分派給目標視窗程序
  • 非零:訊息已被處理/攔截,模態迴圈不再分派該訊息
  • nCode < 0:必須返回 CallNextHookEx(hHook, nCode, wParam, lParam)

WH_MSGFILTER vs WH_SYSMSGFILTER 核心差異

特性WH_MSGFILTERWH_SYSMSGFILTER
作用範圍僅當前執行緒的模態迴圈全系統所有執行緒的模態迴圈
安裝需求同進程,無需 DLL必須在 DLL 中,注入所有進程
攔截目標自身應用的 DialogBox/MessageBox/Menu所有進程的系統模態 UI
典型用途應用內部熱鍵、對話框增強、無障礙系統級熱鍵、全域無障礙、UI 自動化
效能影響極低較高 (跨進程)

實戰範例:為應用程式所有對話框添加全域熱鍵 (Delphi)

unit MsgFilterHookUnit;

interface

uses Windows, Messages, Classes, SysUtils;

type
  THotKeyAction = procedure(HotKeyID: Integer) of object;

  TMsgFilterHook = class
  private
    FHook: HHOOK;
    FHotKeys: array[1..10] of record
      Key: Word;           // 虛擬鍵碼
      Modifiers: Integer;  // MOD_CONTROL, MOD_ALT, MOD_SHIFT, MOD_WIN
      Action: THotKeyAction;
      Active: Boolean;
    end;
    FHotKeyCount: Integer;
    class var FInstance: TMsgFilterHook;
    class function HookProc(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
    function CheckHotKey(const Msg: TMsg): Boolean;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Install;
    procedure Uninstall;
    function RegisterHotKey(Key: Word; Modifiers: Integer; Action: THotKeyAction): Integer;
    procedure UnregisterHotKey(Index: Integer);
  end;

implementation

{ TMsgFilterHook }

constructor TMsgFilterHook.Create;
begin
  inherited;
  FInstance := Self;
  FHotKeyCount := 0;
end;

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

class function TMsgFilterHook.HookProc(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
  pMsg: PMsg;
begin
  Result := 0;
  if (nCode >= 0) and Assigned(FInstance) then
  begin
    // 只處理按鍵訊息
    if (wParam = WM_KEYDOWN) or (wParam = WM_SYSKEYDOWN) then
    begin
      pMsg := PMsg(lParam);
      if FInstance.CheckHotKey(pMsg^) then
      begin
        Result := 1; // 攔截該按鍵,不讓對話框處理
        Exit;
      end;
    end;
  end;
  Result := CallNextHookEx(FInstance.FHook, nCode, wParam, lParam);
end;

function TMsgFilterHook.CheckHotKey(const Msg: TMsg): Boolean;
var
  i: Integer;
  ModifiersMatch: Boolean;
begin
  Result := False;
  for i := 1 to FHotKeyCount do
  begin
    if not FHotKeys[i].Active then Continue;
    if Msg.wParam <> FHotKeys[i].Key then Continue;
    
    // 檢查修飾鍵
    ModifiersMatch := True;
    if (FHotKeys[i].Modifiers and MOD_CONTROL) <> 0 then
      ModifiersMatch := ModifiersMatch and (GetAsyncKeyState(VK_CONTROL) < 0);
    if (FHotKeys[i].Modifiers and MOD_ALT) <> 0 then
      ModifiersMatch := ModifiersMatch and (GetAsyncKeyState(VK_MENU) < 0);
    if (FHotKeys[i].Modifiers and MOD_SHIFT) <> 0 then
      ModifiersMatch := ModifiersMatch and (GetAsyncKeyState(VK_SHIFT) < 0);
    if (FHotKeys[i].Modifiers and MOD_WIN) <> 0 then
      ModifiersMatch := ModifiersMatch and (GetAsyncKeyState(VK_LWIN) < 0 or GetAsyncKeyState(VK_RWIN) < 0);
    
    if ModifiersMatch then
    begin
      // 執行動作 (在主執行緒安全執行)
      if Assigned(FHotKeys[i].Action) then
      begin
        // 使用 PostMessage 避免在鉤子回調中直接執行複雜邏輯
        PostMessage(Application.MainFormHandle, WM_USER + 100, i, 0);
      end;
      Result := True;
      Break;
    end;
  end;
end;

procedure TMsgFilterHook.Install;
begin
  if FHook = 0 then
    FHook := SetWindowsHookEx(WH_MSGFILTER, @HookProc, 0, GetCurrentThreadId);
    // 第3參數 0 = 當前進程模組句柄 (EXE 即可)
    // 第4參數 = 當前執行緒 ID
end;

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

function TMsgFilterHook.RegisterHotKey(Key: Word; Modifiers: Integer; Action: THotKeyAction): Integer;
begin
  Result := -1;
  if FHotKeyCount >= 10 then Exit;
  
  Inc(FHotKeyCount);
  FHotKeys[FHotKeyCount].Key := Key;
  FHotKeys[FHotKeyCount].Modifiers := Modifiers;
  FHotKeys[FHotKeyCount].Action := Action;
  FHotKeys[FHotKeyCount].Active := True;
  Result := FHotKeyCount;
end;

procedure TMsgFilterHook.UnregisterHotKey(Index: Integer);
begin
  if (Index >= 1) and (Index <= FHotKeyCount) then
    FHotKeys[Index].Active := False;
end;

end.

主程式使用範例

unit MainUnit;

interface

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

type
  TForm1 = class(TForm)
    btnShowDialog: TButton;
    btnShowMsgBox: TButton;
    btnInstallHook: TButton;
    btnUninstallHook: TButton;
    memoLog: TMemo;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure btnShowDialogClick(Sender: TObject);
    procedure btnShowMsgBoxClick(Sender: TObject);
    procedure btnInstallHookClick(Sender: TObject);
    procedure btnUninstallHookClick(Sender: TObject);
  private
    MsgFilterHook: TMsgFilterHook;
    procedure WndProc(var Msg: TMessage); override;
    procedure OnHotKeyAction(HotKeyID: Integer);
    procedure ShowCustomDialog;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  MsgFilterHook := TMsgFilterHook.Create;
  
  // 註冊熱鍵:Ctrl+F1 = 顯示幫助, Alt+F4 = 阻止關閉, Ctrl+S = 快速儲存
  MsgFilterHook.RegisterHotKey(VK_F1, MOD_CONTROL, OnHotKeyAction);  // ID=1
  MsgFilterHook.RegisterHotKey(VK_F4, MOD_ALT, OnHotKeyAction);      // ID=2
  MsgFilterHook.RegisterHotKey(Ord('S'), MOD_CONTROL, OnHotKeyAction); // ID=3
end;

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

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

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

procedure TForm1.WndProc(var Msg: TMessage);
begin
  if Msg.Msg = WM_USER + 100 then
  begin
    OnHotKeyAction(Msg.WParam);
    Exit;
  end;
  inherited;
end;

procedure TForm1.OnHotKeyAction(HotKeyID: Integer);
begin
  case HotKeyID of
    1: // Ctrl+F1
    begin
      memoLog.Lines.Add('[HotKey] Ctrl+F1: 顯示線上說明');
      // Application.HelpCommand(HELP_FINDER, 0);
    end;
    2: // Alt+F4
    begin
      memoLog.Lines.Add('[HotKey] Alt+F4: 攔截關閉對話框');
      // 返回非零值已在鉤子中處理,對話框不會收到 WM_SYSCOMMAND/SC_CLOSE
    end;
    3: // Ctrl+S
    begin
      memoLog.Lines.Add('[HotKey] Ctrl+S: 快速儲存');
      // SaveCurrentDocument();
    end;
  end;
end;

procedure TForm1.btnShowDialogClick(Sender: TObject);
begin
  // 測試:顯示一個模態對話框,嘗試按 Ctrl+F1, Alt+F4, Ctrl+S
  ShowCustomDialog;
end;

procedure TForm1.btnShowMsgBoxClick(Sender: TObject);
begin
  // 測試:訊息方塊也會觸發鉤子 (MSGF_MESSAGEBOX)
  MessageBox(Handle, '測試 MsgFilter Hook\n按 Ctrl+F1 或 Alt+F4', '測試', MB_OK or MB_ICONINFORMATION);
end;

procedure TForm1.ShowCustomDialog;
var
  Dlg: TForm;
begin
  Dlg := TForm.Create(nil);
  try
    Dlg.Caption := '測試對話框 - 按熱鍵試試';
    Dlg.Position := poScreenCenter;
    Dlg.BorderStyle := bsDialog;
    Dlg.ClientWidth := 300;
    Dlg.ClientHeight := 150;
    with TLabel.Create(Dlg) do
    begin
      Parent := Dlg;
      Caption := '熱鍵測試:\nCtrl+F1 = 幫助\nAlt+F4 = 攔截關閉\nCtrl+S = 儲存';
      Left := 20; Top := 20; Width := 260; Height := 80;
    end;
    Dlg.ShowModal;
  finally
    Dlg.Free;
  end;
end;

end.

進階應用:功能表鍵盤導航增強

// 在 HookProc 中處理 MSGF_MENU
if nCode = MSGF_MENU then
begin
  pMsg := PMsg(lParam);
  if (pMsg.message = WM_KEYDOWN) then
  begin
    case pMsg.wParam of
      VK_F1: // F1 顯示功能表項說明
      begin
        // 獲取當前功能表控制代碼
        // 發送自定義訊息顯示工具提示
        Result := 1;
        Exit;
      end;
      VK_ESCAPE: // ESC 關閉功能表 (系統預設行為,可攔截修改)
      begin
        // 自定義邏輯
      end;
      'A'..'Z': // 字母鍵快速選擇功能表項
      begin
        // 模擬功能表助憶鍵行為
      end;
    end;
  end;
end;

捲軸訊息攔截 (MSGF_SCROLLBAR)

// 攔截捲軸鍵盤操作,實現自定義捲動行為
if nCode = MSGF_SCROLLBAR then
begin
  pMsg := PMsg(lParam);
  if (pMsg.message = WM_KEYDOWN) then
  begin
    case pMsg.wParam of
      VK_UP, VK_DOWN, VK_LEFT, VK_RIGHT,
      VK_PRIOR, VK_NEXT, VK_HOME, VK_END:
      begin
        // 可修改捲動步長、添加平滑捲動動畫
        // 返回 1 攔截原始捲動,自行發送 WM_VSCROLL/WM_HSCROLL
      end;
    end;
  end;
end;

注意事項與最佳實踐

  1. 僅影響當前執行緒:若應用有多執行緒且各自跑模態迴圈,需在每個執行緒分別安裝 WH_MSGFILTER。
  2. 回調執行緒上下文:回調在觸發訊息的執行緒中執行 (即主執行緒),可直接操作 VCL,但仍建議耗時操作用 PostMessage。
  3. 不影響非模態視窗:普通 Show 顯示的視窗不走模態迴圈,不會觸發此鉤子。
  4. 系統模態對話框:MessageBox、DialogBox 等系統 API 建立的模態迴圈會觸發;但 TForm.ShowModal 內部也是呼叫 DialogBox,同樣有效。
  5. 鉤子鏈順序:若同一執行緒安裝多個 WH_MSGFILTER,按安裝順序逆序調用 (最後安裝最先調用)。
  6. 避免死循環:在回調中發送訊息給同一視窗可能再次觸發鉤子,需加標誌位防重入。

相關 API 快速查閱

  • SetWindowsHookEx(WH_MSGFILTER, proc, 0, GetCurrentThreadId) - 安裝執行緒局部訊息過濾鉤子
  • CallNextHookEx - 傳遞鉤子鏈
  • UnhookWindowsHookEx - 移除鉤子
  • GetAsyncKeyState - 即時查詢修飾鍵狀態
  • IsDialogMessage - 系統內部處理對話框鍵盤導航 (可參考實現自定義導航)
  • TrackPopupMenuEx - 觸發 MSGF_MENU 迴圈

總結

MsgFilterProc 配合 WH_MSGFILTER 是為應用程式自身模態 UI (對話框、訊息框、功能表、捲軸) 添加全域熱鍵、自定義鍵盤導航、攔截特定按鍵的輕量級方案。優勢在於:無需 DLL、僅影響自身執行緒、安裝簡單、效能開銷極低。關鍵掌握:nCode 區分迴圈類型、wParam 識別訊息、lParam 獲取完整 MSG 結構、返回非零攔截訊息。適用於增強現有應用的模態介面交互體驗。

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