Delphi之钩子函数

来源:互联网 发布:淘宝一直显示等待揽收 编辑:程序博客网 时间:2024/06/06 20:58
目前对钩子的理解:

    譬如我们用鼠标在某个窗口上双击了一次, 或者给某个窗口输入了一个字母 A;首先发现这些事件的不是窗口, 而是系统!

    然后系统告诉窗口: 喂! 你让人点了, 并且是连续点了两鼠标, 你准备怎么办?或者是系统告诉窗口: 喂! 有人向你家里扔砖头了, 不信你看看, 那块砖头是 A.

    这时窗口的对有些事件会忽略、对有些事件会做出反应:譬如, 可能对鼠标单击事件忽略, 窗口想: 你单击我不要紧, 累死你我不负责;但一旦谁要双击我, 我会马上行动, 给你点颜色瞧瞧!这里窗口准备要采取的行动, 就是我们提前写好的事件.用 Windows 的话说, 窗口的事件就是系统发送给窗口的消息; 窗口要采取的行动(事件代码)就是窗口的回调函数.

    但是! 往往隔墙有耳. 系统要通知给窗口的"话"(消息), 可能会被另一个家伙(譬如是一个贼)提前听到!有可能这个贼就是专门在这等情报的, 贼知道后, 往往在窗口知道以前就采取了行动!并且这个贼对不同的消息会采取不同的行动方案, 它的行动方案一般也是早就准备好的;当然这个贼也不是对什么消息都感兴趣, 对不感兴趣的消息也就无须制定相应的行动方案.

总结: 这个"贼"就是我们要设置的钩子; "贼"的"行动方案"就是钩子函数, 或者叫钩子的回调函数.钩子分两种, 一种是系统级的全局钩子; 一种是线程级的钩子.全局钩子函数需要定义在 DLL 中, 从线程级的钩子开始比较简单.

其实钩子函数就三个:
设置钩子: SetWindowsHookEx
释放钩子: UnhookWindowsHookEx
继续钩子: CallNextHookEx
在线程级的钩子中经常用到 GetCurrentThreadID 函数来获取当前线程的 ID.

