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

C# 全域鍵盤鉤子 (Global Keyboard Hook) 完整實作:截獲熱鍵、防毒軟體偵測與應對

     編輯:關於C#

為什麼需要全域鉤子?

應用程式需在**無焦點、最小化、背景執行**時仍能接收特定熱鍵 (如 `Ctrl+Alt+A` 截圖)。表單層級 `KeyDown` 事件僅在擁有焦點時觸發,無法滿足需求。

核心實作:KeyboardHook 類別 (封裝 Win32 API)

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;

public class KeyboardHook : IDisposable
{
    // 事件
    public event KeyEventHandler KeyDown;
    public event KeyPressEventHandler KeyPress;
    public event KeyEventHandler KeyUp;

    // Win32 API
    private delegate int HookProc(int nCode, IntPtr wParam, IntPtr lParam);
    private static HookProc _proc;
    private static int _hookId = 0;
    private const int WH_KEYBOARD_LL = 13; // 低層鍵盤鉤子 (全域)

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern bool UnhookWindowsHookEx(int idHook);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern int CallNextHookEx(int idHook, int nCode, IntPtr wParam, IntPtr lParam);

    [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
    private static extern IntPtr GetModuleHandle(string lpModuleName);

    [StructLayout(LayoutKind.Sequential)]
    private struct KBDLLHOOKSTRUCT
    {
        public int vkCode;
        public int scanCode;
        public int flags;
        public int time;
        public int dwExtraInfo;
    }

    private const int WM_KEYDOWN = 0x0100;
    private const int WM_KEYUP = 0x0101;
    private const int WM_SYSKEYDOWN = 0x0104;
    private const int WM_SYSKEYUP = 0x0105;

    public void Start()
    {
        if (_hookId != 0) return;

        _proc = HookCallback; // 必須保持委派參考,防止 GC 回收
        using (ProcessModule module = Process.GetCurrentProcess().MainModule)
        {
            _hookId = SetWindowsHookEx(WH_KEYBOARD_LL, _proc, GetModuleHandle(module.ModuleName), 0);
        }

        if (_hookId == 0)
            throw new Win32Exception(Marshal.GetLastWin32Error(), "安裝鍵盤鉤子失敗");
    }

    public void Stop()
    {
        if (_hookId != 0)
        {
            UnhookWindowsHookEx(_hookId);
            _hookId = 0;
        }
    }

    private int HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0 && (KeyDown != null || KeyUp != null || KeyPress != null))
        {
            var info = Marshal.PtrToStructure(lParam);
            Keys key = (Keys)info.vkCode;

            // KeyDown / SysKeyDown
            if ((KeyDown != null) && (wParam == (IntPtr)WM_KEYDOWN || wParam == (IntPtr)WM_SYSKEYDOWN))
            {
                var args = new KeyEventArgs(key);
                KeyDown(this, args);
                if (args.Handled) return 1; // 阻止傳遞
            }

            // KeyPress (字元輸入)
            if (KeyPress != null && (wParam == (IntPtr)WM_KEYDOWN || wParam == (IntPtr)WM_SYSKEYDOWN))
            {
                // ToAscii 需鍵盤狀態,簡化略過,實際需呼叫 GetKeyboardState + ToAscii
            }

            // KeyUp / SysKeyUp
            if ((KeyUp != null) && (wParam == (IntPtr)WM_KEYUP || wParam == (IntPtr)WM_SYSKEYUP))
            {
                var args = new KeyEventArgs(key);
                KeyUp(this, args);
                if (args.Handled) return 1;
            }
        }
        return CallNextHookEx(_hookId, nCode, wParam, lParam);
    }

    public void Dispose() => Stop();
    ~KeyboardHook() => Dispose();
}

使用範例:註冊 Ctrl+Alt+A 熱鍵

public partial class MainForm : Form
{
    private KeyboardHook _hook;

    public MainForm()
    {
        InitializeComponent();
        _hook = new KeyboardHook();
        _hook.KeyDown += OnGlobalKeyDown;
        _hook.Start();
    }

    private void OnGlobalKeyDown(object sender, KeyEventArgs e)
    {
        // 判斷 Ctrl+Alt+A
        if (e.KeyCode == Keys.A && e.Control && e.Alt)
        {
            e.Handled = true; // 可選:阻止其他程式接收
            this.Invoke((Action)(() => MessageBox.Show("全域熱鍵觸發!")));
        }
    }

    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        _hook?.Dispose();
        base.OnFormClosing(e);
    }
}

修飾鍵判斷對照表

組合判斷條件
單一鍵 (A)`e.KeyCode == Keys.A`
Ctrl + A`e.KeyCode == Keys.A && e.Control`
Alt + A`e.KeyCode == Keys.A && e.Alt`
Shift + A`e.KeyCode == Keys.A && e.Shift`
Ctrl + Alt + A`e.KeyCode == Keys.A && e.Control && e.Alt`
Ctrl + Shift + A`e.KeyCode == Keys.A && e.Control && e.Shift`
Win + A`e.KeyCode == Keys.A && (e.Modifiers & Keys.Windows) == Keys.Windows`

防毒軟體偵測與應對 (關鍵)

  • 360、火綠、卡巴斯基等 會偵測 `SetWindowsHookEx(WH_KEYBOARD_LL, ...)` 並彈窗警告「程式正在監聽鍵盤輸入」。
  • 應對策略:
    1. **數位簽章**:購買 EV 代碼簽章證書簽署 EXE,大幅降低誤報。
    2. **白名單提交**:向主流防毒廠商提交樣本申請白名單。
    3. **使用者引導**:首次啟動顯示說明對話框,告知「需全域熱鍵功能,請允許」。
    4. **替代方案**:若僅需註冊特定熱鍵,改用 `RegisterHotKey` (WM_HOTKEY) 系統級熱鍵註冊,**不觸發鉤子偵測**,但功能較受限 (無法攔截、無法判斷複雜組合)。

RegisterHotKey 替代方案 (較簡單、不被攔截)

[DllImport("user32.dll")]
private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);

[DllImport("user32.dll")]
private static extern bool UnregisterHotKey(IntPtr hWnd, int id);

private const int WM_HOTKEY = 0x0312;
private const uint MOD_ALT = 0x0001;
private const uint MOD_CONTROL = 0x0002;
private const uint MOD_SHIFT = 0x0004;
private const uint MOD_WIN = 0x0008;

protected override void WndProc(ref Message m)
{
    if (m.Msg == WM_HOTKEY)
    {
        int id = m.WParam.ToInt32();
        if (id == 1) MessageBox.Show("熱鍵觸發 (RegisterHotKey)");
    }
    base.WndProc(ref m);
}

// 註冊 Ctrl+Alt+A
RegisterHotKey(this.Handle, 1, MOD_CONTROL | MOD_ALT, (uint)Keys.A);

參考資源

原文連結:https://www.bianceng.cn/Programming/csharp/201410/45484.htm

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved