2012年8月9日 星期四

C# 鍵盤掛鉤(keyboard hook)範例


這是一個以 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, falsefalse);
  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#中使用鉤子

沒有留言:

張貼留言