Potřebovali jste někdy pod platformou .NET globálně odchytávat zprávy z klávesnice? Pokud ano, nejspíše jste narazili, protože .NET Framework tyto globální hooky nepodporuje. Právě kvůli tomu se při implementaci naší aplikace nevyhneme použití Platform Invoke. Při využívaní Win API z řízeného prostředí se často můžeme dostat do problémů když hotovou aplikaci, která byla odladěna na 32 bitovém systému, spustíme na 64 bitovém systému a naopak. Naše ukázková aplikace bude napsána tak, že bude fungovat na obou platformách.
Pro ty, kteří neví, co si představit pod pojmem globální odchytávaní zpráv - znamená to, že naše aplikace může běžet na pozadí a i přes to může odchytávat stisky kláves. Tedy například, když budeme psát něco ve Wordu, tak naše aplikace může klidně zapisovat stisklé klávesy do souboru někam na disk.
Ješte než začneme, chtěl bych říct že Win API, které v příkladu využiji jsou velmi komplexní funkce s velmi širokou funkcionalitou - lze je použít k mnoha jiným účelům, protože záleží pouze na volbě hodnot parametrů, kterých je velké množství. Proto kdyby někdo potřeboval jejich podrobnější popis, tak odkazuji na MSDN, kde je Win API popsáno dostatečně detailně.
Odchytávaní zpráv
Tento proces funguje tak, že do určitého modulu přidáme jakýsi "detektor" (hook) zpráv z klávesnice, který právě při zachycení nějaké zprávy zavolá námi definovanou metodu, která nám zároveň předá informace o stisknutých klávesách. Tímto modulem bude knihovna kernel32.dll, která je potřeba při spouštění všech aplikací (to je pouze má úvaha, ale funguje). Nejdříve začneme implementací třídy, která bude deklarovat Win API funkce potřebné k odchytávání zpráv.
static class api
{
[DllImport("kernel32.dll")]
public static extern IntPtr LoadLibrary(string name);
[DllImport("kernel32.dll")]
public static extern void FreeLibrary(IntPtr lib);
[DllImport("user32.dll")]
public static extern IntPtr SetWindowsHookEx(int hook, LowLevelKeyboardProc callback, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll")]
public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, uint wParam, KeyboardHookStruct lParam);
[DllImport("user32.dll")]
public static extern bool UnhookWindowsHookEx(IntPtr hInstance);
public delegate IntPtr LowLevelKeyboardProc(int nCode, uint wParam, ref KeyboardHookStruct lParam);
[StructLayout(LayoutKind.Sequential)]
public struct KeyboardHookStruct
{
public uint vkCode;
public uint scanCode;
public uint flags;
public uint time;
public IntPtr dwExtraInfo;
}
public const uint WM_KEYFIRST = 0x100;
public const uint WM_SYSKEYDOWN = 0x104;
public const int WH_KEYBOARD_LL = 13;
}
A nyní naimplementujeme vysokoúrovňovější třídu, která bude interně využívat Win API, ale programátor používající tuto třídu již s Win API pracovat nebude. Na venek třída implementuje pouze událost KeyPressed. V callback funkci MyCallbackFunction je uvedena podmínka, která zajišťuje výpis zprávy pouze při stisku. Pokud by tam tato podmínka nebyla tak by se vypisovali i zprávy o zvedání kláves. Zajímavost je, že pokud tato funkce vrátí 1, tak je daná zpráva "zahozena". Pomocí tohoto postupu můžete určité klávesy úplně zablokovat. Náš příklad zablokuje možnost použití klávesy 'D'. Po spuštění této aplikace uvidíte, že Vám přestane úplně fungovat klávesa 'D'.
public class KeyboardHook
{
private IntPtr hook;
private IntPtr lib;
public KeyboardHook()
{
lib = api.LoadLibrary("kernel32.dll"); //nahrajeme modul kernel32.dll
hook = api.SetWindowsHookEx(api.WH_KEYBOARD_LL, MyCallbackFunction, lib, 0); //nastavíme hook
}
~KeyboardHook()
{
api.UnhookWindowsHookEx(hook); //odebereme hook
api.FreeLibrary(lib); //uvolníme z paměti nahraný modul
}
//tato metoda bude volána po každém stisku klávesy
private IntPtr MyCallbackFunction(int code, uint wParam, ref api.KeyboardHookStruct lParam)
{
if (wParam == api.WM_KEYFIRST || wParam == api.WM_SYSKEYDOWN) //pouze při stisku
{
if (KeyPressed != null) KeyPressed((int)lParam.vkCode);
}
if (lParam.vkCode == 68) return new IntPtr(1); //pokud stiskneme 'D', tak zpráva bude zahozena
return api.CallNextHookEx(hook, code, wParam, lParam); //zajistí že pokud někde byly nastaveny ještě další hooky, tak se k nim zpráva dostane
}
}
Simulace stisku klávesy
Simulování stisku kláves je na jednu stranu jednoduší, ale bohužel je tu problém s již zmiňovanou kompatibilitou 32 a 64 bitových operačních systémů. Naštěstí lze programově velmi snadno zjistit pod kterou z uvedených platforem zrovna pracujeme, takže vše bude ošetřeno interně v naší třídě a programátor, který ji bude použivat se už o nic nebude muset starat. Naše simulace bude opět fungovat na globální úrovni. To znaméná, že se to bude chovat jako kdybychom skutečně mačkali tlačítka na klávesnici.
Opět začneme deklaracemi jednotlivých Win API funkcí a struktur, které budeme potřebovat. Můžete si povšimnout že rozdíl mezi strukturami pro 32 a 64 bitové OS je v jejich velikosti a atributu FieldOffset. 64 bitové struktury jsou o 4 byty větší protože obsahují jednu strukturu IntPtr, jejíž velikost zavisí právě na aktuální platformě.
static class api
{
public const int INPUT_KEYBOARD = 1;
public const uint KEYEVENTF_KEYUP = 0x0002;
[DllImport("user32.dll", SetLastError = true)]
public static extern uint SendInput(uint nInputs, Input_32bitSyst[] pInputs, int cbSize);
[DllImport("user32.dll", SetLastError = true)]
public static extern uint SendInput(uint nInputs, Input_64bitSyst[] pInputs, int cbSize);
[StructLayout(LayoutKind.Explicit)]
public struct Input_32bitSyst
{
[FieldOffset(0)]
public int type;
[FieldOffset(4)]
public KeyboardInput_32bitSyst ki;
}
[StructLayout(LayoutKind.Sequential, Size = 24)]
public struct KeyboardInput_32bitSyst
{
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Explicit)]
public struct Input_64bitSyst
{
[FieldOffset(0)]
public int type;
[FieldOffset(8)]
public KeyboardInput_64bitSyst ki;
}
[StructLayout(LayoutKind.Sequential, Size = 28)]
public struct KeyboardInput_64bitSyst
{
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
}
Nyní přestoupíme k zapouzdření Win API do vysokoúrovňovější třídy. Nejdříve ale k tomu jak poznáme pod jakou platformou pracujeme. K tomu využijeme struktury IntPtr reprezentující ukazatel na paměť. Víme, že 32 bitové OS adresují paměť pomocí 32 bitů, to znamená, že struktura IntPtr bude mít velikost 4 byty. A analogicky na 64 bitových OS bude mít struktura 8 bytů. Situaci nám činí ještě jednodušší skutečnost, že struktura IntPtr implementuje vlastnost Size, která nabývá pravě hodnot 4 nebo 8.
public static class KeyboardSimulation
{
public static void Simulate(params ushort[] keys)
{
if (IntPtr.Size == 4)
{
//32 bitová verze
api.Input_32bitSyst[] input = new api.Input_32bitSyst[keys.Length]; //pole kódů kláves
for (int i = 0; i < keys.Length; i++)
{
input[i].type = api.INPUT_KEYBOARD; //jde o stisknutí
input[i].ki.wVk = keys[i]; //o kterou klávesu se jedná
}
api.SendInput(3, input, Marshal.SizeOf(typeof(api.Input_32bitSyst))); //odešleme zprávu o stisknutí všech kláves v poli
for (int i = 0; i < keys.Length; i++)
{
input[i].ki.dwFlags = api.KEYEVENTF_KEYUP; //jde o zvednutí klávesy
}
api.SendInput(3, input, Marshal.SizeOf(typeof(api.Input_32bitSyst))); //odešleme zprávu o zvednutí všech kláves v poli
}
else
{
//64 bitová verze
api.Input_64bitSyst[] input = new api.Input_64bitSyst[keys.Length]; //pole kódů kláves
for (int i = 0; i < keys.Length; i++)
{
input[i].type = api.INPUT_KEYBOARD; //jde o stisknutí
input[i].ki.wVk = keys[i]; //o kterou klávesu se jedná
}
api.SendInput(3, input, Marshal.SizeOf(typeof(api.Input_64bitSyst))); //odešleme zprávu o stisknutí všech kláves v poli
for (int i = 0; i < keys.Length; i++)
{
input[i].ki.dwFlags = api.KEYEVENTF_KEYUP; //jde o zvednutí klávesy
}
api.SendInput(3, input, Marshal.SizeOf(typeof(api.Input_64bitSyst))); //odešleme zprávu o zvednutí všech kláves v poli
}
}
Kód pro 32 i 64 bitovou platformu je stejný pouze s tím rozdílem, že používáme jinou strukturu pro uchování dat o stisknuté klávese. Metoda Simulate přebírá jako parametr pole kódů jednotlivých kláves.
Použití vytvořených tříd
Nejdříve si ukážeme program pro odchytávání stisků kláves a jejich výpis do konzole. (Musíme kompilovat jako konzolovou aplikaci, abychom viděli kozoli.)
class Program
{
static void Main()
{
KeyboardHook hook = new KeyboardHook(); //vytvoříme instanci naší třídy
hook.KeyPressed += new KeyboardHook.HookHandler(hook_KeyPressed); //inicializujeme událost po stisknutí tlačítk
System.Windows.Forms.Application.Run(); //spustíme program
}
static void hook_KeyPressed(int key)
{
Console.WriteLine(key);
}
}
A nakonec program, který po spuštění vyvolá aplikaci Správce úloh pomocí simulace klávesové zkratky Ctrl+Shift+Esc. 162=Ctrl, 160=Shift, 27=Ecs
class Program
{
static void Main()
{
KeyboardSimulation.Simulate(162, 160, 27);
}
}
Hodnoty kódů jednotlivých kláves můžeme zjistit pomocí předchozí aplikace.
Jak můžete vidět, použití tříd KeyboardHook a KeyboardSimulation je naprosto jednoduché a elegantní. Bohužel při testování jsem zjistil, že pokud jste uživately Windows Vista tak tyto aplikace musíte spouštět jako administrátor. Testoval jsem na Windows Vista 64bit, Windows Vista 32bit a Windows XP 32bit.
Ke stažení projekt pro VS 2008 Express: