This
article will show how to register a system hotkey for a currently running
application and how to handle window messages for altering controls
functionality or adding new functions to them.
So
far C# has no native lib functions to do that, so Platform Invoke will be
required to use some of WinAPI functions from user32.dll and kernel32.dll.
Registering
a system wide hotkey enables application to respond to that hotkey even if
application has no focus or is minimalized. This can be console window or
hidden form, important is only to be able to obtain a handle to it.
Window
messages are events passed by the system to the running applications. Every
class derived from Control class have a WndProc() function which give an
ability to process messages (this is handled automatically but we can override
it).
First
of all we have to port win32 functions to C# managed code with Platform Invoke
using DllImport attribute.
public static class Win32
{
[DllImport("kernel32.dll")]
public static extern int
GlobalAddAtom(
string lpString
);
[DllImport("kernel32.dll")]
public static extern int GlobalDeleteAtom(
int nAtom
);
[DllImport("User32")]
public static extern IntPtr
SetForegroundWindow(
IntPtr hwnd
);
[DllImport("User32")]
public static extern bool
RegisterHotKey(
IntPtr hWnd,
int id,
int fsModifiers,
int vk
);
[DllImport("User32")]
public static extern bool
UnregisterHotKey(
IntPtr hWnd,
int id
);
[DllImport("User32")]
public static extern IntPtr
GetForegroundWindow();
[DllImport("user32")]
public static extern IntPtr
FindWindow(
string lpClassName,
string lpWindowName
);
[DllImport("user32")]
public static extern int
SendMessage(
IntPtr hWnd,
uint Msg,
int wParam,
int lParam
);
public const int MOD_SHIFT = 0x4;
public const int MOD_CONTROL = 0x2;
public const int MOD_ALT = 0x1;
public const int MOD_WIN = 0x8;
public const int WM_HOTKEY = 0x312;
public const int WM_MOUSEWHEEL = 0x020A;
public const int WM_PASTE = 0x0302;
}
To
register a hotkey use RegisterHotKey() function. It has 4 parameters: a handle
to the window that will receive WM_HOTKEY messages, the identifier, key
modifiers and virtual-key code of the hotkey. An application must specify an id
value in the range 0x0000 through 0xBFFF. A shared DLL must specify a value in
the range 0xC000 through 0xFFFF. It is generally good to use GlobalAddAtom()
function to generate unique id from the correct range.
private List<int> lHKids; // list of
unique ids for newly registered hotkeys
this_window_handle = this.Handle;
//this_window_handle
= Win32.FindWindow(null, this.Text); // another way
// shift-ctrl-q hotkey
int uid = Win32.GlobalAddAtom((lHKids.Count
+ 1).ToString());
Win32.RegisterHotKey(
this_window_handle,
uid,
Win32.MOD_SHIFT | Win32.MOD_CONTROL,
Convert.ToInt16(Keys.Q)
);
lHKids.Add(uid);
// q hotkey
uid = Win32.GlobalAddAtom((lHKids.Count
+ 1).ToString());
Win32.RegisterHotKey(
this_window_handle,
uid,
0,
Convert.ToInt16(Keys.Q)
);
lHKids.Add(uid);
// ctrl-v hotkey
uid = Win32.GlobalAddAtom((lHKids.Count
+ 1).ToString());
Win32.RegisterHotKey(
this_window_handle,
uid,
Win32.MOD_CONTROL,
Convert.ToInt16(Keys.V)
);
lHKids.Add(uid);
To
unregister hotkey call UnregisterHotKey() function:
Win32.UnregisterHotKey(this_window_handle, lHKids[i]);
Win32.GlobalDeleteAtom(lHKids[i]);
When
hotkey is defined and key is pressed the system posts the WM_HOTKEY message to
the message queue of the window with which the hotkey is associated. Override
WndProc() to handle it.
protected override void WndProc(ref Message m)
{
try
{
// handle WM_HOTKEY message and hotkeys
if ((lHKids != null)
&& (lHKids.Count > 0) && (m.Msg == Win32.WM_HOTKEY))
{
// WParam for WM_HOTKEY message holds id passed to
// RegisterHotKey() function
int idx = lHKids.IndexOf((int)m.WParam);
if (idx >= 0 && (idx != 2)) // without ctrl-v hotkey
{
Win32.SetForegroundWindow(this_window_handle);
MessageBox.Show(string.Format("HotKey {0} pressed", idx), "info");
}
}
// respond to WM_MOUSEWHEEL message
else if (m.Msg == Win32.WM_MOUSEWHEEL)
{
// use the correct control handle
// in this case textbox instead of main form window
Win32.SendMessage(exTextBox1.Handle, Win32.WM_PASTE, 0, 0);
}
}
catch (Exception
ex)
{
MessageBox.Show(ex.Message, "WndProc() exception");
}
base.WndProc(ref m);
}
The
example app code is registering 3 hotkeys: ctrl-shift-q, q itself and ctrl-v to
show different calls of RegisterHotKey() function and some possibilities of
what we can do with them. I.e. we can block ctrl-v paste capability (just this
specific keys combination).
The
second message filtered in this example is WM_MOUSEWHEEL. Here it is used to
send WM_PASTE message (so we cannot use ctrl-v but mouse wheel scroll will
work) to the modified textbox control. Every control has its own handle so it
is important to use the right one when we want to send messages to it.
Controls
(and window forms) are receiving only messages that have a meaning for them that
is why in this example WM_PASTE must be filtered in TextBox's WndProc() instead
of main form's one:
class ExTextBox : TextBox
{
protected override void WndProc(ref Message m)
{
// Window messages filtering
if (m.Msg == Win32.WM_PASTE)
{
string s = "WM_PASTE
message received by TextBox control";
if (!Clipboard.ContainsText())
s += "\n- clipboard data is not a text";
MessageBox.Show(s, "info");
// Uncomment the "return" line to block paste
receiving capability
// for this control regardless of its source.
//return;
}
base.WndProc(ref m);
}
}
We
can use messages to restrict some behavior of the control or implement new one
like checking checkbox with mouse scroll and so on but unlike global hotkeys,
this modification will work only when window has a focus.
See
complete example attached.
Some
useful links: