Возможно, вы когда-то очень хотели чтобы в вашем приложении присутствовала возможность управления чем-либо через глобальные клавиши. И, возможно, вам нравится программировать с использованием технологии WPF. Тогда этот топик для вас.
Для решения проблемы стоит уяснить как работает механизм горячих клавиш в Windows, поскольку методы WPF, работающие с ними напрямую, отсутствуют. Поэтому нам понадобится обращаться к WinAPI.
Нам понадобятся приведённые ниже функции.
Регистрация хоткея:
Удаление хоткея:
Регистрация уникальной строки для идентификации хоткея и получение её идентификатора (атома):
И, соответственно, удаление атома:
Собственно, сам механизм достаточно прост — регистрируем строку идентификации хоткея и при помощи полученного атома регистрируем сам хоткей. При выходе из приложения удаляем регистрацию хоткея и атома — всё. Как видите, очень просто. Теперь перейдём к реализации.
В С# экспортируем эти функции из соответствующих dll:
Тут появляется одна небольшая проблема — обработка вызовов WndProc. Дело в том, что в WPF, в отличие от Windows Forms, нельзя просто перегрузить эту функцию в окне приложения. Но можно всё же обработать вызовы WndProc следующим образом:
Мы полностью готовы к регистрации горячих клавиш. Добавим в наш класс, например, такой метод:
И реализацию WndProc:
Теперь, лёгким движением руки, мы можем наконец-то что-нибудь зарегистрировать:
Не спешите радоваться, в конце жизни приложения лучше бы удалить регистрацию наших хоткеев дабы они вдруг не помешали другим приложениям. Как сказано выше, этот процесс выполняется при помощи функций UnregisterHotKey и GlobalDeleteAtom. В нашей реализации это можно сделать следующим образом:
Всё, можно радоваться — всё реализовано и работает.
UPD: имеются в виду «хоткеи» глобальные на уровне ОС, а не на уровне приложения
UPD2:
Cпасибо Karabasoff за важное дополнение:
При использовании RegisterHotKey и GlobalAddAtom начинаются проблемы как только приложение начинает жить только в виде маленькой иконки глубоко в трее. В таком случае спасают только хуки.
Для решения проблемы стоит уяснить как работает механизм горячих клавиш в Windows, поскольку методы WPF, работающие с ними напрямую, отсутствуют. Поэтому нам понадобится обращаться к WinAPI.
Нам понадобятся приведённые ниже функции.
Регистрация хоткея:
BOOL WINAPI RegisterHotKey(
__in_opt HWND hWnd,
__in int id,
__in UINT fsModifiers,
__in UINT vk
);
Удаление хоткея:
BOOL WINAPI UnregisterHotKey(
__in_opt HWND hWnd,
__in int id
);
Регистрация уникальной строки для идентификации хоткея и получение её идентификатора (атома):
ATOM GlobalAddAtom(
LPCTSTR lpString
);
И, соответственно, удаление атома:
ATOM WINAPI GlobalDeleteAtom(
__in ATOM nAtom
);
Собственно, сам механизм достаточно прост — регистрируем строку идентификации хоткея и при помощи полученного атома регистрируем сам хоткей. При выходе из приложения удаляем регистрацию хоткея и атома — всё. Как видите, очень просто. Теперь перейдём к реализации.
В С# экспортируем эти функции из соответствующих dll:
[DllImport("User32.dll")]
public static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
[DllImport("User32.dll")]
public static extern bool UnregisterHotKey(IntPtr hWnd, int id);
[DllImport("kernel32.dll")]
public static extern Int16 GlobalAddAtom(string name);
[DllImport("kernel32.dll")]
public static extern Int16 GlobalDeleteAtom(Int16 nAtom);
Тут появляется одна небольшая проблема — обработка вызовов WndProc. Дело в том, что в WPF, в отличие от Windows Forms, нельзя просто перегрузить эту функцию в окне приложения. Но можно всё же обработать вызовы WndProc следующим образом:
public HotkeysRegistrator(Window window)
{
_windowHandle = new WindowInteropHelper(window).Handle;
HwndSource source = HwndSource.FromHwnd(_windowHandle);
source.AddHook(WndProc);
}
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam, ref bool handled)
{
if (msg == 0x0312)
{
//Обработка горячей клавиши
}
return IntPtr.Zero;
}
Мы полностью готовы к регистрации горячих клавиш. Добавим в наш класс, например, такой метод:
private Dictionary<Int16, Action> _globalActions = new Dictionary<short, Action>();
public bool RegisterGlobalHotkey(Action action, Keys commonKey, params ModifierKeys[] keys)
{
uint mod = keys.Cast<uint>().Aggregate((current, modKey) => current | modKey);
short atom = GlobalAddAtom("OurAmazingApp" + (_globalActions.Count + 1));
bool status = RegisterHotKey(_windowHandle, atom, mod, (uint)commonKey);
if (status)
{
_globalActions.Add(atom, action);
}
return status;
}
И реализацию WndProc:
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam, ref bool handled)
{
if (msg == 0x0312)
{
short atom = Int16.Parse(wparam.ToString());
if (_globalActions.ContainsKey(atom))
{
_globalActions[atom]();
}
}
return IntPtr.Zero;
}
Теперь, лёгким движением руки, мы можем наконец-то что-нибудь зарегистрировать:
RegisterGlobalHotkey(() => MessageBox.Show("Урааааааааааа!"), Keys.G, ModifierKeys.Alt, ModifierKeys.Control);
Не спешите радоваться, в конце жизни приложения лучше бы удалить регистрацию наших хоткеев дабы они вдруг не помешали другим приложениям. Как сказано выше, этот процесс выполняется при помощи функций UnregisterHotKey и GlobalDeleteAtom. В нашей реализации это можно сделать следующим образом:
public void UnregisterHotkeys()
{
foreach(var atom in _globalActions.Keys)
{
UnregisterHotKey(_windowHandle, atom);
GlobalDeleteAtom(atom);
}
}
Всё, можно радоваться — всё реализовано и работает.
UPD: имеются в виду «хоткеи» глобальные на уровне ОС, а не на уровне приложения
UPD2:
Cпасибо Karabasoff за важное дополнение:
При использовании RegisterHotKey и GlobalAddAtom начинаются проблемы как только приложение начинает жить только в виде маленькой иконки глубоко в трее. В таком случае спасают только хуки.
[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)]
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);
// запуск отлова
private static IntPtr SetHook(LowLevelKeyboardProc proc)
{
using (var curProcess = Process.GetCurrentProcess())
{
using (var curModule = curProcess.MainModule)
{
return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
}
}
}
// отлов и, при необходимости, обработка хоткея
private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
{
var vkCode = (Keys)Marshal.ReadInt32(lParam);
switch (vkCode)
{
case Keys.MediaNextTrack:
{
break;
}
}
}
return CallNextHookEx(hookId, nCode, wParam, lParam);
}