From eed26e82a834d3906d2e2842b1dd3010f205b016 Mon Sep 17 00:00:00 2001 From: ZGGSONG Date: Fri, 16 Dec 2022 10:50:34 +0800 Subject: [PATCH] feat: add global hotkeys --- STranslate/MainWindow.xaml | 8 +- STranslate/MainWindow.xaml.cs | 68 +++- STranslate/Utils/HotKeysUtil.cs | 100 ++++++ STranslate/Utils/KeyboardUtil.cs | 524 +++++++++++++++++++++++++++++++ 4 files changed, 691 insertions(+), 9 deletions(-) create mode 100644 STranslate/Utils/HotKeysUtil.cs create mode 100644 STranslate/Utils/KeyboardUtil.cs diff --git a/STranslate/MainWindow.xaml b/STranslate/MainWindow.xaml index 34aa94d..800293e 100644 --- a/STranslate/MainWindow.xaml +++ b/STranslate/MainWindow.xaml @@ -15,6 +15,7 @@ ResizeMode="NoResize" KeyDown="Window_KeyDown" Topmost="True" + Deactivated="Window_Deactivated" WindowStyle="None" Height="450" Width="400" @@ -45,7 +46,8 @@ CornerRadius="4" Margin="5" MaxHeight="200"> - @@ -74,7 +77,8 @@ CornerRadius="4" Margin="5" MaxHeight="200"> - public partial class MainWindow : Window { + /// + /// 监听全局快捷键 + /// + /// + protected override void OnSourceInitialized(EventArgs e) + { + HotkeysUtil.InitialHook(this); +#if true + HotkeysUtil.Regist(HotkeyModifiers.MOD_ALT, Key.A, () => + { + this.Show(); + this.Activate(); + this.TextBoxInput.Focus(); + System.Diagnostics.Debug.Print("alt + a"); + }); + HotkeysUtil.Regist(HotkeyModifiers.MOD_ALT, Key.D, () => + { + this.Show(); + this.Activate(); + this.TextBoxInput.Text = "123"; + System.Diagnostics.Debug.Print("alt + d"); + //复制内容 + //KeyboardUtil.Press(Key.LeftCtrl); + //KeyboardUtil.Type(Key.C); + //KeyboardUtil.Release(Key.LeftCtrl); + + //this.Show(); + //this.Activate(); + + + //this.TextBoxInput.Text = Clipboard.GetText(); + //this.TextBoxInput.Focus(); + + //KeyboardUtil.Type(Key.Enter); + }); +#endif + } + public MainWindow() { InitializeComponent(); @@ -42,22 +81,24 @@ namespace STranslate /// private void Window_KeyDown(object sender, KeyEventArgs e) { - //置顶/取消置顶 Ctrl+T - if (e.KeyboardDevice.Modifiers.HasFlag(ModifierKeys.Control) && e.Key == Key.T) - { - Topmost = Topmost != true; - Opacity = Topmost ? 1 : 0.9; - } //最小化 Esc if (e.Key == Key.Escape) { this.Hide(); + this.TextBoxOutput.Text = string.Empty; } //退出 Ctrl+Q if (e.KeyboardDevice.Modifiers.HasFlag(ModifierKeys.Control) && e.Key == Key.Q) { Application.Current.Shutdown(); } +#if false + //置顶/取消置顶 Ctrl+T + if (e.KeyboardDevice.Modifiers.HasFlag(ModifierKeys.Control) && e.Key == Key.T) + { + Topmost = Topmost != true; + Opacity = Topmost ? 1 : 0.9; + } //缩小 Ctrl+[ if (e.KeyboardDevice.Modifiers.HasFlag(ModifierKeys.Control) && e.Key == Key.OemOpenBrackets) { @@ -84,11 +125,24 @@ namespace STranslate Width = 400; Height = 450; } +#endif } private void NotifyIcon_Click(object sender, RoutedEventArgs e) { this.Show(); + this.Activate(); + } + + /// + /// 非激活窗口则隐藏起来 + /// + /// + /// + private void Window_Deactivated(object sender, EventArgs e) + { + this.Hide(); + this.TextBoxOutput.Text = string.Empty; } } } diff --git a/STranslate/Utils/HotKeysUtil.cs b/STranslate/Utils/HotKeysUtil.cs new file mode 100644 index 0000000..153c550 --- /dev/null +++ b/STranslate/Utils/HotKeysUtil.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Input; +using System.Windows.Interop; + +namespace STranslate.Utils +{ + /// + /// 引用自 https://zhuanlan.zhihu.com/p/445050708 + /// + static class HotkeysUtil + { + #region 系统api + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool RegisterHotKey(IntPtr hWnd, int id, HotkeyModifiers fsModifiers, uint vk); + + [DllImport("user32.dll")] + static extern bool UnregisterHotKey(IntPtr hWnd, int id); + #endregion + + public static IntPtr hwnd; + public static void InitialHook(Window window) + { + hwnd = new WindowInteropHelper(window).Handle; + var _hwndSource = HwndSource.FromHwnd(hwnd); + _hwndSource.AddHook(WndProc); + } + /// + /// 注册快捷键 + /// + /// 持有快捷键窗口 + /// 组合键 + /// 快捷键 + /// 回调函数 + public static void Regist(HotkeyModifiers fsModifiers, Key key, HotKeyCallBackHanlder callBack) + { + int id = keyid++; + + var vk = KeyInterop.VirtualKeyFromKey(key); + if (!RegisterHotKey(hwnd, id, fsModifiers, (uint)vk)) + throw new Exception("regist hotkey fail."); + keymap[id] = callBack; + } + + /// + /// 快捷键消息处理 + /// + static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + { + if (msg == WM_HOTKEY) + { + int id = wParam.ToInt32(); + if (keymap.TryGetValue(id, out var callback)) + { + callback(); + } + } + return IntPtr.Zero; + } + + /// + /// 注销快捷键 + /// + /// 持有快捷键窗口的句柄 + /// 回调函数 + public static void UnRegist(IntPtr hWnd, HotKeyCallBackHanlder callBack) + { + foreach (KeyValuePair var in keymap) + { + if (var.Value == callBack) + UnregisterHotKey(hWnd, var.Key); + } + } + + + const int WM_HOTKEY = 0x312; + static int keyid = 10; + static Dictionary keymap = new Dictionary(); + + public delegate void HotKeyCallBackHanlder(); + } + + enum HotkeyModifiers + { + MOD_ALT = 0x1, + MOD_CONTROL = 0x2, + MOD_SHIFT = 0x4, + MOD_WIN = 0x8 + } + enum MyHotkeys + { + KEY_A = Key.A, + KEY_S = Key.S, + KEY_D = Key.D, + } + +} diff --git a/STranslate/Utils/KeyboardUtil.cs b/STranslate/Utils/KeyboardUtil.cs new file mode 100644 index 0000000..a8bcf05 --- /dev/null +++ b/STranslate/Utils/KeyboardUtil.cs @@ -0,0 +1,524 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Permissions; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace STranslate.Utils +{ + + + #region Operate Mouse Keyboard + + /// + /// Native methods + /// + internal static class NativeMethods + { + //User32 wrappers cover API's used for Mouse input + #region User32 + // Two special bitmasks we define to be able to grab + // shift and character information out of a VKey. + internal const int VKeyShiftMask = 0x0100; + internal const int VKeyCharMask = 0x00FF; + + // Various Win32 constants + internal const int KeyeventfExtendedkey = 0x0001; + internal const int KeyeventfKeyup = 0x0002; + internal const int KeyeventfScancode = 0x0008; + + internal const int MouseeventfVirtualdesk = 0x4000; + + internal const int SMXvirtualscreen = 76; + internal const int SMYvirtualscreen = 77; + internal const int SMCxvirtualscreen = 78; + internal const int SMCyvirtualscreen = 79; + + internal const int XButton1 = 0x0001; + internal const int XButton2 = 0x0002; + internal const int WheelDelta = 120; + + internal const int InputMouse = 0; + internal const int InputKeyboard = 1; + + // Various Win32 data structures + [StructLayout(LayoutKind.Sequential)] + internal struct INPUT + { + internal int type; + internal INPUTUNION union; + }; + + [StructLayout(LayoutKind.Explicit)] + internal struct INPUTUNION + { + [FieldOffset(0)] + internal MOUSEINPUT mouseInput; + [FieldOffset(0)] + internal KEYBDINPUT keyboardInput; + }; + + [StructLayout(LayoutKind.Sequential)] + internal struct MOUSEINPUT + { + internal int dx; + internal int dy; + internal int mouseData; + internal int dwFlags; + internal int time; + internal IntPtr dwExtraInfo; + }; + + [StructLayout(LayoutKind.Sequential)] + internal struct KEYBDINPUT + { + internal short wVk; + internal short wScan; + internal int dwFlags; + internal int time; + internal IntPtr dwExtraInfo; + }; + + [Flags] + internal enum SendMouseInputFlags + { + Move = 0x0001, + LeftDown = 0x0002, + LeftUp = 0x0004, + RightDown = 0x0008, + RightUp = 0x0010, + MiddleDown = 0x0020, + MiddleUp = 0x0040, + XDown = 0x0080, + XUp = 0x0100, + Wheel = 0x0800, + Absolute = 0x8000, + }; + + // Importing various Win32 APIs that we need for input + [DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Auto)] + internal static extern int GetSystemMetrics(int nIndex); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + internal static extern int MapVirtualKey(int nVirtKey, int nMapType); + + [DllImport("user32.dll", SetLastError = true)] + internal static extern int SendInput(int nInputs, ref INPUT mi, int cbSize); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + internal static extern short VkKeyScan(char ch); + + #endregion + } + + + /// + /// Exposes a simple interface to common mouse operations, allowing the user to simulate mouse input. + /// + /// The following code moves to screen coordinate 100,100 and left clicks. + /// + /** + Mouse.MoveTo(new Point(100, 100)); + Mouse.Click(MouseButton.Left); + */ + /// + /// + public static class Mouse + { + /// + /// Clicks a mouse button. + /// + /// The mouse button to click. + public static void Click(MouseButton mouseButton) + { + Down(mouseButton); + Up(mouseButton); + } + + /// + /// Double-clicks a mouse button. + /// + /// The mouse button to click. + public static void DoubleClick(MouseButton mouseButton) + { + Click(mouseButton); + Click(mouseButton); + } + + /// + /// Performs a mouse-down operation for a specified mouse button. + /// + /// The mouse button to use. + public static void Down(MouseButton mouseButton) + { + switch (mouseButton) + { + case MouseButton.Left: + SendMouseInput(0, 0, 0, NativeMethods.SendMouseInputFlags.LeftDown); + break; + case MouseButton.Right: + SendMouseInput(0, 0, 0, NativeMethods.SendMouseInputFlags.RightDown); + break; + case MouseButton.Middle: + SendMouseInput(0, 0, 0, NativeMethods.SendMouseInputFlags.MiddleDown); + break; + case MouseButton.XButton1: + SendMouseInput(0, 0, NativeMethods.XButton1, NativeMethods.SendMouseInputFlags.XDown); + break; + case MouseButton.XButton2: + SendMouseInput(0, 0, NativeMethods.XButton2, NativeMethods.SendMouseInputFlags.XDown); + break; + default: + throw new InvalidOperationException("Unsupported MouseButton input."); + } + } + + /// + /// Moves the mouse pointer to the specified screen coordinates. + /// + /// The screen coordinates to move to. + public static void MoveTo(System.Drawing.Point point) + { + SendMouseInput(point.X, point.Y, 0, NativeMethods.SendMouseInputFlags.Move | NativeMethods.SendMouseInputFlags.Absolute); + } + + /// + /// Resets the system mouse to a clean state. + /// + public static void Reset() + { + MoveTo(new System.Drawing.Point(0, 0)); + + if (System.Windows.Input.Mouse.LeftButton == MouseButtonState.Pressed) + { + SendMouseInput(0, 0, 0, NativeMethods.SendMouseInputFlags.LeftUp); + } + + if (System.Windows.Input.Mouse.MiddleButton == MouseButtonState.Pressed) + { + SendMouseInput(0, 0, 0, NativeMethods.SendMouseInputFlags.MiddleUp); + } + + if (System.Windows.Input.Mouse.RightButton == MouseButtonState.Pressed) + { + SendMouseInput(0, 0, 0, NativeMethods.SendMouseInputFlags.RightUp); + } + + if (System.Windows.Input.Mouse.XButton1 == MouseButtonState.Pressed) + { + SendMouseInput(0, 0, NativeMethods.XButton1, NativeMethods.SendMouseInputFlags.XUp); + } + + if (System.Windows.Input.Mouse.XButton2 == MouseButtonState.Pressed) + { + SendMouseInput(0, 0, NativeMethods.XButton2, NativeMethods.SendMouseInputFlags.XUp); + } + } + + /// + /// Simulates scrolling of the mouse wheel up or down. + /// + /// The number of lines to scroll. Use positive numbers to scroll up and negative numbers to scroll down. + public static void Scroll(double lines) + { + int amount = (int)(NativeMethods.WheelDelta * lines); + + SendMouseInput(0, 0, amount, NativeMethods.SendMouseInputFlags.Wheel); + } + + /// + /// Performs a mouse-up operation for a specified mouse button. + /// + /// The mouse button to use. + public static void Up(MouseButton mouseButton) + { + switch (mouseButton) + { + case MouseButton.Left: + SendMouseInput(0, 0, 0, NativeMethods.SendMouseInputFlags.LeftUp); + break; + case MouseButton.Right: + SendMouseInput(0, 0, 0, NativeMethods.SendMouseInputFlags.RightUp); + break; + case MouseButton.Middle: + SendMouseInput(0, 0, 0, NativeMethods.SendMouseInputFlags.MiddleUp); + break; + case MouseButton.XButton1: + SendMouseInput(0, 0, NativeMethods.XButton1, NativeMethods.SendMouseInputFlags.XUp); + break; + case MouseButton.XButton2: + SendMouseInput(0, 0, NativeMethods.XButton2, NativeMethods.SendMouseInputFlags.XUp); + break; + default: + throw new InvalidOperationException("Unsupported MouseButton input."); + } + } + + /// + /// Sends mouse input. + /// + /// x coordinate + /// y coordinate + /// scroll wheel amount + /// SendMouseInputFlags flags + [PermissionSet(SecurityAction.Assert, Name = "FullTrust")] + private static void SendMouseInput(int x, int y, int data, NativeMethods.SendMouseInputFlags flags) + { + PermissionSet permissions = new PermissionSet(PermissionState.Unrestricted); + permissions.Demand(); + + int intflags = (int)flags; + + if ((intflags & (int)NativeMethods.SendMouseInputFlags.Absolute) != 0) + { + // Absolute position requires normalized coordinates. + NormalizeCoordinates(ref x, ref y); + intflags |= NativeMethods.MouseeventfVirtualdesk; + } + + NativeMethods.INPUT mi = new NativeMethods.INPUT(); + mi.type = NativeMethods.InputMouse; + mi.union.mouseInput.dx = x; + mi.union.mouseInput.dy = y; + mi.union.mouseInput.mouseData = data; + mi.union.mouseInput.dwFlags = intflags; + mi.union.mouseInput.time = 0; + mi.union.mouseInput.dwExtraInfo = new IntPtr(0); + + if (NativeMethods.SendInput(1, ref mi, Marshal.SizeOf(mi)) == 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + + private static void NormalizeCoordinates(ref int x, ref int y) + { + int vScreenWidth = NativeMethods.GetSystemMetrics(NativeMethods.SMCxvirtualscreen); + int vScreenHeight = NativeMethods.GetSystemMetrics(NativeMethods.SMCyvirtualscreen); + int vScreenLeft = NativeMethods.GetSystemMetrics(NativeMethods.SMXvirtualscreen); + int vScreenTop = NativeMethods.GetSystemMetrics(NativeMethods.SMYvirtualscreen); + + // Absolute input requires that input is in 'normalized' coords - with the entire + // desktop being (0,0)...(65536,65536). Need to convert input x,y coords to this + // first. + // + // In this normalized world, any pixel on the screen corresponds to a block of values + // of normalized coords - eg. on a 1024x768 screen, + // y pixel 0 corresponds to range 0 to 85.333, + // y pixel 1 corresponds to range 85.333 to 170.666, + // y pixel 2 correpsonds to range 170.666 to 256 - and so on. + // Doing basic scaling math - (x-top)*65536/Width - gets us the start of the range. + // However, because int math is used, this can end up being rounded into the wrong + // pixel. For example, if we wanted pixel 1, we'd get 85.333, but that comes out as + // 85 as an int, which falls into pixel 0's range - and that's where the pointer goes. + // To avoid this, we add on half-a-"screen pixel"'s worth of normalized coords - to + // push us into the middle of any given pixel's range - that's the 65536/(Width*2) + // part of the formula. So now pixel 1 maps to 85+42 = 127 - which is comfortably + // in the middle of that pixel's block. + // The key ting here is that unlike points in coordinate geometry, pixels take up + // space, so are often better treated like rectangles - and if you want to target + // a particular pixel, target its rectangle's midpoint, not its edge. + x = ((x - vScreenLeft) * 65536) / vScreenWidth + 65536 / (vScreenWidth * 2); + y = ((y - vScreenTop) * 65536) / vScreenHeight + 65536 / (vScreenHeight * 2); + } + } + + /// + /// Exposes a simple interface to common keyboard operations, allowing the user to simulate keyboard input. + /// + /// + /// The following code types "Hello world" with the specified casing, + /// and then types "hello, capitalized world" which will be in all caps because + /// the left shift key is being held down. + /// + /** + Keyboard.Type("Hello world"); + Keyboard.Press(Key.LeftShift); + Keyboard.Type("hello, capitalized world"); + Keyboard.Release(Key.LeftShift); + */ + /// + /// + public static class KeyboardUtil + { + #region Public Members + + /// + /// Presses down a key. + /// + /// The key to press. + public static void Press(Key key) + { + SendKeyboardInput(key, true); + } + + /// + /// Releases a key. + /// + /// The key to release. + public static void Release(Key key) + { + SendKeyboardInput(key, false); + } + + /// + /// Resets the system keyboard to a clean state. + /// + public static void Reset() + { + foreach (Key key in Enum.GetValues(typeof(Key))) + { + if (key != Key.None && (System.Windows.Input.Keyboard.GetKeyStates(key) & KeyStates.Down) > 0) + { + Release(key); + } + } + } + + /// + /// Performs a press-and-release operation for the specified key, which is effectively equivallent to typing. + /// + /// The key to press. + public static void Type(Key key) + { + Press(key); + Release(key); + } + + /// + /// Types the specified text. + /// + /// The text to type. + public static void Type(string text) + { + foreach (char c in text) + { + // We get the vKey value for the character via a Win32 API. We then use bit masks to pull the + // upper and lower bytes to get the shift state and key information. We then use WPF KeyInterop + // to go from the vKey key info into a System.Windows.Input.Key data structure. This work is + // necessary because Key doesn't distinguish between upper and lower case, so we have to wrap + // the key type inside a shift press/release if necessary. + int vKeyValue = NativeMethods.VkKeyScan(c); + bool keyIsShifted = (vKeyValue & NativeMethods.VKeyShiftMask) == NativeMethods.VKeyShiftMask; + Key key = KeyInterop.KeyFromVirtualKey(vKeyValue & NativeMethods.VKeyCharMask); + + if (keyIsShifted) + { + Type(key, new Key[] { Key.LeftShift }); + } + else + { + Type(key); + } + } + } + + #endregion + + #region Private Members + + /// + /// Types a key while a set of modifier keys are being pressed. Modifer keys + /// are pressed in the order specified and released in reverse order. + /// + /// Key to type. + /// Set of keys to hold down with key is typed. + private static void Type(Key key, Key[] modifierKeys) + { + foreach (Key modiferKey in modifierKeys) + { + Press(modiferKey); + } + + Type(key); + + foreach (Key modifierKey in modifierKeys.Reverse()) + { + Release(modifierKey); + } + } + + /// + /// Injects keyboard input into the system. + /// + /// Indicates the key pressed or released. Can be one of the constants defined in the Key enum. + /// True to inject a key press, false to inject a key release. + [PermissionSet(SecurityAction.Assert, Name = "FullTrust")] + private static void SendKeyboardInput(Key key, bool press) + { + PermissionSet permissions = new PermissionSet(PermissionState.Unrestricted); + permissions.Demand(); + + NativeMethods.INPUT ki = new NativeMethods.INPUT(); + ki.type = NativeMethods.InputKeyboard; + ki.union.keyboardInput.wVk = (short)KeyInterop.VirtualKeyFromKey(key); + ki.union.keyboardInput.wScan = (short)NativeMethods.MapVirtualKey(ki.union.keyboardInput.wVk, 0); + + int dwFlags = 0; + + if (ki.union.keyboardInput.wScan > 0) + { + dwFlags |= NativeMethods.KeyeventfScancode; + } + + if (!press) + { + dwFlags |= NativeMethods.KeyeventfKeyup; + } + + ki.union.keyboardInput.dwFlags = dwFlags; + + if (ExtendedKeys.Contains(key)) + { + ki.union.keyboardInput.dwFlags |= NativeMethods.KeyeventfExtendedkey; + } + + ki.union.keyboardInput.time = 0; + ki.union.keyboardInput.dwExtraInfo = new IntPtr(0); + + if (NativeMethods.SendInput(1, ref ki, Marshal.SizeOf(ki)) == 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + + // From the SDK: + // The extended-key flag indicates whether the keystroke message originated from one of + // the additional keys on the enhanced keyboard. The extended keys consist of the ALT and + // CTRL keys on the right-hand side of the keyboard; the INS, DEL, HOME, END, PAGE UP, + // PAGE DOWN, and arrow keys in the clusters to the left of the numeric keypad; the NUM LOCK + // key; the BREAK (CTRL+PAUSE) key; the PRINT SCRN key; and the divide (/) and ENTER keys in + // the numeric keypad. The extended-key flag is set if the key is an extended key. + // + // - docs appear to be incorrect. Use of Spy++ indicates that break is not an extended key. + // Also, menu key and windows keys also appear to be extended. + private static readonly Key[] ExtendedKeys = new Key[] { + Key.RightAlt, + Key.RightCtrl, + Key.NumLock, + Key.Insert, + Key.Delete, + Key.Home, + Key.End, + Key.Prior, + Key.Next, + Key.Up, + Key.Down, + Key.Left, + Key.Right, + Key.Apps, + Key.RWin, + Key.LWin }; + // Note that there are no distinct values for the following keys: + // numpad divide + // numpad enter + + #endregion + } + #endregion +}