Windowsのキーボードフックの最小サンプル

# 本記事について

WindowsでC#を使いキーボードのフックする方法を記述します。

# キーボードフックと目的

キーボードの入力をPCに発信するものをキャッチし
それを無効にしたり別のキーに置き換えたりするなどの用途で使用します。

![icon_keyboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17192/574409e4-862b-779d-bdcd-aab309a114d1.png)

<img width="80" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17192/9dddd71c-13d8-9acd-d8f9-16e23e86be74.png"> ここの発信をフックする。捨てたり、監視したり、別のものにしたりなど。

<img width="250" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17192/dd2eb95f-e6b5-942a-1e62-b3ddee2ac199.png">

# 最小形プログラムでキーボードフック

以下のコードでキーボード入力をフックします。

## 全てのキー入力を捨てる

プログラム実行中、全ての<font color="Red">キー入力が捨てられます。</font>
フックしたキーはHookCallbackの第3引数、lParamで識別ができます。

```c#
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;

class SampleKeyHook
{
    static void Main(string[] args)
    {
        var myHook = new MyHook();
        myHook.Hook();

        Application.Run();

        myHook.HookEnd();
    }
}

class MyHook
{
    delegate int delegateHookCallback(int nCode, IntPtr wParam, IntPtr lParam);
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    static extern IntPtr SetWindowsHookEx(int idHook, delegateHookCallback lpfn, IntPtr hMod, uint dwThreadId);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool UnhookWindowsHookEx(IntPtr hhk);

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

    IntPtr hookPtr = IntPtr.Zero;

    public void Hook()
    {
        using (Process curProcess = Process.GetCurrentProcess())
        using (ProcessModule curModule = curProcess.MainModule)
        {
            // フックを行う
            // 第1引数	フックするイベントの種類
            //   13はキーボードフックを表す
            // 第2引数 フック時のメソッドのアドレス
            //   フックメソッドを登録する
            // 第3引数	インスタンスハンドル
            //   現在実行中のハンドルを渡す
            // 第4引数	スレッドID
            //   0を指定すると、すべてのスレッドでフックされる
            hookPtr = SetWindowsHookEx(
                13,
                HookCallback,
                GetModuleHandle(curModule.ModuleName),
                0
            );
        }
    }

    int HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        // フックしたキー
        Console.WriteLine((Keys)(short)Marshal.ReadInt32(lParam));

        // 1を戻すとフックしたキーが捨てられます
        return 1;
    }

    public void HookEnd()
    {
        UnhookWindowsHookEx(hookPtr);
        hookPtr = IntPtr.Zero;
    }
}
```

## 全てのキー入力を監視

上記の1を戻すところを1以外にすると、そのまま入力がされます。
ということは、このコードだけだとただ監視するだけのプログラムになります。

```c#
    int HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        // フックしたキー
        Console.WriteLine((Keys)(short)Marshal.ReadInt32(lParam));

        // 1以外を戻すとフックしたキーがそのまま入力されます
        //return 1;
        return 0;
    }
```

## キーを押した時、離した時の区別

HookCallbackの第2引数、wParamで識別ができます。
大体は押した時で何か処理をさせることが多いでしょう。

```c#
    int HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        switch((int)wParam)
        {
            // キーを押したとき
            case 256:
                Console.WriteLine("キーを押した時");
                break;

            // キーを離したとき
            case 257:
                Console.WriteLine("キーを離した時");
                break;
        }

        return 1;
    }
```

# 最小形プログラムでキーボード出力

以下のプログラムを実行すると、キーボードを入力していないのにJキーを出力します。

```c#
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;

class SampleKeyInput
{
    static void Main(string[] args)
    {
        var myInput = new MyInput();
        myInput.Input();

        Application.Run();
    }
}

class MyInput
{
    [DllImport("user32.dll")]
    static extern void SendInput(int nInputs, ref INPUT pInputs, int cbsize);

    [StructLayout(LayoutKind.Sequential)]
    struct MOUSEINPUT
    {
        public int dx;
        public int dy;
        public int mouseData;
        public int dwFlags;
        public int time;
        public int dwExtraInfo;
    };

    [StructLayout(LayoutKind.Sequential)]
    struct KEYBDINPUT
    {
        public short wVk;
        public short wScan;
        public int dwFlags;
        public int time;
        public int dwExtraInfo;
    };

    [StructLayout(LayoutKind.Sequential)]
    struct HARDWAREINPUT
    {
        public int uMsg;
        public short wParamL;
        public short wParamH;
    };

    [StructLayout(LayoutKind.Explicit)]
    struct INPUT
    {
        [FieldOffset(0)]
        public int type;
        [FieldOffset(4)]
        public MOUSEINPUT no;
        [FieldOffset(4)]
        public KEYBDINPUT ki;
        [FieldOffset(4)]
        public HARDWAREINPUT hi;
    };

    public void Input()
    {
        INPUT input = new INPUT
        {
            // 1はキーボードを入力
            type = 1,
            ki = new KEYBDINPUT()
            {
                // 74はJキー
                wVk = 74,
                // DirectInputを介してキーボード入力をフェッチしているソフトウェアの場合は
                // 以下のようにスキャンコードをつけて送らないと無視されてしまうということがある
                // が今回はキーボードだけなので0(ゼロ)で
                //wScan = (short)MapVirtualKey((short)key, 0),
                wScan = 0,
                // キーボードダウンの場合は、0(ゼロ)
                dwFlags = 0,
                time = 0,
                dwExtraInfo = 0
            },
        };

        // 3秒後にJキーが入力されます
        Thread.Sleep(3000);
        SendInput(1, ref input, Marshal.SizeOf(input));
    }
}
```

