12.1 使用鍵盤滑鼠監控鉤子

lyshark發表於2023-10-07

本節將介紹如何使用Windows API中的SetWindowsHookExRegisterHotKey函式來實現鍵盤滑鼠的監控。這些函式可以用來設定全域性鉤子,透過對特定熱鍵掛鉤實現監控的效果,兩者的區別在於SetWindowsHookEx函式可以對所有執行緒進行監控,包括其他程式中的執行緒,而RegisterHotKey函式只能對當前執行緒進行監控。

首先我們來實現註冊熱鍵功能,註冊熱鍵可以使用RegisterHotKey()函式,該函式可以將一個熱鍵與當前應用程式或執行緒繫結,使得當使用者按下熱鍵時,系統會自動將該熱鍵的訊息傳送到該應用程式或執行緒中,該函式原型如下;

BOOL RegisterHotKey(
  HWND hWnd,
  int id,
  UINT fsModifiers,
  UINT vk
);

其中,引數的含義如下:

  • hWnd:熱鍵所屬的視窗控制程式碼,通常設定為NULL,表示與當前執行緒繫結
  • id:熱鍵的ID號,用於區分不同的熱鍵
  • fsModifiers:熱鍵的修飾鍵,可以使用組合鍵,例如CtrlAltShift
  • vk:熱鍵的虛擬鍵碼,例如VK_F1表示F1VK_LEFT表示左箭頭鍵等

函式需要傳入一個視窗控制程式碼、熱鍵ID、熱鍵組合鍵等引數來設定熱鍵。當熱鍵被按下時,系統會自動將一個WM_HOTKEY訊息傳送給註冊了該熱鍵的視窗,應用程式需要過載該視窗的訊息處理函式來響應該事件,從而實現相應的響應操作。該函式會返回一個BOOL型別的值,表示熱鍵設定是否成功。

當熱鍵被註冊後則就需要接收熱鍵訊息,通常可以使用GetMessage函式,該函式用於從訊息佇列中獲取一個訊息並將其儲存在一個結構體中,通常用於在一個迴圈中不斷地獲取訊息,從而實現對Windows訊息的處理。

該函式的原型定義如下所示;

BOOL GetMessage(
  LPMSG lpMsg,
  HWND hWnd,
  UINT wMsgFilterMin,
  UINT wMsgFilterMax
);

其中,引數的含義如下:

  • lpMsg:指向MSG結構體的指標,用於儲存獲取到的訊息
  • hWnd:訊息接收者的視窗控制程式碼,通常設定為NULL,表示接收所有視窗的訊息
  • wMsgFilterMin:指定獲取訊息的最小訊息值,通常設定為0
  • wMsgFilterMax:指定獲取訊息的最大訊息值,通常設定為0

GetMessage函式需要傳入一個指向MSG結構體的指標,該結構體包含了訊息的各種資訊,例如訊息的型別、傳送者、接收者、時間戳等等。讀者只需要透過判斷函式內的WM_HOTKEY訊息,並監控是否為我們所需要的即可,如下程式碼是一段註冊熱鍵的實現,分別註冊了Ctrl+F1 Ctrl+F2 Ctrl+F3三個熱鍵組;

#include <windows.h>
#include <iostream>

using namespace std;

int main(int argc, char* argv[])
{
  // 分別註冊三個熱鍵 Ctrl+F1 , Ctrl+F2 , Ctrl+F3
  if (0 == RegisterHotKey(NULL, 1, MOD_CONTROL, VK_F1))
  {
    cout << GetLastError() << endl;
  }
  if (0 == RegisterHotKey(NULL, 2, MOD_CONTROL, VK_F2))
  {
    cout << GetLastError() << endl;
  }
  if (0 == RegisterHotKey(NULL, 3, MOD_CONTROL, VK_F3))
  {
    cout << GetLastError() << endl;
  }

  // 訊息迴圈
  MSG msg = { 0 };

  // 從訊息迴圈內讀出按鍵碼
  while (GetMessage(&msg, NULL, 0, 0))
  {
    switch (msg.message)
    {
    case WM_HOTKEY:
    {
      if (1 == msg.wParam)
      {
        std::cout << "CTRL + F1" << std::endl;
      }

      else if (2 == msg.wParam)
      {
        std::cout << "CTRL + F2" << std::endl;
      }

      else if (3 == msg.wParam)
      {
        std::cout << "CTRL + F3" << std::endl;
      }
      break;
    }
    default:
      break;
    }
  }
  return 0;
}

