Как стать автором
Обновить

Регистрация глобальных «хоткеев» при использовании WPF

Время на прочтение4 мин
Количество просмотров6K
Возможно, вы когда-то очень хотели чтобы в вашем приложении присутствовала возможность управления чем-либо через глобальные клавиши. И, возможно, вам нравится программировать с использованием технологии WPF. Тогда этот топик для вас.


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

Теги:
Хабы:
Всего голосов 43: ↑37 и ↓6+31
Комментарии14

Публикации

Истории

Работа

.NET разработчик
68 вакансий

Ближайшие события