Глобальный захват клавиатуры в приложении C#
Я хочу перехватить комбинацию клавиш в моем приложении и вызвать диалоговое окно, которое появляется, если пользователь нажимает комбинацию клавиш даже вне приложения. Похоже на Ctrl Google Desktop, Ctrl, чтобы вызвать диалоговое окно поиска.
Я пытался использовать некоторые модули для подключения клавиатуры, которые в основном используют взаимодействие Win32 для получения этого эффекта, но каждая реализация, которую я пробовал, в некоторой степени связывает клавиатуру с тем, где вы начинаете получать странные поведения, когда приложение делает что-то интенсивное. Например, при загрузке большого количества данных это может привести к блокировке клавиатуры и мыши.
Я ищу легкое решение, которое позволило бы сделать это, не связывая клавиатуру и мышь.
7 ответов
Стивен Туб написал отличную статью о реализации глобальных хуков клавиатуры в C#:
using System;
using System.Diagnostics;
using System.Windows.Forms;
using System.Runtime.InteropServices;
class InterceptKeys
{
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 0x0100;
private static LowLevelKeyboardProc _proc = HookCallback;
private static IntPtr _hookID = IntPtr.Zero;
public static void Main()
{
_hookID = SetHook(_proc);
Application.Run();
UnhookWindowsHookEx(_hookID);
}
private static IntPtr SetHook(LowLevelKeyboardProc proc)
{
using (Process curProcess = Process.GetCurrentProcess())
using (ProcessModule curModule = curProcess.MainModule)
{
return SetWindowsHookEx(WH_KEYBOARD_LL, proc,
GetModuleHandle(curModule.ModuleName), 0);
}
}
private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
{
int vkCode = Marshal.ReadInt32(lParam);
Console.WriteLine((Keys)vkCode);
}
return CallNextHookEx(_hookID, nCode, wParam, lParam);
}
[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)]
[return: MarshalAs(UnmanagedType.Bool)]
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);
}
Вот мой код, который работает:
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace SnagFree.TrayApp.Core
{
class GlobalKeyboardHookEventArgs : HandledEventArgs
{
public GlobalKeyboardHook.KeyboardState KeyboardState { get; private set; }
public GlobalKeyboardHook.LowLevelKeyboardInputEvent KeyboardData { get; private set; }
public GlobalKeyboardHookEventArgs(
GlobalKeyboardHook.LowLevelKeyboardInputEvent keyboardData,
GlobalKeyboardHook.KeyboardState keyboardState)
{
KeyboardData = keyboardData;
KeyboardState = keyboardState;
}
}
//Based on https://gist.github.com/Stasonix
class GlobalKeyboardHook : IDisposable
{
public event EventHandler<GlobalKeyboardHookEventArgs> KeyboardPressed;
public GlobalKeyboardHook()
{
_windowsHookHandle = IntPtr.Zero;
_user32LibraryHandle = IntPtr.Zero;
_hookProc = LowLevelKeyboardProc; // we must keep alive _hookProc, because GC is not aware about SetWindowsHookEx behaviour.
_user32LibraryHandle = LoadLibrary("User32");
if (_user32LibraryHandle == IntPtr.Zero)
{
int errorCode = Marshal.GetLastWin32Error();
throw new Win32Exception(errorCode, $"Failed to load library 'User32.dll'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
}
_windowsHookHandle = SetWindowsHookEx(WH_KEYBOARD_LL, _hookProc, _user32LibraryHandle, 0);
if (_windowsHookHandle == IntPtr.Zero)
{
int errorCode = Marshal.GetLastWin32Error();
throw new Win32Exception(errorCode, $"Failed to adjust keyboard hooks for '{Process.GetCurrentProcess().ProcessName}'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
}
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// because we can unhook only in the same thread, not in garbage collector thread
if (_windowsHookHandle != IntPtr.Zero)
{
if (!UnhookWindowsHookEx(_windowsHookHandle))
{
int errorCode = Marshal.GetLastWin32Error();
throw new Win32Exception(errorCode, $"Failed to remove keyboard hooks for '{Process.GetCurrentProcess().ProcessName}'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
}
_windowsHookHandle = IntPtr.Zero;
// ReSharper disable once DelegateSubtraction
_hookProc -= LowLevelKeyboardProc;
}
}
if (_user32LibraryHandle != IntPtr.Zero)
{
if (!FreeLibrary(_user32LibraryHandle)) // reduces reference to library by 1.
{
int errorCode = Marshal.GetLastWin32Error();
throw new Win32Exception(errorCode, $"Failed to unload library 'User32.dll'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
}
_user32LibraryHandle = IntPtr.Zero;
}
}
~GlobalKeyboardHook()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private IntPtr _windowsHookHandle;
private IntPtr _user32LibraryHandle;
private HookProc _hookProc;
delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll")]
private static extern IntPtr LoadLibrary(string lpFileName);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
private static extern bool FreeLibrary(IntPtr hModule);
/// <summary>
/// The SetWindowsHookEx function installs an application-defined hook procedure into a hook chain.
/// You would install a hook procedure to monitor the system for certain types of events. These events are
/// associated either with a specific thread or with all threads in the same desktop as the calling thread.
/// </summary>
/// <param name="idHook">hook type</param>
/// <param name="lpfn">hook procedure</param>
/// <param name="hMod">handle to application instance</param>
/// <param name="dwThreadId">thread identifier</param>
/// <returns>If the function succeeds, the return value is the handle to the hook procedure.</returns>
[DllImport("USER32", SetLastError = true)]
static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId);
/// <summary>
/// The UnhookWindowsHookEx function removes a hook procedure installed in a hook chain by the SetWindowsHookEx function.
/// </summary>
/// <param name="hhk">handle to hook procedure</param>
/// <returns>If the function succeeds, the return value is true.</returns>
[DllImport("USER32", SetLastError = true)]
public static extern bool UnhookWindowsHookEx(IntPtr hHook);
/// <summary>
/// The CallNextHookEx function passes the hook information to the next hook procedure in the current hook chain.
/// A hook procedure can call this function either before or after processing the hook information.
/// </summary>
/// <param name="hHook">handle to current hook</param>
/// <param name="code">hook code passed to hook procedure</param>
/// <param name="wParam">value passed to hook procedure</param>
/// <param name="lParam">value passed to hook procedure</param>
/// <returns>If the function succeeds, the return value is true.</returns>
[DllImport("USER32", SetLastError = true)]
static extern IntPtr CallNextHookEx(IntPtr hHook, int code, IntPtr wParam, IntPtr lParam);
[StructLayout(LayoutKind.Sequential)]
public struct LowLevelKeyboardInputEvent
{
/// <summary>
/// A virtual-key code. The code must be a value in the range 1 to 254.
/// </summary>
public int VirtualCode;
/// <summary>
/// A hardware scan code for the key.
/// </summary>
public int HardwareScanCode;
/// <summary>
/// The extended-key flag, event-injected Flags, context code, and transition-state flag. This member is specified as follows. An application can use the following values to test the keystroke Flags. Testing LLKHF_INJECTED (bit 4) will tell you whether the event was injected. If it was, then testing LLKHF_LOWER_IL_INJECTED (bit 1) will tell you whether or not the event was injected from a process running at lower integrity level.
/// </summary>
public int Flags;
/// <summary>
/// The time stamp stamp for this message, equivalent to what GetMessageTime would return for this message.
/// </summary>
public int TimeStamp;
/// <summary>
/// Additional information associated with the message.
/// </summary>
public IntPtr AdditionalInformation;
}
public const int WH_KEYBOARD_LL = 13;
//const int HC_ACTION = 0;
public enum KeyboardState
{
KeyDown = 0x0100,
KeyUp = 0x0101,
SysKeyDown = 0x0104,
SysKeyUp = 0x0105
}
public const int VkSnapshot = 0x2c;
//const int VkLwin = 0x5b;
//const int VkRwin = 0x5c;
//const int VkTab = 0x09;
//const int VkEscape = 0x18;
//const int VkControl = 0x11;
const int KfAltdown = 0x2000;
public const int LlkhfAltdown = (KfAltdown >> 8);
public IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam)
{
bool fEatKeyStroke = false;
var wparamTyped = wParam.ToInt32();
if (Enum.IsDefined(typeof(KeyboardState), wparamTyped))
{
object o = Marshal.PtrToStructure(lParam, typeof(LowLevelKeyboardInputEvent));
LowLevelKeyboardInputEvent p = (LowLevelKeyboardInputEvent)o;
var eventArguments = new GlobalKeyboardHookEventArgs(p, (KeyboardState)wparamTyped);
EventHandler<GlobalKeyboardHookEventArgs> handler = KeyboardPressed;
handler?.Invoke(this, eventArguments);
fEatKeyStroke = eventArguments.Handled;
}
return fEatKeyStroke ? (IntPtr)1 : CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
}
}
}
Использование:
using System;
using System.Windows.Forms;
namespace SnagFree.TrayApp.Core
{
internal class Controller : IDisposable
{
private GlobalKeyboardHook _globalKeyboardHook;
public void SetupKeyboardHooks()
{
_globalKeyboardHook = new GlobalKeyboardHook();
_globalKeyboardHook.KeyboardPressed += OnKeyPressed;
}
private void OnKeyPressed(object sender, GlobalKeyboardHookEventArgs e)
{
//Debug.WriteLine(e.KeyboardData.VirtualCode);
if (e.KeyboardData.VirtualCode != GlobalKeyboardHook.VkSnapshot)
return;
// seems, not needed in the life.
//if (e.KeyboardState == GlobalKeyboardHook.KeyboardState.SysKeyDown &&
// e.KeyboardData.Flags == GlobalKeyboardHook.LlkhfAltdown)
//{
// MessageBox.Show("Alt + Print Screen");
// e.Handled = true;
//}
//else
if (e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyDown)
{
MessageBox.Show("Print Screen");
e.Handled = true;
}
}
public void Dispose()
{
_globalKeyboardHook?.Dispose();
}
}
}
По запросу dube выкладываю модифицированный вариант ответа Siarhei Kuchuk.
Если вы хотите проверить мои изменения, найдите// EDT
. Я прокомментировал большинство из них.
Установка
class GlobalKeyboardHookEventArgs : HandledEventArgs
{
public GlobalKeyboardHook.KeyboardState KeyboardState { get; private set; }
public GlobalKeyboardHook.LowLevelKeyboardInputEvent KeyboardData { get; private set; }
public GlobalKeyboardHookEventArgs(
GlobalKeyboardHook.LowLevelKeyboardInputEvent keyboardData,
GlobalKeyboardHook.KeyboardState keyboardState)
{
KeyboardData = keyboardData;
KeyboardState = keyboardState;
}
}
//Based on https://gist.github.com/Stasonix
class GlobalKeyboardHook : IDisposable
{
public event EventHandler<GlobalKeyboardHookEventArgs> KeyboardPressed;
// EDT: Added an optional parameter (registeredKeys) that accepts keys to restict
// the logging mechanism.
/// <summary>
///
/// </summary>
/// <param name="registeredKeys">Keys that should trigger logging. Pass null for full logging.</param>
public GlobalKeyboardHook(Keys[] registeredKeys = null)
{
RegisteredKeys = registeredKeys;
_windowsHookHandle = IntPtr.Zero;
_user32LibraryHandle = IntPtr.Zero;
_hookProc = LowLevelKeyboardProc; // we must keep alive _hookProc, because GC is not aware about SetWindowsHookEx behaviour.
_user32LibraryHandle = LoadLibrary("User32");
if (_user32LibraryHandle == IntPtr.Zero)
{
int errorCode = Marshal.GetLastWin32Error();
throw new Win32Exception(errorCode, $"Failed to load library 'User32.dll'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
}
_windowsHookHandle = SetWindowsHookEx(WH_KEYBOARD_LL, _hookProc, _user32LibraryHandle, 0);
if (_windowsHookHandle == IntPtr.Zero)
{
int errorCode = Marshal.GetLastWin32Error();
throw new Win32Exception(errorCode, $"Failed to adjust keyboard hooks for '{Process.GetCurrentProcess().ProcessName}'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
}
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// because we can unhook only in the same thread, not in garbage collector thread
if (_windowsHookHandle != IntPtr.Zero)
{
if (!UnhookWindowsHookEx(_windowsHookHandle))
{
int errorCode = Marshal.GetLastWin32Error();
throw new Win32Exception(errorCode, $"Failed to remove keyboard hooks for '{Process.GetCurrentProcess().ProcessName}'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
}
_windowsHookHandle = IntPtr.Zero;
// ReSharper disable once DelegateSubtraction
_hookProc -= LowLevelKeyboardProc;
}
}
if (_user32LibraryHandle != IntPtr.Zero)
{
if (!FreeLibrary(_user32LibraryHandle)) // reduces reference to library by 1.
{
int errorCode = Marshal.GetLastWin32Error();
throw new Win32Exception(errorCode, $"Failed to unload library 'User32.dll'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
}
_user32LibraryHandle = IntPtr.Zero;
}
}
~GlobalKeyboardHook()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private IntPtr _windowsHookHandle;
private IntPtr _user32LibraryHandle;
private HookProc _hookProc;
delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll")]
private static extern IntPtr LoadLibrary(string lpFileName);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
private static extern bool FreeLibrary(IntPtr hModule);
/// <summary>
/// The SetWindowsHookEx function installs an application-defined hook procedure into a hook chain.
/// You would install a hook procedure to monitor the system for certain types of events. These events are
/// associated either with a specific thread or with all threads in the same desktop as the calling thread.
/// </summary>
/// <param name="idHook">hook type</param>
/// <param name="lpfn">hook procedure</param>
/// <param name="hMod">handle to application instance</param>
/// <param name="dwThreadId">thread identifier</param>
/// <returns>If the function succeeds, the return value is the handle to the hook procedure.</returns>
[DllImport("USER32", SetLastError = true)]
static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId);
/// <summary>
/// The UnhookWindowsHookEx function removes a hook procedure installed in a hook chain by the SetWindowsHookEx function.
/// </summary>
/// <param name="hhk">handle to hook procedure</param>
/// <returns>If the function succeeds, the return value is true.</returns>
[DllImport("USER32", SetLastError = true)]
public static extern bool UnhookWindowsHookEx(IntPtr hHook);
/// <summary>
/// The CallNextHookEx function passes the hook information to the next hook procedure in the current hook chain.
/// A hook procedure can call this function either before or after processing the hook information.
/// </summary>
/// <param name="hHook">handle to current hook</param>
/// <param name="code">hook code passed to hook procedure</param>
/// <param name="wParam">value passed to hook procedure</param>
/// <param name="lParam">value passed to hook procedure</param>
/// <returns>If the function succeeds, the return value is true.</returns>
[DllImport("USER32", SetLastError = true)]
static extern IntPtr CallNextHookEx(IntPtr hHook, int code, IntPtr wParam, IntPtr lParam);
[StructLayout(LayoutKind.Sequential)]
public struct LowLevelKeyboardInputEvent
{
/// <summary>
/// A virtual-key code. The code must be a value in the range 1 to 254.
/// </summary>
public int VirtualCode;
// EDT: added a conversion from VirtualCode to Keys.
/// <summary>
/// The VirtualCode converted to typeof(Keys) for higher usability.
/// </summary>
public Keys Key { get { return (Keys)VirtualCode; } }
/// <summary>
/// A hardware scan code for the key.
/// </summary>
public int HardwareScanCode;
/// <summary>
/// The extended-key flag, event-injected Flags, context code, and transition-state flag. This member is specified as follows. An application can use the following values to test the keystroke Flags. Testing LLKHF_INJECTED (bit 4) will tell you whether the event was injected. If it was, then testing LLKHF_LOWER_IL_INJECTED (bit 1) will tell you whether or not the event was injected from a process running at lower integrity level.
/// </summary>
public int Flags;
/// <summary>
/// The time stamp stamp for this message, equivalent to what GetMessageTime would return for this message.
/// </summary>
public int TimeStamp;
/// <summary>
/// Additional information associated with the message.
/// </summary>
public IntPtr AdditionalInformation;
}
public const int WH_KEYBOARD_LL = 13;
//const int HC_ACTION = 0;
public enum KeyboardState
{
KeyDown = 0x0100,
KeyUp = 0x0101,
SysKeyDown = 0x0104,
SysKeyUp = 0x0105
}
// EDT: Replaced VkSnapshot(int) with RegisteredKeys(Keys[])
public static Keys[] RegisteredKeys;
const int KfAltdown = 0x2000;
public const int LlkhfAltdown = (KfAltdown >> 8);
public IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam)
{
bool fEatKeyStroke = false;
var wparamTyped = wParam.ToInt32();
if (Enum.IsDefined(typeof(KeyboardState), wparamTyped))
{
object o = Marshal.PtrToStructure(lParam, typeof(LowLevelKeyboardInputEvent));
LowLevelKeyboardInputEvent p = (LowLevelKeyboardInputEvent)o;
var eventArguments = new GlobalKeyboardHookEventArgs(p, (KeyboardState)wparamTyped);
// EDT: Removed the comparison-logic from the usage-area so the user does not need to mess around with it.
// Either the incoming key has to be part of RegisteredKeys (see constructor on top) or RegisterdKeys
// has to be null for the event to get fired.
var key = (Keys)p.VirtualCode;
if (RegisteredKeys == null || RegisteredKeys.Contains(key))
{
EventHandler<GlobalKeyboardHookEventArgs> handler = KeyboardPressed;
handler?.Invoke(this, eventArguments);
fEatKeyStroke = eventArguments.Handled;
}
}
return fEatKeyStroke ? (IntPtr)1 : CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
}
}
Различия в использовании можно увидеть здесь
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private GlobalKeyboardHook _globalKeyboardHook;
private void buttonHook_Click(object sender, EventArgs e)
{
// Hooks only into specified Keys (here "A" and "B").
_globalKeyboardHook = new GlobalKeyboardHook(new Keys[] { Keys.A, Keys.B });
// Hooks into all keys.
_globalKeyboardHook = new GlobalKeyboardHook();
_globalKeyboardHook.KeyboardPressed += OnKeyPressed;
}
private void OnKeyPressed(object sender, GlobalKeyboardHookEventArgs e)
{
// EDT: No need to filter for VkSnapshot anymore. This now gets handled
// through the constructor of GlobalKeyboardHook(...).
if (e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyDown)
{
// Now you can access both, the key and virtual code
Keys loggedKey = e.KeyboardData.Key;
int loggedVkCode = e.KeyboardData.VirtualCode;
}
}
}
Спасибо Siarhei Kuchuk за его пост. Даже несмотря на то, что я упростил использование, этот исходный код был мне очень полезен.
Если глобальной горячей клавиши будет достаточно, то RegisterHotKey сделает свое дело
Для системных глобальных комбинаций клавиш и многоклавишных коротких клавиш здесь есть очень простой. Код класса:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
namespace KeyboardUtils
{
/// <summary>
/// Provide a way to handle a global keyboard hooks
/// <remarks>This hook is called in the context of the thread that installed it.
/// The call is made by sending a message to the thread that installed the hook.
/// Therefore, the thread that installed the hook must have a message loop.</remarks>
/// </summary>
public sealed class GlobalKeyboardHook : IDisposable
{
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 0x0100;
private const int WM_KEYUP = 0x0101;
private LowLevelKeyboardProc _proc;
private readonly IntPtr _hookId = IntPtr.Zero;
private static GlobalKeyboardHook _instance;
private Dictionary<int, KeyValuePair<KeyCombination, HookActions>> _hookEvents;
private bool _disposed;
private KeyCombination _pressedKeys;
/// <summary>
/// Return a singleton instance of <see cref="GlobalKeyboardHook"/>
/// </summary>
public static GlobalKeyboardHook Instance
{
get
{
Interlocked.CompareExchange(ref _instance, new GlobalKeyboardHook(), null);
return _instance;
}
}
private GlobalKeyboardHook()
{
_proc = HookCallback;
_hookEvents = new Dictionary<int, KeyValuePair<KeyCombination, HookActions>>();
_hookId = SetHook(_proc);
_pressedKeys = new KeyCombination();
}
/// <summary>
/// Register a keyboard hook event
/// </summary>
/// <param name="keys">The short keys. minimum is two keys</param>
/// <param name="execute">The action to run when the key ocmbination has pressed</param>
/// <param name="message">Empty if no error occurred otherwise error message</param>
/// <param name="runAsync">True if the action should execute in the background. -Be careful from thread affinity- Default is false</param>
/// <param name="dispose">An action to run when unsubscribing from keyboard hook. can be null</param>
/// <returns>Event id to use when unregister</returns>
public int Hook(List<Key> keys, Action execute, out string message, bool runAsync = false, Action<object> dispose = null)
{
if (_hookEvents == null)
{
message = "Can't register";
return -1;
}
if (keys == null || execute == null)
{
message = "'keys' and 'execute' can't be null";
return -1;
}
if (keys.Count < 2)
{
message = "You must provide at least two keys";
return -1;
}
if (!ValidateKeys(keys))
{
message = "Unallowed key. Only 'shift', 'ctrl' and 'a' - 'z' are allowed";
return -1;
}
var kc = new KeyCombination(keys);
int id = kc.GetHashCode();
if (_hookEvents.ContainsKey(id))
{
message = "The key combination is already exist it the application";
return -1;
}
// if the action should run async, wrap it with Task
Action asyncAction = null;
if (runAsync)
asyncAction = () => Task.Run(() => execute);
_hookEvents[id] = new KeyValuePair<KeyCombination, HookActions>(kc, new HookActions(asyncAction ?? execute, dispose));
message = string.Empty;
return id;
}
private bool ValidateKeys(IEnumerable<Key> keys)
{
return keys.All(t => IsKeyValid((int)t));
}
private bool IsKeyValid(int key)
{
// 'alt' is sys key and hence is disallowed.
// a - z and shift, ctrl.
return key >= 44 && key <= 69 || key >= 116 && key <= 119;
}
/// <summary>
/// Un register a keyboard hook event
/// </summary>
/// <param name="id">event id to remove</param>
/// <param name="obj">parameter to pass to dispose method</param>
public void UnHook(int id, object obj = null)
{
if (_hookEvents == null || id < 0 || !_hookEvents.ContainsKey(id)) return;
var hook = _hookEvents[id];
if (hook.Value != null && hook.Value.Dispose != null)
{
try
{
hook.Value.Dispose(obj);
}
catch (Exception)
{
// need to be define if we need to throw the exception
}
}
_hookEvents.Remove(id);
}
private IntPtr SetHook(LowLevelKeyboardProc proc)
{
using (Process curProcess = Process.GetCurrentProcess())
using (ProcessModule curModule = curProcess.MainModule)
{
return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
}
}
private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode < 0)
return CallNextHookEx(_hookId, nCode, wParam, lParam);
var result = new IntPtr(0);
if (wParam == (IntPtr)WM_KEYDOWN)
{
_pressedKeys.Add(KeyInterop.KeyFromVirtualKey(Marshal.ReadInt32(lParam))); // vkCode (in KBDLLHOOKSTRUCT) is DWORD (actually it can be 0-254)
if (_pressedKeys.Count >= 2)
{
var keysToAction = _hookEvents.Values.FirstOrDefault(val => val.Key.Equals(_pressedKeys));
if (keysToAction.Value != null)
{
keysToAction.Value.Execute();
// don't try to get the action again after the execute because it may removed already
result = new IntPtr(1);
}
}
}
else if (wParam == (IntPtr)WM_KEYUP)
{
_pressedKeys.Clear();
}
// in case we processed the message, prevent the system from passing the message to the rest of the hook chain
// return result.ToInt32() == 0 ? CallNextHookEx(_hookId, nCode, wParam, lParam) : result;
return CallNextHookEx(_hookId, nCode, wParam, lParam);
}
#region extern
[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)]
[return: MarshalAs(UnmanagedType.Bool)]
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);
#endregion
#region IDsiposable
private void Dispose(bool dispose)
{
try
{
if (_disposed)
return;
UnhookWindowsHookEx(_hookId);
if (dispose)
{
_proc = null;
_hookEvents = null;
_pressedKeys = null;
GC.SuppressFinalize(this);
}
_disposed = true;
}
// ReSharper disable once EmptyGeneralCatchClause
catch
{
}
}
public void Dispose()
{
Dispose(true);
}
~GlobalKeyboardHook()
{
Dispose(false);
}
#endregion
private class HookActions
{
public HookActions(Action excetue, Action<object> dispose = null)
{
Exceute = excetue;
Dispose = dispose;
}
public Action Exceute { get; set; }
public Action<object> Dispose { get; set; }
}
private class KeyCombination : IEquatable<KeyCombination>
{
private readonly bool _canModify;
public KeyCombination(List<Key> keys)
{
_keys = keys ?? new List<Key>();
}
public KeyCombination()
{
_keys = new List<Key>();
_canModify = true;
}
public void Add(Key key)
{
if (_canModify)
{
_keys.Add(key);
}
}
public void Remove(Key key)
{
if (_canModify)
{
_keys.Remove(key);
}
}
public void Clear()
{
if (_canModify)
{
_keys.Clear();
}
}
public int Count { get { return _keys.Count; } }
private readonly List<Key> _keys;
public bool Equals(KeyCombination other)
{
return other._keys != null && _keys != null && KeysEqual(other._keys);
}
private bool KeysEqual(List<Key> keys)
{
if (keys == null || _keys == null || keys.Count != _keys.Count) return false;
for (int i = 0; i < _keys.Count; i++)
{
if (_keys[i] != keys[i])
return false;
}
return true;
}
public override bool Equals(object obj)
{
if (obj is KeyCombination)
return Equals((KeyCombination)obj);
return false;
}
public override int GetHashCode()
{
if (_keys == null) return 0;
//http://stackoverflow.com/a/263416
//http://stackoverflow.com/a/8094931
//assume keys not going to modify after we use GetHashCode
unchecked
{
int hash = 19;
for (int i = 0; i < _keys.Count; i++)
{
hash = hash * 31 + _keys[i].GetHashCode();
}
return hash;
}
}
public override string ToString()
{
if (_keys == null)
return string.Empty;
var sb = new StringBuilder((_keys.Count - 1) * 4 + 10);
for (int i = 0; i < _keys.Count; i++)
{
if (i < _keys.Count - 1)
sb.Append(_keys[i] + " , ");
else
sb.Append(_keys[i]);
}
return sb.ToString();
}
}
}
}
И как использовать его в приложении формы:
string message;
var hookId = GlobalKeyboardHook.Instance.Hook(
new List<System.Windows.Input.Key> {
System.Windows.Input.Key.A,
System.Windows.Input.Key.B
},
() =>
{
Console.WriteLine("a-b");
},
out message);
И очень простой пример для консольного приложения:
[STAThread]
static void Main()
{
string message;
var hookId = GlobalKeyboardHook.Instance.Hook(
new List<System.Windows.Input.Key> {
System.Windows.Input.Key.A,
System.Windows.Input.Key.B
},
() =>
{
Console.WriteLine("a-b");
},
out message);
Console.WriteLine(message);
Application.Run();
GlobalKeyboardHook.Instance.UnHook(hookId);
}
Я также рекомендую прочитать этот пост: /questions/6315631/c-globalnaya-klaviatura-huk-kotoraya-otkryivaet-formu-iz-konsolnogo-prilozheniya/6315636#6315636
Важное примечание: общесистемные хуки чрезвычайно опасны, вы ДОЛЖНЫ быть очень осторожны в том, что вы делаете. Если вы перехватываете ключевое событие с помощью этого метода, убедитесь, что вы правильно отцепили и освободили память, что может быть сложно, особенно в консольном приложении. Я столкнулся с несколькими синими страницами и некоторой нестабильностью системы, такой как проблемы с выключением и сном, а также зависание после нескольких часов использования. Поэтому, пожалуйста, будьте особенно осторожны при его использовании.
Мой представитель слишком низок, чтобы комментировать, но что касается исключения, я изменил
public void SetupKeyboardHooks()
в ответе C4d выглядеть так:
public void SetupKeyboardHooks(out object hookProc)
{
_globalKeyboardHook = new GlobalKeyboardHook();
_globalKeyboardHook.KeyboardPressed += OnKeyPressed;
hookProc = _globalKeyboardHook.GcSafeHookProc;
}
куда
GcSafeHookProc
просто публичный добытчик для
_hookProc
в ОП
_hookProc = LowLevelKeyboardProc; // we must keep alive _hookProc, because GC is not aware about SetWindowsHookEx behaviour.
и сохранил
hookProc
как приватное поле в классе, вызывающем
SetupKeyboardHooks(...)
, поэтому сохраняя ссылку живой, за исключением сборки мусора, не более
CallbackOnCollectedDelegate
исключение. Кажется, что эта дополнительная ссылка в
GlobalKeyboardHook
класса не достаточно. Возможно, убедитесь, что эта ссылка также удаляется при закрытии вашего приложения.
private void buttonHook_Click(object sender, EventArgs e)
{
// Hooks only into specified Keys (here "A" and "B").
// (***) Use this constructor
_globalKeyboardHook = new GlobalKeyboardHook(new Keys[] { Keys.A, Keys.B });
// Hooks into all keys.
// (***) Or this - not both
_globalKeyboardHook = new GlobalKeyboardHook();
_globalKeyboardHook.KeyboardPressed += OnKeyPressed;
}
А потом работает нормально.