讀者可自行編譯上述程式碼片段,並執行,分別按下Ctrl+F1 Ctrl+F2 Ctrl+F3即可看到輸出效果圖;

當然上述方法是區域性的,讀者只能在當前程式內使用,如果離開了程式窗體則這類熱鍵將會失效,此時我們就需要使用SetWindowsHookEx函式註冊全域性鉤子,該函式可以在系統中安裝鉤子,以便監視或攔截特定的事件或訊息。

以下是SetWindowsHookEx的函式原型:

HHOOK SetWindowsHookEx(
  int       idHook,
  HOOKPROC  lpfn,
  HINSTANCE hMod,
  DWORD     dwThreadId
);

引數說明:

  • idHook:鉤子型別,可以是WH_KEYBOARD(鍵盤鉤子)或WH_MOUSE(滑鼠鉤子)等
  • lpfn:回撥函式,當特定事件或訊息發生時,作業系統會呼叫此函式。該函式的返回值由鉤子型別和引數決定
  • hMod:包含lpfnDLL控制程式碼。如果lpfn引數在當前程式內,則該引數可以為NULL
  • dwThreadId:執行緒識別符號,指定與鉤子相關聯的執行緒。如果dwThreadId引數為0,則鉤子將應用於所有執行緒

函式會返回一個型別為HHOOK的控制程式碼,該控制程式碼可以在解除安裝鉤子時使用,讀者需要注意由於全域性鉤子會影響系統效能,因此在使用SetWindowsHookEx函式時應謹慎,並在使用結束後及時的透過UnhookWindowsHookEx釋放鉤子控制程式碼。

如下所示程式碼則是一個鍵盤鉤子監控案例,在該案例中我們透過SetWindowsHookEx註冊一個全域性鉤子,並設定回撥函式LowLevelKeyboardProc透過使用PeekMessageA監控鍵盤事件,當有鍵盤事件產生時則自動路由到LowLevelKeyboardProc函式內,此時即可得到按鍵的型別以及按下鍵位,如下所示;

#include <windows.h>
#include <iostream>
#include <conio.h>

using namespace std;

// 鉤子控制程式碼
HHOOK keyboardHook = 0;

// 鍵盤鉤子
LRESULT CALLBACK LowLevelKeyboardProc(_In_ int nCode, _In_ WPARAM wParam, _In_ LPARAM lParam)
{
    KBDLLHOOKSTRUCT* ks = (KBDLLHOOKSTRUCT*)lParam;
    if (ks->flags == 128 || ks->flags == 129)
    {
        // 監控按鍵狀態
        if (nCode >= 0)
        {
            switch (wParam)
            {
            case WM_KEYDOWN:
                cout << "普通按鍵抬起" << endl;
                break;
            case WM_KEYUP:
                cout << "普通按鍵按下" << endl;
                break;

            case WM_SYSKEYDOWN:
                cout << "系統按鍵抬起" << endl;
                break;
            case WM_SYSKEYUP:
                cout << "系統按鍵按下" << endl;
                break;
            }
        }

        // 監控鍵盤,並判斷鍵
        switch (ks->vkCode)
        {
        case 0x41:
            cout << "檢測到按鍵:" << "A" << endl;
            break;
        case 0x0D:
            cout << "檢測到按鍵:" << "Enter" << endl;
            break;
        case 0xA0: case 0xA1:
            cout << "檢測到按鍵:" << "Shift" << endl;
            break;
        case 0x08:
            cout << "檢測到按鍵:" << "Backspace" << endl;
            break;
        case 0x20:
            cout << "檢測到按鍵:" << "Space" << endl;
            break;
        }

        // 直接返回1可以使按鍵失效
        //return 1;
    }
    return CallNextHookEx(NULL, nCode, wParam, lParam);
}

