首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 全域鍵盤鉤子實作螢幕鍵盤:WH_KEYBOARD_LL 與 SendInput 模擬輸入

     編輯:關於C#

需求分析

螢幕鍵盤必須:

  1. **全域監聽**所有鍵盤事件 (無論焦點在哪個視窗) → 需 **低層鍵盤鉤子 (WH_KEYBOARD_LL)**。
  2. **模擬按鍵輸入**至作用中視窗 → 需 **SendInput API**。
  3. **自身不搶焦點** → 視窗樣式加 `WS_EX_NOACTIVATE`,滑鼠點擊標題列時才暫時啟用焦點以便拖曳。

核心 P/Invoke 宣告


internal static class NativeMethods
{
    // 鉤子
    [DllImport("user32.dll", SetLastError = true)]
    internal static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
    [DllImport("user32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static extern bool UnhookWindowsHookEx(IntPtr hhk);
    [DllImport("user32.dll")]
    internal static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

    // 模擬輸入
    [DllImport("user32.dll", SetLastError = true)]
    internal static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);

    // 視窗樣式
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern IntPtr GetWindowLong(IntPtr hWnd, int nIndex);
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    internal static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

    // 常數
    internal const int WH_KEYBOARD_LL = 13;
    internal const int WH_MOUSE_LL = 14;
    internal const int WM_KEYDOWN = 0x0100;
    internal const int WM_KEYUP = 0x0101;
    internal const int GWL_EXSTYLE = -20;
    internal const int WS_EX_NOACTIVATE = 0x08000000;
    internal const int WM_MOUSEACTIVATE = 0x0021;
    internal const int MA_NOACTIVATE = 3;
}

internal delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);

[StructLayout(LayoutKind.Sequential)]
internal struct KBDLLHOOKSTRUCT
{
    public uint vkCode;
    public uint scanCode;
    public uint flags;
    public uint time;
    public IntPtr dwExtraInfo;
}

[StructLayout(LayoutKind.Sequential)]
internal struct INPUT
{
    public uint type;
    public InputUnion u;
    public static INPUT CreateKeyboard(ushort vk, uint flags = 0) => new INPUT
    {
        type = 1, // INPUT_KEYBOARD
        u = new InputUnion { ki = new KEYBDINPUT { wVk = vk, dwFlags = flags } }
    };
}

[StructLayout(LayoutKind.Explicit)]
internal struct InputUnion
{
    [FieldOffset(0)] internal MOUSEINPUT mi;
    [FieldOffset(0)] internal KEYBDINPUT ki;
    [FieldOffset(0)] internal HARDWAREINPUT hi;
}

[StructLayout(LayoutKind.Sequential)]
internal struct KEYBDINPUT
{
    public ushort wVk;
    public ushort wScan;
    public uint dwFlags;
    public uint time;
    public IntPtr dwExtraInfo;
}
internal const uint KEYEVENTF_KEYUP = 0x0002;
internal const uint KEYEVENTF_UNICODE = 0x0004;

鍵盤鉤子管理類


public class GlobalKeyboardHook : IDisposable
{
    private readonly HookProc _proc;
    private IntPtr _hook = IntPtr.Zero;

    public event Action KeyEvent; // (Key, isDown)

    public GlobalKeyboardHook()
    {
        _proc = HookCallback;
        _hook = NativeMethods.SetWindowsHookEx(
            NativeMethods.WH_KEYBOARD_LL, _proc,
            Marshal.GetHINSTANCE(typeof(GlobalKeyboardHook).Assembly.GetModules()[0]), 0);
        if (_hook == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error());
    }

    private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0)
        {
            var info = Marshal.PtrToStructure(lParam);
            bool down = wParam == (IntPtr)NativeMethods.WM_KEYDOWN || wParam == (IntPtr)0x0104; // WM_SYSKEYDOWN
            KeyEvent?.Invoke((Keys)info.vkCode, down);
        }
        return NativeMethods.CallNextHookEx(_hook, nCode, wParam, lParam);
    }

    public void Dispose()
    {
        if (_hook != IntPtr.Zero) NativeMethods.UnhookWindowsHookEx(_hook);
        _hook = IntPtr.Zero;
        GC.SuppressFinalize(this);
    }
}

模擬按鍵輸入


public static class KeyboardSimulator
{
    public static void KeyDown(Keys key) => Send(KEYBDINPUT.Create(key, 0));
    public static void KeyUp(Keys key) => Send(KEYBDINPUT.Create(key, KEYEVENTF_KEYUP));
    public static void KeyPress(Keys key) { KeyDown(key); KeyUp(key); }

