這是一個以 C# 撰寫的 Windows Forms 範例程式,示範如何設置鍵盤掛鉤,以攔截特定的按鍵。

除了示範鍵盤掛鉤的設置與解除,同時也包含兩個取得鍵盤狀態的類別:KeyboardInfo 與 KeyStateInfo。這兩個類別取自文章 Obtaining Key State info in .NET,它們等於是傳統 WinAPI 的 GetKeyState 函式的實作,但使用起來方便許多。我針對 ALT 鍵無法正確判斷的 bug 作了修正。以下是範例程式的完整原始碼:

    1 using System;

    2 using System.ComponentModel;

    3 using System.Windows.Forms;

    4 using System.Diagnostics;

    5 using System.Runtime.InteropServices;

    6 

    7 namespace KeyboardHook

    8 {

    9     public partial class Form1 : Form

   10     {

   11         public Form1()

   12         {

   13             InitializeComponent();

   14         }

   15 

   16         const int WH_KEYBOARD = 2;

   17 

   18         public delegate int HookProc(int nCode, IntPtr wParam, IntPtr lParam);

   19 

   20         private static int m_HookHandle = 0;    // Hook handle

   21         private HookProc m_KbdHookProc;            // 鍵盤掛鉤函式指標

   22 

   23         // 設置掛鉤.

   24         [DllImport("user32.dll", CharSet = CharSet.Auto,

   25         CallingConvention = CallingConvention.StdCall)]

   26         public static extern int SetWindowsHookEx(int idHook, HookProc lpfn,

   27         IntPtr hInstance, int threadId);

   28 

   29         // 將之前設置的掛鉤移除。記得在應用程式結束前呼叫此函式.

   30         [DllImport("user32.dll", CharSet = CharSet.Auto,

   31         CallingConvention = CallingConvention.StdCall)]

   32         public static extern bool UnhookWindowsHookEx(int idHook);

   33 

   34         // 呼叫下一個掛鉤處理常式(若不這麼做,會令其他掛鉤處理常式失效).

   35         [DllImport("user32.dll", CharSet = CharSet.Auto,

   36         CallingConvention = CallingConvention.StdCall)]

   37         public static extern int CallNextHookEx(int idHook, int nCode,

   38         IntPtr wParam, IntPtr lParam);

   39 

   40         [DllImport("kernel32.dll")]

   41         static extern int GetCurrentThreadId();

   42 

   43         private void button1_Click(object sender, EventArgs e)

   44         {

   45             if (m_HookHandle == 0)

   46             {

   47                 m_KbdHookProc = new HookProc(Form1.KeyboardHookProc);

   48 

   49                 m_HookHandle = SetWindowsHookEx(WH_KEYBOARD, m_KbdHookProc, IntPtr.Zero, GetCurrentThreadId());

   50 

   51                 if (m_HookHandle == 0)

   52                 {

   53                     MessageBox.Show("呼叫 SetWindowsHookEx 失敗!");

   54                     return;

   55                 }

   56                 button1.Text = "解除鍵盤掛鉤";

   57             }

   58             else

   59             {

   60                 bool ret = UnhookWindowsHookEx(m_HookHandle);

   61                 if (ret == false)

   62                 {

   63                     MessageBox.Show("呼叫 UnhookWindowsHookEx 失敗!");

   64                     return;

   65                 }

   66                 m_HookHandle = 0;

   67                 button1.Text = "設置鍵盤掛鉤";

   68             }

   69         }

   70 

   71         public static int KeyboardHookProc(int nCode, IntPtr wParam, IntPtr lParam)

   72         {

   73             // 當按鍵按下及鬆開時都會觸發此函式,這裡只處理鍵盤按下的情形。

   74             bool isPressed = (lParam.ToInt32() & 0x80000000) == 0;   

   75 

   76             if (nCode < 0 || !isPressed)

   77             {

   78                 return CallNextHookEx(m_HookHandle, nCode, wParam, lParam);

   79             }

   80 

   81             // 取得欲攔截之按鍵狀態

   82             KeyStateInfo ctrlKey = KeyboardInfo.GetKeyState(Keys.ControlKey);

   83             KeyStateInfo altKey = KeyboardInfo.GetKeyState(Keys.Alt);

   84             KeyStateInfo shiftKey = KeyboardInfo.GetKeyState(Keys.ShiftKey);

   85             KeyStateInfo f8Key = KeyboardInfo.GetKeyState(Keys.F8);

   86 

   87             if (ctrlKey.IsPressed)

   88             {

   89                 System.Diagnostics.Debug.WriteLine("Ctrl Pressed!");

   90             }

   91             if (altKey.IsPressed)

   92             {

   93                 System.Diagnostics.Debug.WriteLine("Alt Pressed!");

   94             }

   95             if (shiftKey.IsPressed)

   96             {

   97                 System.Diagnostics.Debug.WriteLine("Shift Pressed!");

   98             }

   99             if (f8Key.IsPressed)

  100             {

  101                 System.Diagnostics.Debug.WriteLine("F8 Pressed!");

  102             }

  103 

  104             return CallNextHookEx(m_HookHandle, nCode, wParam, lParam);

  105         }

  106     }

  107 

  108     public class KeyboardInfo

  109     {

  110         private KeyboardInfo() { }

  111 

  112         [DllImport("user32")]

  113         private static extern short GetKeyState(int vKey);

  114 

  115         public static KeyStateInfo GetKeyState(Keys key)

  116         {

  117             int vkey = (int)key;

  118 

  119             if (key == Keys.Alt)

  120             {

  121                 vkey = 0x12;    // VK_ALT

  122             }

  123 

  124             short keyState = GetKeyState(vkey);

  125             int low = Low(keyState);

  126             int high = High(keyState);

  127             bool toggled = (low == 1);

  128             bool pressed = (high == 1);

  129 

  130             return new KeyStateInfo(key, pressed, toggled);

  131         }

  132 

  133         private static int High(int keyState)

  134         {

  135             if (keyState > 0)

  136             {

  137                 return keyState >> 0x10;

  138             }

  139             else

  140             {

  141                 return (keyState >> 0x10) & 0x1;

  142             }

  143 

  144         }

  145 

  146         private static int Low(int keyState)

  147         {

  148             return keyState & 0xffff;

  149         }

  150     }

  151 

  152 

  153     public struct KeyStateInfo

  154     {

  155         Keys m_Key;

  156         bool m_IsPressed;

  157         bool m_IsToggled;

  158 

  159         public KeyStateInfo(Keys key, bool ispressed, bool istoggled)

  160         {

  161             m_Key = key;

  162             m_IsPressed = ispressed;

  163             m_IsToggled = istoggled;

  164         }

  165 

  166         public static KeyStateInfo Default

  167         {

  168             get

  169             {

  170                 return new KeyStateInfo(Keys.None, false, false);

  171             }

  172         }

  173 

  174         public Keys Key

  175         {

  176             get { return m_Key; }

  177         }

  178 

  179         public bool IsPressed

  180         {

  181             get { return m_IsPressed; }

  182         }

  183 

  184         public bool IsToggled

  185         {

  186             get { return m_IsToggled; }

  187         }

  188     }

  189 }

