【コピペで使える】C#からKey Hookでキー入力を取得する

当ページのリンクには広告が含まれています。

今回はC#を使って Key Hook (キーボードフック)のWindow API を呼び出し、任意のキーを取得する方法を解説します。

自作アプリであれば、表示中の画面から簡単にキーを取得できますが、タスクトレイに隠れている常駐アプリなどは画面が非表示且つ非アクティブのため、それができません。

そこで、キーボードフック(キーボードの横取り)が登場します。

すぐにお使い頂けるようクラス化したサンプルのほか、物理キーとキーコードの対応表も纏めましたので、併せてご覧下さい。

目次

Key Hook の概要

通常は、OS(Windows)がキーボードを一括監視し、その時アクティブなアプリに対してキー情報を転送します。

キーボードフックは、OSが受け取ったキーを、アクティブなアプリより前に受け取る仕組みです。

WindowsForm や WPF では、標準でこの機能が搭載されていないため、WindowsのAPIを呼び出さなければなりません。

Key Hookの方法

Key Hook を使うには、次の手順が必要です。

必要なDLLのインポート

インポート対象のDLLと、今回使うAPI(関数)は次の通りです。

関数名DLL機能
SetWindowsHookExuser32.dllキーボードフック時に呼び出すユーザー関数
を登録する
CallNextHookExuser32.dllキーボードフックを行う他のアプリに対して
キーボード情報を転送する
GetModuleHandlekernel32.dllモジュールを操作するためのハンドルを
取得する
UnhookWindowsHookExuser32.dllキーボードフックを取り止める

コールバック関数の登録

コールバック関数を登録した瞬間から、キーボードフックが開始されます。

やっていることは次の通りです。

  • GetCurrentProcess() で自分自身のプロセス情報を取り出し、コールバック関数と一緒にSetWindowsHookExの引数に指定して呼び出す。
  • 戻り値のキーボードフック・ハンドルを変数に保存する(キーボードフックを終了する際に使用)
//キーボードフックの登録
using (Process process = Process.GetCurrentProcess())
{
    using (ProcessModule module = process.MainModule)
    {
        _hookHandle = SetWindowsHookEx(
           13,                                   //フックするイベントの種類
           _callBack,                            //フック時のコールバック関数
           GetModuleHandle(module.ModuleName),   //インスタンスハンドル
           0                                     //スレッドID(0:全てのスレッドでフック)
       );
    }
}

SetWindowsHookExの第一引数で指定されている13は、フックするイベントを指定するための固定値で、キーボードの場合は13を指定します。

キーボードフックの取り止め

キーボードフックを取り止めるには、UnhookWindowsHookExを使用します。

コールバック関数登録時に取得したキーボードフック・ハンドルを引数に指定して呼び出すことで、キーボードフックを取り止めることが出来ます。

UnhookWindowsHookEx(_hookHandle);
_hookHandle = IntPtr.Zero;

ここでは念のため、_hookHandle 変数の中身もクリアしています。

コールバック関数

コールバック関数の wParam 引数にはキーの種類が、lParam にはキーの押下状態(キーが押された/離された)が渡ってきます。

関数の最後に CallNextHookEx の戻り値を return で返していますが、これはキーボードフックを行っている他のアプリに対して、キー情報を転送するためのものです。

private IntPtr CallbackProc(int nCode, IntPtr wParam, IntPtr lParam)
{
    Keys key = (Keys)(short)Marshal.ReadInt32(lParam);

    //キーボードが押された
    if ((int)wParam == 256)
    {
        /* ~ここに処理を記述~ */
    }
    //キーボードが離された
    if ((int)wParam == 257) 
    {
        /* ~ここに処理を記述~ */
    }

    return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
}
   

CallNextHookEx の戻り値を return することは強く推奨されていますが、必須ではありません。

例えば、F1キーを押したときに画面キャプチャを取るようなプログラムにおいて、他のアプリがF1を検知して誤動作することを避けたい場合は、return 1 で終了させます。

return (IntPtr)1;

コールバック関数の戻り値として 1 を指定した場合、他のアプリにキー情報が転送されません。

逆に、戻り値に 0 (実際には1以外の数値)を指定した場合、そのキー情報は他のアプリに転送されます。

return (IntPtr)0;

最小限のサンプルソース(クラス化したもの)

これまでの内容を踏まえて、キーボードフックをクラス化したソースコードの紹介と、使い方について解説します。

使い方

使い方の手順は次の通りです。

  • KeyboardHook() のインスタンスを生成する。
  • キーフックのイベントハンドラ(OnKeyDown,OnKeyUp)を記述する。
  • フックの開始は Hook メソッドを呼び出す。
  • フック終了時は、UnHook メソッドを呼び出す。

フックしたキーを、他のアプリで利用させたくない場合は、イベントハンドラ内で RetCode =1 を代入してください。

下記は WindowsアプリのMainWindowクラス内でキーボードフックを使うサンプルです。

public partial class MainWindow : Window
{
    //キーボードフッククラスのインスタンス生成
    KeyboardHook _hook = new KeyboardHook();

    //コンストラクタ
    public MainWindow()
    {
        InitializeComponent();
    }

    //画面起動時のイベント
    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        //キーボードが押された時のイベントハンドラを登録
        _hook.OnKeyDown += (s, ea) =>
        {
            Console.WriteLine(ea.Key);
            ea.RetCode = 0 //他のプログラムにキーを転送したくない場合は1を代入;
        };
        //キーボードフックの開始
        _hook.Hook();
    }

    //画面終了時のイベント
    private void Window_Closed(object sender, EventArgs e)
    {
        //キーボードフックの終了
        _hook.UnHook();
    }
}

ソースコード

以下はソースコード全体です。

そのままコピペでも使えますが、必要に応じて追加修正して下さい。

public class KeyboardHook
{
    private const int WH_KEYBOARD_LL = 0x0D;
    private const int WM_KEYBOARD_DOWN = 0x100;
    private const int WM_KEYBOARD_UP = 0x101;
    private const int WM_SYSKEY_DOWN = 0x104;
    private const int WM_SYSKEY_UP = 0x105;

    //イベントハンドラの定義
    public event EventHandler<KeyboardHookEventArgs> OnKeyDown = delegate { };
    public event EventHandler<KeyboardHookEventArgs> OnKeyUp = delegate { };

    //コールバック関数のdelegate 定義
    private delegate IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam);

    //キーボードフックに必要なDLLのインポート
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr SetWindowsHookEx(int idHook, HookCallback lpfn, IntPtr hMod, uint dwThreadId);

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

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

    //フックプロシージャのハンドル
    private static IntPtr _hookHandle = IntPtr.Zero;

    //フック時のコールバック関数
    private static HookCallback _callback;

    /// <summary>
    /// キーボードHook の開始
    /// </summary>
    /// <param name="callback"></param>
    public void Hook()
    {
        _callback = CallbackProc;
        using (Process process = Process.GetCurrentProcess())
        {
            using (ProcessModule module = process.MainModule)
            {
                _hookHandle = SetWindowsHookEx(
                   WH_KEYBOARD_LL,                                          //フックするイベントの種類
                   _callback, //フック時のコールバック関数
                   GetModuleHandle(module.ModuleName),                      //インスタンスハンドル
                   0                                                        //スレッドID(0:全てのスレッドでフック)
               );
            }
        }
    }
    /// <summary>
    /// コールバック関数
    /// </summary>
    /// <param name="nCode"></param>
    /// <param name="wParam"></param>
    /// <param name="lParam"></param>
    /// <returns></returns>
    private IntPtr CallbackProc(int nCode, IntPtr wParam, IntPtr lParam)
    {
        var args = new KeyboardHookEventArgs();
        Keys key = (Keys)(short)Marshal.ReadInt32(lParam);
        args.Key = key;

        if ((int)wParam == WM_KEYBOARD_DOWN || (int)wParam == WM_SYSKEY_DOWN) OnKeyDown(this, args);
        if ((int)wParam == WM_KEYBOARD_UP || (int)wParam == WM_SYSKEY_UP) OnKeyUp(this, args);

        return (args.RetCode == 0) ? CallNextHookEx(_hookHandle, nCode, wParam, lParam) : (IntPtr)1;
    }
    /// <summary>
    /// キーボードHockの終了
    /// </summary>
    public void UnHook()
    {
        UnhookWindowsHookEx(_hookHandle);
        _hookHandle = IntPtr.Zero;
    }
}

/// <summary>
/// キーボードフックのイベント引数
/// </summary>
public class KeyboardHookEventArgs
{
    public Keys Key { get; set; }
    public int RetCode { get; set; } = 0;
}

物理キーとキーコードの対応表

最後に、物理的なキーボードとキーコード(KeyboardHookEventArgs で受け取れる Keyプロパティの値)の対応表を紹介しておきます。

詳しくは、 lParamの値を Marshal.ReadInt32() メソッドでキーコードに変換し、更にToString() で文字列にした結果と、物理キーボードの対応表になります。

キーボードはメーカーによって配置が異なりますので、あくまでも参考例としてお考え下さい。

var key =  (Keys)(short)Marshal.ReadInt32(lParam);
if(key.ToString() == "LShiftKey")
{
    Console.WriteLine("左シフトキーが押されました)
}

ちなみに、Alt キーは "LMenu" 、"LMenu" という文字列で返されますので、ご注意ください。

一方、Keysで判断する場合は次のようになります。

var key =  (Keys)(short)Marshal.ReadInt32(lParam);
if(key == Keys.LShiftKey)
{
    Console.WriteLine("左シフトキーが押されました)
}

では、"D4, Oemtilde"(半角/全角キー)や "ShiftKey, OemBackslash"(カタカナ・ひらかな・ローマ字キー)のようにカンマ区切りになっている場合はどう判断すればよいでしょう。

"ShiftKey, OemBackslash"の場合、Keys.ShiftKey と Keys.Oem102 の論理和の値になっています。

従って、次のような記述になります。

var key =  (Keys)(short)Marshal.ReadInt32(lParam);
if(key ==  (Keys.ShiftKey | Keys.Oem102))
{
    Console.WriteLine("カタカナ・ひらかな・ローマ字キーが押されました)
}

同様に、"D4, Oemtilde" の場合は (Keys.D4 | Oem3) で判定が可能です。

まとめ

今回はC#を用たKey Hook(キーボードフック)について、下記3点を解説しました。

  • キーボードフックの実現方法
  • コピペで使えるようクラス化したサンプルプログラム
  • 物理キーとキーコードの対応表

通常のWindowアプリでキーボードフックを使うケースは稀ですが、タスクトレイに常駐するアプリ、例えば画面をキャプチャするようなケースでは、キーボードフックが必須となります。

今回紹介したように、キーボードフックは簡単に実装できますので、もし常駐型のアプリを開発する場合は、この記事を参考にして頂ければ幸いです。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメント一覧 (1件)

  • このページを参考にキーボードフックを作成したのですが、
    “カタカナ・ひらかな・ローマ字”キーの
    「キーボードが離された時」のイベントが取得できませんでした。
    何かご存知でしょうか?
    よろしくお願いします。

コメントする

目次