Windows中系统范围热键的实现方法

  1. 云栖社区>
  2. 博客>
  3. 正文

Windows中系统范围热键的实现方法

余二五 2017-11-09 01:37:00 浏览637
展开阅读全文

    在Windows环境下运行的程序,大多数都支持热键,热键一般被人们称为快捷键,例如:大多数Windows程序都支持“复制”和“粘贴”操作,它们的快捷键一般是:“Ctrl+C”和“Ctrl+V”;在“记事本”中通过按下“Ctrl+C”或“Ctrl+V”键,可以将一段选定的文本复制到剪贴板或者将剪贴板中的文本粘贴到“记事本”中。但是,以上所说的这些必须是在该程序处于前台时,才会起作用,也就是说这些热键仅仅是进程范围的,当该程序不在前台时,这些热键对该程序是没有作用的。然而,有些时候希望在任何情况下这些热键都会有效,即无论应用程序处于前台还是后台,或者是缩小到任务栏和系统状态栏中,只要按下热键,那么在程序中所编写的动作就会被执行。这时,仅仅依靠进程范围的热键是无法达到目的的,必须要使用系统范围的热键才行。一般实现系统范围的热键有两种方法:一种是使用现有的Windows API函数,通过注册系统范围的热键来达到目的;另一种是使用按键Hook(挂钩)技术,将一个特定按键Hook动态链接库加入到系统Hook链中,同样可以达到目的。

    下面将详细讨论这两种方法的实现过程及应用范围。

方法一:使用API函数直接注册系统范围的热键。

    在Windows中提供了一组定义系统级热键的API函数,这些API函数主要有以下四个:
1、RegisterHotKey:该函数注册一个系统范围的热键;
2、UnreglsterHotKey:该函数释放先前注册的热键;
3、GlobalAddAtom:该函数把字符串加到系统原子表中并返回一个唯一标识该字符串的原子;
4、GlobalDeleteAtom:该函数把一个全局原子的引用计数减一。如果原子的引用计数变为零,则把与该原子相关的字符串从系统原子表中删除。
在Windows中所有系统范围的热键都有一个唯一的原子,或者称为标识符,系统使用这些原子来识别这些热键。所有这些原子被存储在一张系统原子表中,当需要注册一个系统范围的热键的时候,必须给该热键分配一个的原子,否则热键将无法注册。所以,注册一个新的系统范围的热键时,通过使用GlobalAddAtom API函数来得到一个原子。
Delphi函数原型:


  1. function GlobalAddAtom(lpString: PAnsiChar): ATOM; stdcall;  

参数:
lpString:指向一个以NULL结尾的字符串的指针。该字符串最大允许255个字节,该字符串大小写不敏感。
    返回值:如果函数调用成功,返回值是标识该字符串的一个原子;否则返回0。
    使用GlobalAddAtom函数可以给一个字符串分配对应的原子,并将该字符串和分配的原子存储在系统原子表中。当指定的字符串已经存在于系统原子表中时,则该函数返回已存在的字符串所对应的原子值,且把原子的引用计数加一。所谓原子的引用计数,它是一个数值,它给出已为一个特定字符串所调用的GlobalAddAtom的次数,表示当前系统中有多少线程引用了该原子。与原子关联的字符串只有在其引用计数为零时才会从系统原子表中删除。这一点与Windows中的共享DLL很相似,共享DLL的引用计数为零时,操作系统会自动将它们移出内存,所以当程序结束时,必须将先前分配的原子删除,这可以通过GlobalDeleteAtom API函数完成,GlobalDeleteAtom API函数用以将原子的引用计数减一。
Delphi函数原型:


  1. function GlobalDeleteAtom(nAtom: ATOM): ATOM; stdcall;  

参数:
nAtom:标识要删除的原子。
返回值:若函数调用成功,返回0。如果函数失败,返回nAtom参数。
当原子被成功分配后,便可以使用RegisterHotKey API函数来注册系统范围的热键了。
Delphi函数原形:


  1. RegisterHotKey(hWnd: HWND; id: Integer; fsModifiers, vk: UINT): BOOL; stdcall;  