NOTE:

  • 此範例的鍵盤掛鉤攔截四個按鍵:Ctrl、Alt、Shift、和 F8。執行時,可在 Visual Studio 的 Output 視窗觀察輸出的除錯訊息。
  • 此範例的鍵盤掛夠只有當此應用程式為作用中視窗時才有作用。
  • 在第 49 行呼叫 SetWindowHookEx 以設置鍵盤掛鉤時,最後一個傳入參數也可以用 AppDomain.GetCurrentThreadId(),可是此方法在 .NET 2.0 已標示為「已過時」(deprecated) ,且建議改用 Thread.ManagedThreadId 屬性。但問題是,ManagedThreadId 傳回的執行緒 ID 並不是底層的 Win32 執行緒 ID,在這裡並不適用。因此,為了取得正確的 win32 執行緒 ID,且避免 Visual Studio 編譯時發出警告,在此範例中是利用 P/Invoke 的方式直接呼叫 WinAPI GetCurrentThreadId 來取得執行緒 ID。
  • 在鍵盤掛鉤程序中(KeyboardHookProc),如果要"吃掉"攔到的按鍵,可直接傳回 1,且不要呼叫 CallNextHookEx
  • 執行此範例時,如果想要將鍵盤掛鉤的處理抽離出來,成為一個獨立的類別,可以參考這篇文章:在C#中使用鉤子