    private static void Send(INPUT input)
    {
        if (NativeMethods.SendInput(1, new[] { input }, Marshal.SizeOf()) == 0)
            throw new Win32Exception(Marshal.GetLastWin32Error());
    }
}

螢幕鍵盤視窗:防焦點搶奪


public partial class OnScreenKeyboard : Form
{
    private readonly GlobalKeyboardHook _hook = new GlobalKeyboardHook();

    public OnScreenKeyboard()
    {
        InitializeComponent();
        // 1. 建立時即設定不啟用
        Load += (s, e) => SetNoActivate(true);
        // 2. 滑鼠點擊標題列才暫時允許焦點 (以便拖曳)
        var mouseHook = new GlobalMouseHook(); // 類似鍵盤鉤子實作 WH_MOUSE_LL
        mouseHook.MouseEvent += (btn, pt) =>
        {
            if (btn == MouseButtons.Left)
            {
                var captionRect = new Rectangle(Location, new Size(Width, SystemInformation.CaptionHeight));
                bool onCaption = captionRect.Contains(pt);
                SetNoActivate(!onCaption); // 標題列可焦點,其他區域不可
                if (onCaption) NativeMethods.SendMessage(Handle, NativeMethods.WM_MOUSEACTIVATE, Handle, (IntPtr)NativeMethods.MA_NOACTIVATE);
            }
        };
    }

    private void SetNoActivate(bool enable)
    {
        var exStyle = NativeMethods.GetWindowLong(Handle, NativeMethods.GWL_EXSTYLE);
        if (enable) exStyle = new IntPtr(exStyle.ToInt64() | NativeMethods.WS_EX_NOACTIVATE);
        else exStyle = new IntPtr(exStyle.ToInt64() & ~NativeMethods.WS_EX_NOACTIVATE);
        NativeMethods.SetWindowLong(Handle, NativeMethods.GWL_EXSTYLE, exStyle);
    }

    // 按鈕點擊事件範例
    private void BtnKey_Click(object sender, EventArgs e)
    {
        var btn = (Button)sender;
        var key = (Keys)btn.Tag; // Tag 存放 Keys 值
        if (IsModifier(key)) // Shift/Ctrl/Alt 鎖定切換
        {
            if (btn.BackColor == SystemColors.Control)
            {
                KeyboardSimulator.KeyDown(key);
                btn.BackColor = SystemColors.ControlDark;
            }
            else
            {
                KeyboardSimulator.KeyUp(key);
                btn.BackColor = SystemColors.Control;
            }
        }
        else
        {
            KeyboardSimulator.KeyPress(key);
        }
    }

    private bool IsModifier(Keys k) => k is Keys.ShiftKey or Keys.ControlKey or Keys.Menu; // Alt = Menu

    protected override void Dispose(bool disposing)
    {
        if (disposing) { _hook.Dispose(); /* mouseHook.Dispose(); */ }
        base.Dispose(disposing);
    }
}

虛擬鍵碼對照表 (節錄)


Keys.A = 0x41  Keys.B = 0x42  ... Keys.Z = 0x5A
Keys.D0 = 0x30 ... Keys.D9 = 0x39
Keys.NumPad0 = 0x60 ... Keys.NumPad9 = 0x69
Keys.F1 = 0x70 ... Keys.F12 = 0x7B
Keys.Left = 0x25 Keys.Up = 0x26 Keys.Right = 0x27 Keys.Down = 0x28
Keys.ShiftKey = 0x10 Keys.ControlKey = 0x11 Keys.Menu = 0x12 (Alt)
Keys.LWin = 0x5B Keys.RWin = 0x5C Keys.Apps = 0x5D

注意事項與最佳實踐

議題對策
鉤子效能影響低層鉤子在每次鍵盤事件都會回呼 managed 代碼,**務必極簡化回呼邏輯**,避免 UI 執行緒卡頓。
64 位元相容P/Invoke 簽名使用 IntPtr、uint、SetLastError=true,確保 x64 正常運作。
UAC / 高權限視窗低權限進程**無法**向高權限 (管理員) 視窗注入鍵盤輸入,需將螢幕鍵盤同樣以管理員身分執行。
多語言/IME模擬字元輸入建議用 KEYEVENTF_UNICODE + SendInput 送 Unicode 碼點,而非虛擬鍵碼。
現代替代Windows 10+ 內建 觸控鍵盤 (TabTip.exe),可透過 InputPane API 或啟動 `osk.exe` 替代自製方案。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved