本記事について
WindowsでC#を使いキーボードのフックする方法を記述します。
キーボードフックと目的
キーボードの入力をPCに発信するものをキャッチし
それを無効にしたり別のキーに置き換えたりするなどの用途で使用します。
ここの発信をフックする。捨てたり、監視したり、別のものにしたりなど。
最小形プログラムでキーボードフック
以下のコードでキーボード入力をフックします。
全てのキー入力を捨てる
プログラム実行中、全てのキー入力が捨てられます。
フックしたキーは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;
}
}
まとめ
極力最小形でシンプルな記述にしましたがどうだったでしょうか。
この仕組みがわかれば、キーの入れ替え、マクロで自動化などができそうです。