## キーを押した時、離した時の出力

上記のようにキーを押していないのにプログラムでキーを出力すると物理的にはキーを離す
ということができません。
そのため、キーを離す出力をする必要があります。
以下でJキーを3秒間だけ押した時と同様の出力をします。

```c#
    public void Input()
    {
        INPUT input = new INPUT
        {
            // 1はキーボードを入力
            type = 1,
            ki = new KEYBDINPUT()
            {
                // 74はJキー
                wVk = 74,
                // DirectInputを介してキーボード入力をフェッチしているソフトウェアの場合は
                // 以下のようにスキャンコードをつけて送らないと無視されてしまうということがある
                // が今回はキーボードだけなので0(ゼロ)で
                //wScan = (short)MapVirtualKey((short)key, 0),
                wScan = 0,
                // キーボードダウンの場合は、0(ゼロ)
                dwFlags = 0,
                time = 0,
                dwExtraInfo = 0
            },
        };

        INPUT input2 = new INPUT
        {
            type = 1,
            ki = new KEYBDINPUT()
            {
                wVk = 74,
                wScan = 0,
                // キーボードアップの場合は、2
                dwFlags = 2,
                time = 0,
                dwExtraInfo = 0
            },
        };

        Thread.Sleep(3000);
        SendInput(1, ref input, Marshal.SizeOf(input));
        Thread.Sleep(3000);
        SendInput(1, ref input2, Marshal.SizeOf(input));
    }
```

# フックと出力の組み合わせ

組み合わせを行い、JキーをUキーとして入れ替えるような処理が以下になります。

```c#
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;

class SampleKeyHookAndInput
{
    static void Main(string[] args)
    {
        var myHookAndInput = new MyHookAndInput();
        myHookAndInput.Hook();

        Application.Run();

        myHookAndInput.HookEnd();
    }
}

class MyHookAndInput
{
    delegate int delegateHookCallback(int nCode, IntPtr wParam, IntPtr lParam);
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    static extern IntPtr SetWindowsHookEx(int idHook, delegateHookCallback lpfn, IntPtr hMod, uint dwThreadId);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool UnhookWindowsHookEx(IntPtr hhk);

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

    IntPtr hookPtr = IntPtr.Zero;

    [DllImport("user32.dll")]
    static extern void SendInput(int nInputs, ref INPUT pInputs, int cbsize);

    [StructLayout(LayoutKind.Sequential)]
    struct MOUSEINPUT
    {
        public int dx;
        public int dy;
        public int mouseData;
        public int dwFlags;
        public int time;
        public int dwExtraInfo;
    };

    [StructLayout(LayoutKind.Sequential)]
    struct KEYBDINPUT
    {
        public short wVk;
        public short wScan;
        public int dwFlags;
        public int time;
        public int dwExtraInfo;
    };

    [StructLayout(LayoutKind.Sequential)]
    struct HARDWAREINPUT
    {
        public int uMsg;
        public short wParamL;
        public short wParamH;
    };

    [StructLayout(LayoutKind.Explicit)]
    struct INPUT
    {
        [FieldOffset(0)]
        public int type;
        [FieldOffset(4)]
        public MOUSEINPUT no;
        [FieldOffset(4)]
        public KEYBDINPUT ki;
        [FieldOffset(4)]
        public HARDWAREINPUT hi;
    };

    public void Hook()
    {
        using (Process curProcess = Process.GetCurrentProcess())
        using (ProcessModule curModule = curProcess.MainModule)
        {
            hookPtr = SetWindowsHookEx(
                13,
                HookCallback,
                GetModuleHandle(curModule.ModuleName),
                0
            );
        }
    }

    int HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        Keys v = (Keys)(short)Marshal.ReadInt32(lParam);
        // Jキーの入力の場合
        if (v == Keys.J)
        {
            switch ((int)wParam)
            {
                // キー押下の時
                case 256:
                    INPUT input = new INPUT
                    {
                        type = 1,
                        ki = new KEYBDINPUT()
                        {
                            // 85はUキー
                            wVk = 85,
                            wScan = 0,
                            // キーボードダウンの場合は、0(ゼロ)
                            dwFlags = 0,
                            time = 0,
                            dwExtraInfo = 0
                        },
                    };
                    SendInput(1, ref input, Marshal.SizeOf(input));

                    return 1;

                // キー離した時
                case 257:
                    INPUT input2 = new INPUT
                    {
                        type = 1,
                        ki = new KEYBDINPUT()
                        {
                            // 85はUキー
                            wVk = 85,
                            wScan = 0,
                            // キーボードアップの場合は、2
                            dwFlags = 2,
                            time = 0,
                            dwExtraInfo = 0
                        },
                    };
                    SendInput(1, ref input2, Marshal.SizeOf(input2));

                    return 1;
            }
        }

        return 0;
    }

    public void HookEnd()
    {
        UnhookWindowsHookEx(hookPtr);
        hookPtr = IntPtr.Zero;
    }
}
```

# まとめ

極力最小形でシンプルな記述にしましたがどうだったでしょうか。
この仕組みがわかれば、キーの入れ替え、マクロで自動化などができそうです。