螢幕鍵盤需攔截系統層級鍵盤訊息,無論視窗是否具焦點皆能運作,核心技術是 低階鍵盤鉤子。本文完整解析鉤子原理、P/Invoke 封裝、安裝/卸載流程、虛擬按鍵模擬與 UI 焦點管理。
SetWindowsHookEx 傳入當前模組控制代碼與線程 ID 0 即可實現進程外全域鉤子。internal static class NativeMethods {
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
internal static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", EntryPoint = "SendInput", CharSet = CharSet.Auto)]
internal static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
[DllImport("user32.dll", EntryPoint = "GetKeyState", CharSet = CharSet.Auto)]
internal static extern short GetKeyState(int nVirtKey);
// ... GetWindowLong, SetWindowLong, SendMessage, GetTickCount 等
}
private IntPtr hKeyboardHook;
private HookProc KeyboardHookProcedure;
public void Start(bool installKeyboardHook) {
if (hKeyboardHook == IntPtr.Zero && installKeyboardHook) {
KeyboardHookProcedure = KeyboardHookProc;
hKeyboardHook = NativeMethods.SetWindowsHookEx(
13, // WH_KEYBOARD_LL
KeyboardHookProcedure,
Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]),
0);
if (hKeyboardHook == IntPtr.Zero)
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
public void Stop() {
if (hKeyboardHook != IntPtr.Zero) {
NativeMethods.UnhookWindowsHookEx(hKeyboardHook);
hKeyboardHook = IntPtr.Zero;
}
}
透過 SendInput 發送 KeyDown/KeyUp:
private void SendKeyDown(short vk) {
var input = new INPUT { type = 1, ki = new KEYBDINPUT { wVk = vk, time = NativeMethods.GetTickCount() } };
if (NativeMethods.SendInput(1, new[] { input }, Marshal.SizeOf(input)) == 0)
throw new Win32Exception(Marshal.GetLastWin32Error());
}
private void SendKeyUp(short vk) {
var input = new INPUT { type = 1, ki = new KEYBDINPUT { wVk = vk, dwFlags = 0x0002, time = NativeMethods.GetTickCount() } };
NativeMethods.SendInput(1, new[] { input }, Marshal.SizeOf(input));
}
螢幕鍵盤視窗預設不可獲焦點(WS_EX_NOACTIVATE),僅在使用者點擊標題列時暫時移除該樣式以允許拖曳:
private void HookOnMouseActivity(object sender, MouseEventArgs e) {
if (e.Button == MouseButtons.Left) {
var captionRect = new Rectangle(Location, new Size(Width, SystemInformation.CaptionHeight));
if (captionRect.Contains(e.Location)) {
// 允許獲焦點
NativeMethods.SetWindowLong(Handle, -20,
NativeMethods.GetWindowLong(Handle, -20).ToInt32() & ~0x08000000);
NativeMethods.SendMessage(Handle, 0x0007, IntPtr.Zero, IntPtr.Zero);
} else {
// 禁止獲焦點
NativeMethods.SetWindowLong(Handle, -20,
NativeMethods.GetWindowLong(Handle, -20).ToInt32() | 0x08000000);
}
}
}
文中附贈完整 KeyboardConstaint 類別,涵蓋 F1-F12、方向鍵、修飾鍵、數字鍵盤、OEM 鍵等 90+ 常數,開箱即用。
編譯為 DLL 後,主程式訂閱 KeyDown/KeyUp 事件即可接收全域按鍵,結合自訂按鈕控制項與虛擬按鍵發送,即可打造功能完整的螢幕鍵盤。