参数:
hWnd:表示接收热键产生WM_HOTKEY消息的窗口句柄。
在Delphi中定义热键的程序通过拦截WM_­HOTKEY消息,便可以在热键被按下时作出响应。
    id:定义热键的标识符,即原子。
调用该函数线程中的其它热键不能使用同样的标识符。当一个共享的动态链接库使用该函数时,为了避免与其他动态链接库定义的热键冲突,所以一个DLL必须使用GlobalAddAtom API函数获得热键的原子(标识符),在一般情况下,最好都使用GlobalAddAtom API函数来获得热键的原子(标识符)。
fsModifoers:定义为了产生WM_HOTKEY消息而必须与由vk参数定义的键同时按下的键。
一般情况下,热键不会使用单一的按键,那么使用该参数就可以指定同时按下的键。该参数可以是如下值的组合:
MOD_ALT:按下的可以是任一Alt键;
MOD_CONTROL:按下的可以是任一Ctrl键;
MOD_SHIFT:按下的可以是任一Shift键;
MOD_WIN:按下的可以是任一Win键。
这些键可以用Microsoft Windows日志记录下来。
vk:定义热键的虚拟键码。
    在Delphi中,所有的可见字符都使用它们对应的ASCII码,对于字母键来说使用的是它们的大写状态。
返回值:若函数调用成功,返回一个非0值。若函数调用失败,则返回0。
当某键被按下时,系统在所有的热键中寻找匹配者。一旦找到一个匹配的热键,系统将把WM_HOTKEY消息传递给登记了该热键的线程的消息队列。该消息被传送到队列头部,因此它将在下一轮消息循环中被移去,所以热键的响应总是非常及时的。如果为一个热键定义的击键已被其它热键所定义了,则RegisterHotKey API函数调用失败。如果hWnd参数标识的窗口已用与id参数定义的相同的原子登记了一个热键,则参数fsModifiers和vk的新值将替代这些参数先前定义的值。不同的线程定义不同热键时,可以使用相同的原子,因为它们直接与hWnd参数所标识的窗口相关联,所以它们并不会混淆。
当注册了一个系统范围的热键成功后,在Delphi中可以通过拦截WM_HOTKEY消息,然后,对消息的参数分析并作出对热键的响应。
在需要释放先前注册的系统范围的热键时,使用UnregisterHotKey API函数。
Delphi函数原型:


  1. function UnregisterHotKey(hWnd: HWND; id: Integer): BOOL; stdcall;  

参数:
hWnd:与被释放的热键相关的窗口句柄;
id:定义被释放的热键的原子(标识符)。
返回值:若函数调用成功,返回值不为0。若函数调用失败,返回0。
    WM_HOTKEY消息的wParam参数指明与当前所按热键所关联的原子;lParam参数的低位部分指出是否按下了Ctrl、Alt、Shift或Win键,它可以是RegisterHotKey函数fsModifoers参数所包含的任一组合;lParam参数的高位部分指出虚拟键码,它可以是RegisterHotKey函数vk参数所包含的值。
举例如下:
在Delphi类声明的Public部分定义:
 


  1. Atom_1, Atom_2: Atom;       // 原子变量  
  2. procedure ProcMsg(var Msg: TMessage); message WM_HOTKEY; 

捕获消息后的处理方法:
 


  1. procedure TForm1.ProcMsg(var Msg: TMessage);  
  2. begin 
  3.  with Msg do  
  4.  begin 
  5.     if (LoWord(LParam) = MOD_CONTROL or MOD_ALT) and 
  6.       (HiWord(LParam) = VK_F7) then    // 用户按下的是Ctrl+Alt+F7  
  7.       WinExec('notepad.exe', SW_SHOW); // 打开“记事本”程序  
  8.     if (LoWord(LParam) = MOD_SHIFT) and 
  9.       (HiWord(LParam) = VK_F8) then    // 用户按下的是Shift+F8  
  10.       WinExec('calc.exe', SW_SHOW);    // 打开“计算器”程序  
  11.  end;  
  12. end

