Introduction
The KeyboardListener
class presented here allows capturing keyboard events even when another application is running in the foreground. Several threads inside your application might be listening for events in parallel, each handling the events in its own way.
In case you are only looking for a means to add hotkey support to your application, you might also want to take a look at the CodeProject article '.NET System wide hotkey component'. However, if you have other plans with respect to handling keyboard events inside your application, this KeyboardListener
class might be the one you're looking for.
The KeyboardListener
class can only be used on Windows XP because its implementation depends on the Win32 methods RegisterRawInputDevices
and GetRawInputData
which are currently only available on XP.
Using the code
1. The demo application
Let's start with the easy part: using the class inside an application. The demo application is a plain .NET application containing only one form that in turn only contains one text label (see demo project). The implementation of the Form1_Load
method that is called whenever the form is loaded only adds a new EventHandler
to the static s_KeyEventHandler
event exported by the KeyboardListener
class. Of course, you are free to add multiple EventHandler
instances on several locations inside your application.
private void Form1_Load(object sender, System.EventArgs e)
{
KeyboardListener.s_KeyEventHandler += new
EventHandler(KeyboardListener_s_KeyEventHandler);
}
The KeyboardListener_s_KeyEventHandler
method that is passed to the EventHandler
constructor analyzes the arguments, updates the text label lblKeyPressed
on the form with some key information, and changes the back color of the form depending on the action of the key: when a key is pressed the background becomes red; when it is released, it becomes green again.
private void KeyboardListener_s_KeyEventHandler(object sender, EventArgs e)
{
KeyboardListener.UniversalKeyEventArgs eventArgs =
(KeyboardListener.UniversalKeyEventArgs)e;
lblKeyPressed.Text = string.Format("Key = {0} Msg = {1} Text = {2}",
eventArgs.m_Key, eventArgs.m_Msg, eventArgs.KeyData);
if(eventArgs.m_Msg == 256)
{
this.BackColor = Color.Red;
}
else
{
this.BackColor = Color.Green;
}
}
2. The KeyboardListener class
The KeyboardListener
class is made up of three important parts. First, there is the already mentioned static EventHandler
variable s_KeyEventHandler
. Every application thread that is interested to receive keyboard events can subscribe to this event.
public static event EventHandler s_KeyEventHandler;
Second, there is the static KeyHandler
method that is called whenever a keyboard event occurs. It loops across all EventHandler
instances contained by s_KeyEventHandler
calling each one with a UniversalKeyEventArgs
instance that holds the key and message provided by the keyboard event. The advantage of looping across all delegates is that by adding a try
/catch
block to the loop, an EventHandler
can fail while all other EventHandler
instances still get called. In other words, one subscriber that fails doesn't prevent other subscribers from being called.
private static void KeyHandler(ushort key, uint msg)
{
if(s_KeyEventHandler != null)
{
Delegate[] delegates = s_KeyEventHandler.GetInvocationList();
foreach(Delegate del in delegates)
{
EventHandler sink = (EventHandler)del;
try
{
sink(null,new UniversalKeyEventArgs(key,msg));
}
catch{};
}
}
}
Last, there is the ListeningWindow
class. As the name might suggest, an instance of such a class is used to listen. It listens for keyboard events to happen. In fact, there is only one instance of this class. This instance is created when the static constructor for the KeyboardListener
class is called. Looking at the ListeningWindow
constructor below, you see that it needs a delegate when it gets constructed. This delegate happens to be the KeyHandler
mentioned above. So, while the application is running, whenever the ListeningWindow
finds out about a keyboard event, it calls the KeyHandler
delegate that in turn calls all installed EventHandler
s.
public ListeningWindow(KeyDelegate keyHandlerFunction)
{
m_KeyHandler = keyHandlerFunction;
CreateParams cp = new CreateParams();
cp.Caption = "Hidden window";
cp.ClassName = null;
cp.X = 0x7FFFFFFF;
cp.Y = 0x7FFFFFFF;
cp.Height = 0;
cp.Width = 0;
cp.Style = WS_CLIPCHILDREN;
this.CreateHandle(cp);
unsafe
{
try
{
RAWINPUTDEV myRawDevice = new RAWINPUTDEV();
myRawDevice.usUsagePage = 0x01;
myRawDevice.usUsage = 0x06;
myRawDevice.dwFlags = RIDEV_INPUTSINK;
myRawDevice.hwndTarget = this.Handle.ToPointer();
if (RegisterRawInputDevices(&myRawDevice, 1,
(uint)sizeof(RAWINPUTDEV)) == false)
{
int err = Marshal.GetLastWin32Error();
throw new Win32Exception(err,
"ListeningWindow::RegisterRawInputDevices");
}
}
catch {throw;}
}
}
The ListeningWindow
class, which is a subclass of the NativeWindow
class, only contains two interesting definitions: the constructor and the WndProc
method.
Inside the ListeningWindow
constructor, an invisible window is created after which it is registered to receive keyboard input messages. These messages are analyzed by the WndProc
method that is overriding the NativeWindow
base class method. Whenever the WndProc
method receives a keyboard message, it checks whether it differs from the previous message and if so, it calls the delegate installed inside the ListeningWindow
constructor.
protected override void WndProc(ref Message m)
{
...
receivedBytes =
(uint)GetRawInputData((RAWINPUTHKEYBOARD*)(m.LParam.ToPointer()),
RID_INPUT, lpb, &dwSize, sizeof_RAWINPUTHEADER);
if ( receivedBytes == dwSize )
{
RAWINPUTHKEYBOARD* keybData = (RAWINPUTHKEYBOARD*)lpb;
if(keybData->header.dwType == RIM_TYPEKEYBOARD)
{
if((m_PrevControlKey != keybData->VKey) ||
(m_PrevMessage != keybData->Message))
{
m_PrevControlKey = keybData->VKey;
m_PrevMessage = keybData->Message;
m_KeyHandler(keybData->VKey,keybData->Message);
}
}
}
...
}
Using PInvoke
The two methods that are key to the KeyboardListener
class implementation are the RegisterRawInputDevices
and the GetRawInputData
methods. They enable capturing and handling keyboard events in the background. Since these calls are not supported by .NET, they are called via the PInvoke mechanism.
[DllImport("User32.dll",CharSet = CharSet.Ansi,SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]
internal static extern unsafe bool
RegisterRawInputDevices( RAWINPUTDEV* rawInputDevices,
uint numDevices, uint size);
[DllImport("User32.dll",CharSet = CharSet.Ansi,SetLastError=true)]
[return : MarshalAs(UnmanagedType.I4)]
internal static extern unsafe int GetRawInputData( void* hRawInput,
uint uiCommand, byte* pData, uint* pcbSize, uint cbSizeHeader);
Future ideas
- The
UniversalKeyEventArgs
is a sub-class of the EventArgs
class and you might ask why we need it. Well, frankly, you can do without. But then you will have to assemble the correct Keys
instance to pass as a parameter to the EventArgs
constructor. In this release, I went for an easy way out that is certainly something that can be improved in a future release.
- Using the same technology, a
MouseListener
class can be created. See my next article.
History
- 20 January 2005, Version 1.0.0, Initial release.