摘要: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, 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#中使用鉤子。