int main(int argc, char* argv[])
{
    // 安裝鉤子WH_KEYBOARD_LL為鍵盤鉤子
    keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandleA(NULL), NULL);
    if (keyboardHook == 0)
    {
        cout << "掛鉤鍵盤失敗" << endl; return -1;
    }

    MSG msg;
    while (1)
    {
        if (PeekMessageA(&msg, NULL, NULL, NULL, PM_REMOVE))
        {
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }
        else
            Sleep(0);
    }
    UnhookWindowsHookEx(keyboardHook);
    return 0;
}

編譯並執行上述程式碼,讀者可自行按下鍵盤鍵位,則可看到如下圖所示的輸出;

滑鼠鉤子的掛鉤與鍵盤基本一致,只是在呼叫SetWindowsHookEx傳遞引數時設定了WH_MOUSE_LL滑鼠事件,當有滑鼠訊息時則透過MouseProc滑鼠回撥函式執行,

#include <windows.h>
#include <iostream>
#include <conio.h>

using namespace std;

// 鉤子控制程式碼
HHOOK keyboardHook = 0;

// 滑鼠鉤子
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
  LPMSLLHOOKSTRUCT p = (LPMSLLHOOKSTRUCT)lParam;
  POINT   pt = p->pt;
  DWORD   mouseData = p->time;

  const char* info = NULL;
  char text[60], pData[50], mData[50];

  PAINTSTRUCT ps;
  HDC hdc;

  if (nCode >= 0)
  {
    if (wParam == WM_MOUSEMOVE)
    {
      info = "滑鼠 [移動]";
    }
    else if (wParam == WM_LBUTTONDOWN)
    {
      info = "滑鼠 [左鍵] 按下";
    }
    else if (wParam == WM_LBUTTONUP)
    {
      info = "滑鼠 [左鍵] 抬起";
    }
    else if (wParam == WM_LBUTTONDBLCLK)
    {
      info = "滑鼠 [左鍵] 雙擊";
    }
    else if (wParam == WM_RBUTTONDOWN)
    {
      info = "滑鼠 [右鍵] 按下";
    }
    else if (wParam == WM_RBUTTONUP)
    {
      info = "滑鼠 [右鍵] 抬起";
    }
    else if (wParam == WM_RBUTTONDBLCLK)
    {
      info = "滑鼠 [右鍵] 雙擊";
    }
    else if (wParam == WM_MBUTTONDOWN)
    {
      info = "滑鼠 [滾輪] 按下";
    }
    else if (wParam == WM_MBUTTONUP)
    {
      info = "滑鼠 [滾輪] 抬起";
    }
    else if (wParam == WM_MBUTTONDBLCLK)
    {
      info = "滑鼠 [滾輪] 雙擊";
    }
    else if (wParam == WM_MOUSEWHEEL)
    {
      info = "滑鼠 [滾輪] 滾動";
    }

    ZeroMemory(text, sizeof(text));
    ZeroMemory(pData, sizeof(pData));
    ZeroMemory(mData, sizeof(mData));

    std::cout << "滑鼠狀態: " << info << std::endl;
    std::cout << "X: " << pt.x << " Y: " << pt.y << std::endl;
    std::cout << "附加資料: " << mouseData << std::endl;
  }

  return CallNextHookEx(NULL, nCode, wParam, lParam);
}

int main(int argc, char* argv[])
{
  // 安裝鉤子
  keyboardHook = SetWindowsHookEx(WH_MOUSE_LL,MouseProc,GetModuleHandleA(NULL),NULL);
  if (keyboardHook == 0)
  {
    cout << "掛鉤滑鼠失敗" << endl; return -1;
  }

  MSG msg;
  while (1)
  {
    // 如果訊息佇列中有訊息
    if (PeekMessageA(&msg,NULL,NULL,NULL,PM_REMOVE))
    {
      // 把按鍵訊息傳遞給字元訊息
      TranslateMessage(&msg);

      // 將訊息分派給視窗程式
      DispatchMessageW(&msg);
    }
    else
      Sleep(0);
  }
  UnhookWindowsHookEx(keyboardHook);

  return 0;
}

讀者可自行編譯並執行上述程式碼片段,當掛鉤後我們就可以看到滑鼠的移動位置以及滑鼠擊鍵情況,如下圖所示;

本文作者: 王瑞
本文連結: https://www.lyshark.com/post/bdb59f93.html
版權宣告: 本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協議。轉載請註明出處!

相關文章