SetWindowsHookEx 函數的第一個參數表示鉤子類型, 共有 14 種選擇, 前面我們已經用過兩種:
WH_KEYBOARD、WH_MOUSE.
系統會為每一種類型的鉤子建立一個表(那就是 14 個表), 譬如某個應用程序啟動了鍵盤鉤子, 我們自己的程序也啟動了鍵盤鉤子, 同樣是鍵盤鉤子就會進入同一個表. 這個表(可能不止一個, 可能還會有鼠標鉤子等等)就是傳說中的"鉤子鏈".
假如某個鉤子鏈中共進來了三個鉤子(譬如是: 鉤子A、鉤子B、鉤子C 依次進來), 最後進來的 "鉤子C" 會先執行.
是不是先進後出? 我覺得應該說成: 後進先出! 這有區別嗎? 有! 因為先進來的不一定出得來.
最後進了的鉤子會最先得到執行, 先前進來的鉤子(鉤子A、鉤子B)能不能得到執行那還得兩說, 這得有正在執行的 "鉤子C" 說了算.
如果 "鉤子C" 的函數中包含了 CallNextHookEx 語句, 那麼 "鉤子A、鉤子B" 就有可能得以天日; 不然就只有等著相應的
UnhookWindowsHookEx 來把它們帶走(我想起趙本山的小品...).
這時你也許會想到: 這樣太好了, 我以後就不加 CallNextHookEx , 只讓自己的鉤子"橫行"; 但如果是你的鉤子先進去的呢?
所以 Windows 建議: 鉤子函數要調用 CallNextHookEx, 並把它的返回值當作鉤子函數自己的返回值.
CallNextHookEx 同時要給鉤子鏈中的下一個(或許應該叫上一個)鉤子傳遞參數(譬如在鍵盤消息中按了哪個鍵). 一個鍵盤鉤子和鼠標鉤子的參數一樣嗎? 當然不一樣, 所以它們也不在一個 "鏈" 中啊; 同一個鏈中的鉤子的類型肯定是一樣的.
再聊聊鉤子函數的返回值:
在這之前, 鉤子函數的返回值, 我們都是遵循 Windows 的慣例, 返回了 CallNextHookEx 的返回值.
如果 CallNextHookEx 成功, 它會返回下一個鉤子的返回值, 是個連環套;
如果 CallNextHookEx 失敗, 會返回 0, 這樣鉤子鏈也就斷了, 只有當前鉤子還在執行任務.
不同類型的鉤子函數的返回值是不同的, 對鍵盤鉤子來講如果返回一個非 0 的值, 表示它處理完以後就把消息給消滅了.
換句話說:
如果給鍵盤的鉤子函數 Result := 0; 說明消息被鉤子攔截並處理後就給 "放" 了;
如果給鍵盤的鉤子函數 Result := 1; 說明消息被鉤子攔截並處理後又給 "殺" 了.
在下面的例子中, 我們干脆不使用 CallNextHookEx (反正暫時就我一個鉤子), 直接給返回值!
這是接下來例子的演示動畫:
動畫中, 我在三種狀態下分別給 Memo 輸入了字母 a
當沒啟動鉤子時, Memo 是可以正常輸入的;
當鉤子函數返回 0, 鉤上以後, 先執行了鉤子函數的功能(返回鍵值), 字母 a 也能輸入成功;
當鉤子函數返回非 0 值(譬如1), 鉤上以後, 就只執行了鉤子函數的功能(返回鍵值), 可憐的 Memo 不知道發生了什麼.
但這裡又有了新問題: 鉤子函數返回鍵值時怎麼...不是一個?
先提醒: 在前面用 Beep 測試時, 你有沒有發現那個聲音也不只一次, 這是一個道理.
因為按一次鍵就會發出兩個消息: WM_KEYDOWN、WM_KEYUP, 我們沒有指定攔截哪個, 就都攔截了.
那怎麼區分這兩個消息呢? 秘密在鍵盤鉤子函數的第三個參數 lParam 裡面, 等下一個話題再研究吧.
//示例代碼:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
Memo1: TMemo;
procedure FormDestroy(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
end;
{鉤子函數聲明}
function MyKeyHook(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
Form1: TForm1;
implementation
{$R *.dfm}
var
hook: HHOOK;
function MyKeyHook(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT;
begin
Form1.Memo1.Lines.Add(IntToStr(wParam)); {參數二是鍵值}
Result := 0; {分別測試返回 0 或非 0 這兩種情況}
end;
{派出鉤子}
procedure TForm1.Button1Click(Sender: TObject);
begin
hook := SetWindowsHookEx(WH_KEYBOARD, MyKeyHook, HInstance, GetCurrentThreadId);
Memo1.Clear;
Text := '鉤子啟動';
end;
{收回鉤子}
procedure TForm1.Button2Click(Sender: TObject);
begin
UnhookWindowsHookEx(hook);
Text := '鉤子關閉';
end;
{如果忘了收回鉤子...}
procedure TForm1.FormDestroy(Sender: TObject);
begin
if hook<>0 then UnhookWindowsHookEx(hook);
end;
end.
小秘密: 發現沒有, 這次在 SetWindowsHookEx 時, 第二參數(函數地址), 沒有使用 @、也沒有用 Addr, 怎麼也行呢?
因為函數名本身就是個地址.