25
sie/090
sie/090
[C#] Niskopoziomowe przechwytywanie klawiszy klawiatury
Aby przechwycić naciśnięcie klawiszy na niskim poziomie (np. przed wszystkimi innymi aplikacjami) należy skorzystać z InteropSys aby „wpiąć” się do API systemu operacyjnego. Poniższy przykład z bloga Stephena Touba pokazuje jak to szybko i prosto zrobić:
using System;
using System.Diagnostics;
using System.Windows.Forms;
using System.Runtime.InteropServices;
class InterceptKeys
{
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 0x0100;
private const int WM_KEYUP = 0x0101;
private static LowLevelKeyboardProc _proc = HookCallback;
private static IntPtr _hookID = IntPtr.Zero;
public static void Main()
{
_hookID = SetHook(_proc);
Application.Run();
UnhookWindowsHookEx(_hookID);
}
private static IntPtr SetHook(LowLevelKeyboardProc proc)
{
using (Process curProcess = Process.GetCurrentProcess())
using (ProcessModule curModule = curProcess.MainModule)
{
return SetWindowsHookEx(WH_KEYBOARD_LL, proc,
GetModuleHandle(curModule.ModuleName), 0);
}
}
private delegate IntPtr LowLevelKeyboardProc(
int nCode, IntPtr wParam, IntPtr lParam);
private static IntPtr HookCallback(
int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
{
int vkCode = Marshal.ReadInt32(lParam);
Console.WriteLine((Keys)vkCode);
}
return CallNextHookEx(_hookID, nCode, wParam, lParam);
}
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook,
LowLevelKeyboardProc 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("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,
IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
}
„Problem” zaczyna się w przypadku gdy chcemy obsłużyć naciśnięcie kombinacji klawiszy, a w szczególnym przypadku ALT i jakiegoś innego klawisza. ALT nie jest „przechwytywany” przez hook, przez co funkcję private static IntPtr HookCallback należy nieco przerobić, korzystając ze wcześniej zdefiniowanej struktury KeyboardHookStruct w której to możemy sprawdzić również flagi naciśniętych przycisków (w tym przypadku ALT+ESC):
public struct KeyboardHookStruct
{
public int vkCode;
public int scanCode;
public int flags;
public int time;
public int dwExtraInfo;
};
// ...
KeyboardHookStruct hookStruct = (KeyboardHookStruct)Marshal.PtrToStructure(lParam, typeof(KeyboardHookStruct));
if (hookStruct.vkCode == ESCAPE && (hookStruct.flags & LLKHF_ALTDOWN) == hookStruct.flags)
MessageBox.Show("I pressed ALT + ESC!");
Kilka uwag:
- Należy zauważć że w tym przypadku najpierw sprawdzamy drugi klawisz z kombinacji, a potem fakt czy jest wciśnięty ALT.
- Druga istotna rzecz to fakt zastosowania porównania wartości klawiszy do znanych z WinAPI nazw klawiszy wirtualnych.
- Listę kodów klawiszy wirtualnych można znaleźć na tej stronie Microsoft MSDN
- Łatwo napisać aplikację która przechwytuje klawisze, ale nie przekazuje ich dalej do kolejnych aplikacji zarejestrowanych w systemie w ramach łańcucha uchwytów. Wystarczy usunąć wywołania do funkcji: CallNextHookEx
- Jeżeli wciśniemy klawisz i przytrzymamy, informacje o jego naciśnięciu (KEYDOWN) będą w kółko (w odpowiednim odstępie czasowym) przesyłane. Należy to odpowiednio obsłużyć.
- Przykład z klawiszem ALT będzie wykonywał się dwa razy, ze względu na fakt że w nacisnięcie klawisza to w rzeczywistości dwie akcje. Naciśnięcie (KEY_DOWN) oraz podniesienie (KEY_UP) klawisza. Można to naprawić poprzez rozpoznanie jaka aktualnie akcja (DOWN/UP) jest wykonywana (poprzez porównanie z wParam) lub poprzez dodanie prostego semaforu