LoginSignup
19

More than 3 years have passed since last update.

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

Posted at

本記事について

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

キーボードフックと目的

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

icon_keyboard.png

ここの発信をフックする。捨てたり、監視したり、別のものにしたりなど。

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

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

全てのキー入力を捨てる

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

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以外にすると、そのまま入力がされます。
ということは、このコードだけだとただ監視するだけのプログラムになります。

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

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

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

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

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

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

        return 1;
    }

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

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

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秒間だけ押した時と同様の出力をします。

    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キーとして入れ替えるような処理が以下になります。

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;
    }
}

まとめ

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19