Возможно, вы когда-то очень хотели чтобы в вашем приложении присутствовала возможность управления чем-либо через глобальные клавиши. И, возможно, вам нравится программировать с использованием технологии 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); }