对于WM_HOTKEY消息的判定还可以直接判定wParam参数是否等于相应的原子,这样,当需要改变热键时就无需改动程序,故以上程序可改写成如下形式:
 


  1. with Msg do  
  2. begin 
  3.    if wParam = Atom_1 then 
  4.      WinExec('notepad.exe', SW_SHOW);     // 打开“记事本”程序  
  5.    if wParam = Atom_2 then 
  6.      WinExec('calc.exe', SW_SHOW);        // 打开“计算器”程序  
  7. end

分配原子:
 


  1. Atom_1 := GlobalAddAtom('Example: Register hotkey Ctrl+Alt+F7.');  
  2. Atom_2 := GlobalAddAtom('Example: Register hotkey Shift+F8.');  

这里使用了一些说明性的字符串,在实际应用中,一般使用与线程窗口关联的窗口类标识符。
删除原子:
 


  1. GlobalDeleteAtom(Atom_1);  
  2. GlobalDeleteAtom(Atom_2);  

注册系统范围的热键:
 


  1. RegisterHotKey(Handle, GolAtom, MOD_CONTROL or MOD_ALT, VK_F7);  
  2. //Hotkey: Ctrl+Alt+F7  
  3. RegisterHotKey(Handle, GolAtom, MOD_SHIFT, VK_F8);  
  4. // Hotkey: Shift+F8  

释放注册的系统范围的热键:
 


  1. UnregisterHotKey(Handle, Atom_1);  
  2. UnregisterHotKey(Handle, Atom_2);  

上述代码可以在任何情况下按下“Ctrl+Alt+F7”键组合打开“记事本”程序;按“Shift+F8”键组合打开“计算器”程序。

方法二:使用Windows按键Hook(挂钩)技术,捕捉任何按键消息。

Hook是Windows中消息处理机制的一个要点,通过安装各种Hook,应用程序能够设置相应的子例程来监视系统里的消息传递以及在这些消息到达目的地之前截获它们并根据用户要求做出相应处理。Hook的种类很多其作用也不同,如键盘Hook可以截获键盘消息,鼠标Hook可以截获鼠标消息,外壳Hook可以截获启动和关闭应用程序的消息,日志Hook可以监视和记录输入事件。Hook分为线程专用Hook和全局Hook,线程专用Hook只监视指定的线程,要监视系统中的所有线程,必须用到全局Hook。对于全局Hook,Hook函数必须包含在独立的动态链接库(DLL)中,这样才能被各种相关联的应用程序调用。
由上可知,Windows中系统范围的热键完全可以通过全局键盘Hook进行实现。
Windows提供API函数SetwindowsHookEx来建立一个Hook,通过这个函数可以将一个程序添加到Hook链中监视Windows消息。
Delphi函数原型:
 


  1. function SetWindowsHookEx(idHook: Integer; lpfn: TFNHookProc; hmod: HINST;dwThreadId: DWORD): HHOOK; stdcall;  