下面例子中设定了一个线程级的键盘钩子, 专门拦截字母 A.
[delphi] view plaincopy
  1. unit Unit1;   
  2.    
  3. interface   
  4.    
  5. uses   
  6.   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,   
  7.   Dialogs;   
  8.    
  9. type   
  10.   TForm1 = class(TForm)   
  11.     procedure FormCreate(Sender: TObject);   
  12.     procedure FormDestroy(Sender: TObject);   
  13.   end;   
  14.    
  15. {声明键盘钩子回调函数; 其参数传递方式要用 API 的 stdcall}   
  16. function KeyHook(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;   
  17.    
  18. var   
  19.   Form1: TForm1;   
  20.    
  21. implementation   
  22.    
  23. {$R *.DFM}   
  24.    
  25. var   
  26.   hook: HHOOK; {定义一个钩子句柄}   
  27.    
  28. {实现键盘钩子回调函数}   
  29. function KeyHook(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT;   
  30. begin   
  31.   if (wParam = 65then Beep; {每拦截到字母 A 会发声}   
  32.   Result := CallNextHookEx(hook, nCode, wParam, lParam);   
  33. end;   
  34.    
  35. {设置键盘钩子}   
  36. procedure TForm1.FormCreate(Sender: TObject);   
  37. begin   
  38.   hook := SetWindowsHookEx(WH_KEYBOARD, @KeyHook, 0, GetCurrentThreadID);   
  39. end;   
  40.    
  41. {释放键盘钩子}   
  42. procedure TForm1.FormDestroy(Sender: TObject);   
  43. begin   
  44.   UnhookWindowsHookEx(hook);   
  45. end;   
  46.    
  47. end.  
尽管这个例子已经很简单了, 但还不足以让人明白彻底; 下面还得从更简单的开始. 

钩子函数虽然不多, 但其参数复杂, 应该从参数入手才能深入进去.

UnhookWindowsHookEx 只需要 SetWindowsHookEx 返回的钩子句柄作参数, 这个简单;

先看看 SetWindowsHookEx 的声明:

SetWindowsHookEx(
idHook: Integer; {钩子类型}
lpfn: TFNHookProc; {函数指针}
hmod: HINST; {包含钩子函数的模块(EXE、DLL)的句柄}
dwThreadId: DWORD {关联的线程}
): HHOOK;

第一个参数非常麻烦, 从后面说:

参数四 dwThreadId : 在设置全局钩子时这个参数一般是 0, 表示关联所有线程; 本例是线程级的钩子, 所以是GetCurrentThreadId.

参数三 hmod: 是模块实例的句柄, 在 EXE 和 DLL 中都可以用 HInstance 得到当前实例的句柄; 直接用 API 也可以:GetModuleHandle(nil).

参数二 lpfn: 是钩子函数的指针, 用 @ 和 Addr 函数都可以得到函数指针; 这里的关键是那个钩子函数:首先不同的钩子类型对应着不同的钩子函数结构, Win32 共有 14 种钩子类型, 这是 详细注释;本例用的是键盘钩子, 键盘钩子的回调函数的参数结构在 这里, 我们定义的函数名无所谓, 参数必须按照Windows的规定来.还有, 这个回调函数的调用惯例必须是: stdcall; 我们在上例中是先在接口区声明, 如果不要声明直接实现, 也不能忘了这个 stdcall.

根据以上说明, 做如下修改:SetWindowsHookEx 的参数有变通;并且取消了钩子函数在接口区的声明, 是直接实现的;取消了拦截条件, 现在只要是键盘消息全都拦截.
[delphi] view plaincopy
  1. unit Unit1;   
  2.    
  3. interface   
  4.    
  5. uses   
  6.   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,   
  7.   Dialogs;   
  8.    
  9. type   
  10.   TForm1 = class(TForm)   
  11.     procedure FormCreate(Sender: TObject);   
  12.     procedure FormDestroy(Sender: TObject);   
  13.   end;   
  14.    
  15. var   
  16.   Form1: TForm1;   
  17.    
  18. implementation   
  19.    
  20. {$R *.DFM}   
  21.    
  22. var   
  23.   hook: HHOOK; {定义一个钩子句柄}   
  24.    
  25. {现在这个钩子函数没有在接口区声明, 这里必须指定参数调用方式: stdcall}   
  26. function KeyHook(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;   
  27. begin   
  28.   Beep;   
  29.   Result := CallNextHookEx(hook, nCode, wParam, lParam);   
  30. end;   
  31.    
  32. {设置键盘钩子}   
  33. procedure TForm1.FormCreate(Sender: TObject);   
  34. begin   
  35.   hook := SetWindowsHookEx(WH_KEYBOARD, Addr(KeyHook), HInstance, GetCurrentThreadId);   
  36. end;   
  37.    
  38. {释放键盘钩子}   
  39. procedure TForm1.FormDestroy(Sender: TObject);   
  40. begin   
  41.   UnhookWindowsHookEx(hook);   
  42. end;   
  43.    
  44. end.  
钩子函数为什么非得使用 stdcall 调用机制? 因为钩子函数不是被应用程序调用, 而是被系统调用的. 

建立一个全局的鼠标钩子:
先建立一个 DLL 工程, 自动初始的代码如下(这里可以参考这篇文章中建立dll部分 [delphi之调用外部dll中的函数])
[delphi] view plaincopy
  1. library Project1;   
  2.    
  3. uses   
  4.   SysUtils,   
  5.   Classes;   
  6.    
  7. {$R *.res}   
  8.    
  9. begin   
  10. end.   
  11.    
  12.    
  13. //把工程保存为 MyHook.dpr, 并实现如下:   
  14.    
  15. library MyHook;   
  16.    
  17. uses   
  18.   SysUtils,   
  19.   Windows,  {钩子函数都来自 Windows 单元}   
  20.   Messages, {消息 WM_LBUTTONDOWN 定义在 Messages 单元}   
  21.   Classes;   
  22.    
  23. {$R *.res}   
  24.    
  25. var   
  26.   hook: HHOOK; {钩子变量}   
  27.    
  28. {钩子函数, 鼠标消息太多(譬如鼠标移动), 必须要有选择, 这里选择了鼠标左键按下}   
  29. function MouseHook(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;   
  30. begin   
  31.   if wParam = WM_LBUTTONDOWN then   
  32.   begin   
  33.     MessageBeep(0);   
  34.   end;   
  35.   Result := CallNextHookEx(hook, nCode, wParam, lParam);   
  36. end;   
  37.    
  38. {建立钩子}   
  39. function SetHook: Boolean; stdcall;   
  40. begin   
  41.   hook := SetWindowsHookEx(WH_MOUSE, @MouseHook, HInstance, 0);   
  42.   Result := hook <> 0;   
  43. end;   
  44.    
  45. {释放钩子}   
  46. function DelHook: Boolean; stdcall;   
  47. begin   
  48.   Result := UnhookWindowsHookEx(hook);   
  49. end;   
  50.    
  51. {按 DLL 的要求输出函数}   
  52. exports   
  53.   SetHook name 'SetHook',   
  54.   DelHook name 'DelHook',   
  55.   MouseHook name 'MouseHook';   
  56.    
  57. //SetHook, DelHook, MouseHook; {如果不需要改名, 可以直接这样 exports}   
  58.    
  59. begin   
  60. end.  
注意: SetWindowsHookEx 的第一个参数 WH_MOUSE 说明这是个鼠标钩子; 第四个参数 0 说明是全局的.鼠标钩子回调函数的格式在 这里然后按 Ctrl+F9 编译, 在工程目录下会生成一个和工程同名的文件, 这里是: MyHook.dll.

第二步: 调用
新建工程后, 保存, 并把刚才制作的 MyHook.dll 复制到这个工程目录下;然后添加两个按钮, 实现如下:
[delphi] view plaincopy
  1. unit Unit1;   
  2.    
  3. interface   
  4.    
  5. uses   
  6.   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,   
  7.   Dialogs, StdCtrls;   
  8.    
  9. type   
  10.   TForm1 = class(TForm)   
  11.     Button1: TButton;   
  12.     Button2: TButton;   
  13.     procedure Button1Click(Sender: TObject);   
  14.     procedure Button2Click(Sender: TObject);   
  15.   end;   
  16.    
  17.   {DLL 中的函数声明}   
  18.   function SetHook: Boolean; stdcall;   
  19.   function DelHook: Boolean; stdcall;   
  20.    
  21. var   
  22.   Form1: TForm1;   
  23.    
  24. implementation   
  25.    
  26. {$R *.dfm}   
  27.    
  28. {DLL 中的函数实现, 也就是说明来自那里, 原来叫什么名}   
  29. function SetHook; external 'MyHook.dll' name 'SetHook';   
  30. function DelHook; external 'MyHook.dll' name 'DelHook';   
  31.    
  32. {建立钩子}   
  33. procedure TForm1.Button1Click(Sender: TObject);   
  34. begin   
  35.   SetHook;   
  36. end;   
  37.    
  38. {销毁钩子}   
  39. procedure TForm1.Button2Click(Sender: TObject);   
  40. begin   
  41.   DelHook;   
  42. end;   
  43.    
  44. end.   
测试: 点击第一个按钮后, 钩子就启动了; 这是不管鼠标在哪点一下鼠标左键都会 "呯" 的一下; 点击第二个按钮可以收回钩子.
下面是动态调用的方法, 功能和上面完全一直:
[delphi] view plaincopy
  1. unit Unit1;   
  2.    
  3. interface   
  4.    
  5. uses   
  6.   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,   
  7.   Dialogs, StdCtrls;   
  8.    
  9. type   
  10.   TForm1 = class(TForm)   
  11.     Button1: TButton;   
  12.     Button2: TButton;   
  13.     procedure Button1Click(Sender: TObject);   
  14.     procedure Button2Click(Sender: TObject);   
  15.   end;   
  16.    
  17. var   
  18.   Form1: TForm1;   
  19.    
  20. implementation   
  21.    
  22. {$R *.dfm}   
  23.    
  24. {要先要定义和 DLL 中同样参数和返回值的的函数类型}   
  25. type   
  26.   TDLLFun = function: Boolean; stdcall;   
  27.   {现在需要的 DLL 中的函数的格式都是这样, 定义一个就够了}   
  28.    
  29. var   
  30.  h: HWND;                   {声明一个 DLL 句柄}   
  31.  SetHook, DelHook: TDLLFun; {声明两个 TDLLFun 变量}   
  32.    
  33.    
  34. {载入 DLL 并调用其函数}   
  35. procedure TForm1.Button1Click(Sender: TObject);   
  36. begin   
  37.   h := LoadLibrary('MyHook.dll'); {载入 DLL 并获取句柄}   
  38.   if h<>0 then   
  39.   begin   
  40.     SetHook := GetProcAddress(h, 'SetHook'); {让 SetHook 指向 DLL 中相应的函数}   
  41.     DelHook := GetProcAddress(h, 'DelHook'); {让 DelHook 指向 DLL 中相应的函数}   
  42.   end else ShowMessage('Err');   
  43.    
  44.   SetHook; {执行钩子建立函数, 这里的 SetHook 和它指向的函数是同名的, 也可以不同名}   
  45. end;   
  46.    
  47. {销毁钩子, 并释放 DLL}   
  48. procedure TForm1.Button2Click(Sender: TObject);   
  49. begin   
  50.   DelHook;        {执行钩子释放函数}   
  51.   FreeLibrary(h); {释放 DLL 资源}   
  52. end;   
  53.    
  54. end.   

为什么全局钩子非要在 DLL 中呢?
因为每个 EXE 都是一个独立而封闭的进程; 而 DLL 则是面向系统的公用资源.如果一个钩子不是面向系统的, 恐怕意义不大; 所以在实用中, 钩子是离不开 DLL 的.

钩子链和 CallNextHookEx 的返回值
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 里面, 等下一个话题再研究吧.

[delphi] view plaincopy
  1. //示例代码:   
  2. unit Unit1;   
  3.    
  4. interface   
  5.    
  6. uses   
  7.   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,   
  8.   Dialogs, StdCtrls;   
  9.    
  10. type   
  11.   TForm1 = class(TForm)   
  12.     Button1: TButton;   
  13.     Button2: TButton;   
  14.     Memo1: TMemo;   
  15.     procedure FormDestroy(Sender: TObject);   
  16.     procedure Button1Click(Sender: TObject);   
  17.     procedure Button2Click(Sender: TObject);   
  18.   end;   
  19.    
  20.   {钩子函数声明}   
  21.   function MyKeyHook(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;   
  22.    
  23. var   
  24.   Form1: TForm1;   
  25.    
  26. implementation   
  27.    
  28. {$R *.dfm}   
  29.    
  30. var   
  31.   hook: HHOOK;   
  32.    
  33. function MyKeyHook(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT;   
  34. begin   
  35.   Form1.Memo1.Lines.Add(IntToStr(wParam)); {参数二是键值}   
  36.   Result := 0{分别测试返回 0 或非 0 这两种情况}   
  37. end;   
  38.    
  39. {派出钩子}   
  40. procedure TForm1.Button1Click(Sender: TObject);   
  41. begin   
  42.   hook := SetWindowsHookEx(WH_KEYBOARD, MyKeyHook, HInstance, GetCurrentThreadId);   
  43.   Memo1.Clear;   
  44.   Text := '钩子启动';   
  45. end;   
  46.    
  47. {收回钩子}   
  48. procedure TForm1.Button2Click(Sender: TObject);   
  49. begin   
  50.   UnhookWindowsHookEx(hook);   
  51.   Text := '钩子关闭';   
  52. end;   
  53.    
  54. {如果忘了收回钩子...}   
  55. procedure TForm1.FormDestroy(Sender: TObject);   
  56. begin   
  57.   if hook<>0 then UnhookWindowsHookEx(hook);   
  58. end;   
  59.    
  60. end.  
注意: 在 SetWindowsHookEx 时, 第二参数(函数地址), 没有使用 @、也没有用 Addr, 也行。因为函数名本身就是个地址.
0 0