参数:
idHook:指示欲被安装的挂钩处理过程之类型。
此参数可以是以下值之一:
WH_CALLWNDPROC:安装一个挂钩处理过程,在系统将消息发送至目标窗口处理过程之前,对消息进行监视;
WH_CALLWNDPROCRET:安装一个挂钩处理过程,它对已被目标窗口处理过程处理过了的消息进行监视;
WH_CBT:安装一个挂钩处理过程,接受对CBT应用程序的有用的消息;
WH_DEBUG:安装一个挂钩处理过程,以便对其他挂钩处理过程进行调试;
WH_GETMESSAGE:安装一个挂钩处理过程,对寄送至消息队列的消息进行监视;
WH_JOURNALPLAYBACK:安装一个挂钩处理过程,对此前由WH_JOURNALRECORD挂钩处理过程记录的消息进行寄送。
WH_JOURNALRECORD:安装一个挂钩处理过程,对寄送至系统消息队列的输入消息进行记录;
WH_KEYBOARD:安装一个挂钩处理过程,对击键消息进行监视;
WH_MOUSE:安装一个挂钩处理过程,对鼠标消息进行监视;
WH_MSGFILTER:安装一个挂钩处理过程,以监视对话框、消息框、菜单条、或滚动条中的输入事件引发的消息;
WH_SHELL:安装一个挂钩处理过程,以接受对Shell应用程序有用的通知;
WH_SYSMSGFILTER:安装一个挂钩处理过程,以监视对话框、消息框、菜单条、或滚动条中的输入事件引发的消息,这个挂钩处理过程对系统中所有应用程序的这类消息都进行监视。
作为系统范围的热键应用,则使用WH_KEYBOARD作为idHook的参数。
lpfn:指向相应的挂钩回调函数。
若参数dwThreadId为0或者指示了一个其他进程创建的线程之标示符,则参数lpfn必须指向一个动态链接库中的挂钩回调函数。否则,参数lpfn可以指向一个与当前进程相关的代码中定义的挂钩回调函数。
hmod:指示了一个动态链接库的句柄,该动态链接库包含了参数lpfn所指向的挂钩回调函数。
若参数dwThreadId指示的线程由当前进程创建,并且相应的挂钩回调函数定义于当前进程相关的代码中,则hMod必须被置为NULL。
dwThreadId:指示了一个线程标示符,挂钩处理过程与该线程相关。
若此参数值为0,则该挂钩处理过程与所有现存的线程相关。
返回值:若此函数执行成功,则返回值就是该挂钩处理过程的句柄;否则,则返回值为NULL。
在使用Hook方法实现Windows系统范围的热键时,回调函数使用KeyboardProc,此函数是应用程序或库中定义的回调函数,当应用程序调用函数GetMessage或PeekMessage并且当前恰好有一个键盘消息(WM_KEYUP或WM_KEYDOWN)将要被处理时,系统就调用此挂钩回调函数。类型HookProc定义了指向此类回调函数的指针,KeyboardProc是应用程序或DLL中定义的相应回调函数的位置标示符。
Delphi函数原型:
 


  1. function KeyboardProc(code: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;  

参数:
code:指定一个代码,该代码被挂钩处理过程用于决定如何处理此消息。
此参数可以为以下值之一:
HC_ACTION:参数WParam和LParam包含—个击键消息的信息。
HC_NOREMOVE:参数WParam和LParam包含—个击键消息的信息,并且此击键消息尚未从消息队列中被删除(—个应用程序调用函数PeekMessage并设置了PM_NOREMOVE标志)。
若code的值小于0,则此挂钩处理过程必须不加处理地将此消息传送给函数CallNextHookEx,并返回其返回值。
WParam:指示产生此击键消息的虚拟键代码。
LParam:指示重复次数,扫描吗,扩展键标志,上下文代码,此前的键状态标志和变化状态标志。
此参数可以是以下值的组合值:
0—15位:指示重复次数,此值记录了由于用户继续按键引发的击键重复次数。
16—23位:指示扫描码,此值依赖于键盘生产厂家。
第24位:指示此键是否为扩展键,比如功能键或数字小键盘上的键,当该键为扩展键时,其值1,否则为0。
25—28位:保留未用。
第29位:上下文代码,若Alt键被按下,则其值为1,否则为0。
第30位:指示此前的键状态,若在此消息被发送之前该键是按下的,其值为1。若此前该键未被按下,则其值为0。
第31位:指示变化状态,若此键正在被按下,则其值为0。
返回值:若参数nCode的值小于0,则此挂钩处理过程必须返回函数CallNextHookEx的返回值;若参数nCode的值大于或等于0,并且此挂钩处理过程并未对此消息进行处理,则调用函数CallNextHookEx并返回。否则,其他安装了挂钩WH_KEYBOARD的应用程序将不能收到挂钩通知并可能由此导致错误的行为,若此挂钩处理过程对此消息进行了处理,它应返回非0值以避免系统将此消息传送给挂钩链上的其他处理过程或目标窗口的处理过程。
在不需要监视键盘消息时需要调用UnHookWindowsHookEx来解除对消息的监视。
Delphi函数原型:
 


  1. function UnhookWindowsHookEx(hhk: HHOOK): BOOL; stdcall;  

参数:
hhk:将要被删除的挂钩的句柄。
为了保证系统Hook链的连续,需要通过调用CallNextHookEx来维护Hook链的完整。
Delphi函数原型:
 


  1. function CallNextHookEx(hhk: HHOOK; nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;  

该函数的参数应与调用SetWindowsHookEx函数和KeyboardProc函数时的参数保持一致。
举例如下:
DLL程序
Project文件:
 


  1. Library HKKProc;  
  2. uses  
  3.   UnitKHK in 'UnitKHK.pas';  
  4. exports  
  5.   EnableKeyHook,  
  6.   DisableKeyHook;  
  7. begin 
  8.   hKeyHookProc := 0;  
  9.   // 以下这两列, 旨在确定 DLL UnLoad 时, 确定将 Key Hook 解除  
  10.   KeyProcSaveExit := ExitProc;  
  11.   ExitProc := @KeyHookExit;  
  12. end.  

Unit文件:
interface声明部分:
 


  1. var  
  2.   hKeyHookProc: HHook;  
  3.   KeyProcSaveExit: Pointer;  
  4. function KeyHookHandler(iCode: Integer; wParam: wParam; lParam: lParam): LResult; stdcall; export;  
  5. function EnableKeyHook: BOOL; export;  
  6. function DisableKeyHook: BOOL; export;  
  7. procedure KeyHookExit; far;  

implementation部分:
 


  1. function KeyboardHookHandler(iCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall; export;  
  2. const  
  3.   _KeyPressMask = $80000000;  // 取第lParam参数的第31位  
  4. begin 
  5.   Result := 0;  
  6.   if iCode < 0 Then 
  7.     Result := CallNextHookEx(hNextHookProc, iCode, wParam, lParam);  
  8.   else 
  9.     if ((lParam and _KeyPressMask) = 0) and (GetKeyState(VK_CONTROL) < 0)  
  10. and (wParam = Ord('Z')) then   // 捕捉按键Ctrl+Z  
  11.   begin 
  12.       WinExec('Notepad.exe', sw_Normal);  // 记事本  
  13.       Result := 1;  
  14.   end;  
  15. end;  
  16.  
  17. function EnableKeyHook: BOOL; export;  
  18. begin 
  19.   Result := False;  
  20.   if hKeyHookProc <> 0 then Exit;  
  21.   hKeyHookProc := SetWindowsHookEx(WH_KEYBOARD, KeyHookHandler,HInstance, 0);  
  22.   Result := hKeyHookProc <> 0;  
  23. end;  
  24.  
  25. function DisableKeyHook: BOOL; export;  
  26. begin 
  27.   if hKeyHookProc <> 0 then 
  28.   begin 
  29.     UnHookWindowsHookEx(hKeyHookProc);  
  30.     hKeyHookProc := 0;  
  31.   end;  
  32.   Result := hKeyHookProc = 0;  
  33. end;  
  34.  
  35. procedure KeyHookExit;  
  36. begin 
  37.   if hKeyHookProc <> 0 then 
  38.     DisableKeyHook;  
  39.   ExitProc := KeyProcSaveExit;  
  40. end;  

结语
方法一和方法二采用了两种截然不同的思路,其中方法一简单、方便,但同时功能单一,例如无法实现计数、回放、复杂组合按键的区分等功能;方法二虽然复杂,需要额外的DLL编写,但功能强大,可以实现任何与按键相关的功能。故而,一般情况下,若只是做一个系统范围的热键功能,多采用方法一。










本文转自 BlackAlpha 51CTO博客,原文链接:http://blog.51cto.com/mengliao/465341,如需转载请自行联系原作者

网友评论

登录后评论
0/500
评论
余二五
+ 关注