[PowerAccent] Move low-level keyboard hook to c++ (#20190)

* Move llkeyboardhook to c++

* expect.txt

* Address PR comment - Resolve namespaces

* Address PR comments - CallNextHook and correct char selection

* Also unpress the letter key

Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
This commit is contained in:
Stefan Markovic
2022-09-09 13:27:56 +02:00
committed by GitHub
parent 7f8c5c9f0c
commit 7b0f97597d
25 changed files with 760 additions and 548 deletions

View File

@@ -1,27 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Vanara.PInvoke;
namespace PowerAccent.Core;
public enum LetterKey
{
A = User32.VK.VK_A,
C = User32.VK.VK_C,
E = User32.VK.VK_E,
I = User32.VK.VK_I,
N = User32.VK.VK_N,
O = User32.VK.VK_O,
S = User32.VK.VK_S,
U = User32.VK.VK_U,
Y = User32.VK.VK_Y,
}
public enum TriggerKey
{
Left = User32.VK.VK_LEFT,
Right = User32.VK.VK_RIGHT,
Space = User32.VK.VK_SPACE,
}

View File

@@ -1,20 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Version.props" />
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<Import Project="..\..\..\Version.props" />
<PropertyGroup>
<TargetFramework>net6.0-windows10.0.19041.0</TargetFramework>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<UseWPF>true</UseWPF>
</PropertyGroup>
<PropertyGroup>
<CsWinRTIncludes>PowerToys.PowerAccentKeyboardService</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
<PackageReference Include="Vanara.PInvoke.User32" Version="3.3.15" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.0.0" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
<PackageReference Include="Vanara.PInvoke.User32" Version="3.3.15" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\PowerAccentKeyboardService\PowerAccentKeyboardService.vcxproj" />
</ItemGroup>
</Project>

View File

@@ -2,173 +2,159 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using Microsoft.PowerToys.Settings.UI.Library.Enumerations;
using System.Windows;
using PowerAccent.Core.Services;
using PowerAccent.Core.Tools;
using PowerToys.PowerAccentKeyboardService;
namespace PowerAccent.Core;
public class PowerAccent : IDisposable
{
private readonly SettingsService _settingService = new SettingsService();
private readonly KeyboardListener _keyboardListener = new KeyboardListener();
private readonly SettingsService _settingService;
private LetterKey? letterPressed;
private bool _visible;
private char[] _characters = Array.Empty<char>();
private int _selectedIndex = -1;
private Stopwatch _stopWatch;
private bool _triggeredWithSpace;
public event Action<bool, char[]> OnChangeDisplay;
public event Action<int, char> OnSelectCharacter;
private KeyboardListener _keyboardListener;
public PowerAccent()
{
_keyboardListener.KeyDown += PowerAccent_KeyDown;
_keyboardListener.KeyUp += PowerAccent_KeyUp;
_keyboardListener = new KeyboardListener();
_keyboardListener.InitHook();
_settingService = new SettingsService(_keyboardListener);
SetEvents();
}
private bool PowerAccent_KeyDown(object sender, KeyboardListener.RawKeyEventArgs args)
private void SetEvents()
{
if (Enum.IsDefined(typeof(LetterKey), (int)args.Key))
_keyboardListener.SetShowToolbarEvent(new PowerToys.PowerAccentKeyboardService.ShowToolbar((LetterKey letterKey) =>
{
_stopWatch = Stopwatch.StartNew();
letterPressed = (LetterKey)args.Key;
}
TriggerKey? triggerPressed = null;
if (letterPressed.HasValue)
{
if (Enum.IsDefined(typeof(TriggerKey), (int)args.Key))
Application.Current.Dispatcher.Invoke(() =>
{
triggerPressed = (TriggerKey)args.Key;
ShowToolbar(letterKey);
});
}));
if ((triggerPressed == TriggerKey.Space && _settingService.ActivationKey == PowerAccentActivationKey.LeftRightArrow) ||
((triggerPressed == TriggerKey.Left || triggerPressed == TriggerKey.Right) && _settingService.ActivationKey == PowerAccentActivationKey.Space))
{
triggerPressed = null;
}
}
}
if (!_visible && letterPressed.HasValue && triggerPressed.HasValue)
_keyboardListener.SetHideToolbarEvent(new PowerToys.PowerAccentKeyboardService.HideToolbar((InputType inputType) =>
{
// Keep track if it was triggered with space so that it can be typed on false starts.
_triggeredWithSpace = triggerPressed.Value == TriggerKey.Space;
_visible = true;
_characters = WindowsFunctions.IsCapitalState() ? ToUpper(_settingService.GetLetterKey(letterPressed.Value)) : _settingService.GetLetterKey(letterPressed.Value);
Task.Delay(_settingService.InputTime).ContinueWith(
t =>
Application.Current.Dispatcher.Invoke(() =>
{
SendInputAndHideToolbar(inputType);
});
}));
_keyboardListener.SetNextCharEvent(new PowerToys.PowerAccentKeyboardService.NextChar((TriggerKey triggerKey) =>
{
Application.Current.Dispatcher.Invoke(() =>
{
ProcessNextChar(triggerKey);
});
}));
}
private void ShowToolbar(LetterKey letterKey)
{
_visible = true;
_characters = WindowsFunctions.IsCapitalState() ? ToUpper(SettingsService.GetDefaultLetterKey(letterKey)) : SettingsService.GetDefaultLetterKey(letterKey);
Task.Delay(_settingService.InputTime).ContinueWith(
t =>
{
if (_visible)
{
if (_visible)
OnChangeDisplay?.Invoke(true, _characters);
}
}, TaskScheduler.FromCurrentSynchronizationContext());
}
private void SendInputAndHideToolbar(InputType inputType)
{
switch (inputType)
{
case InputType.Space:
{
WindowsFunctions.Insert(' ');
break;
}
case InputType.Char:
{
if (_selectedIndex != -1)
{
OnChangeDisplay?.Invoke(true, _characters);
WindowsFunctions.Insert(_characters[_selectedIndex], true);
}
}, TaskScheduler.FromCurrentSynchronizationContext());
break;
}
}
if (_visible && triggerPressed.HasValue)
OnChangeDisplay?.Invoke(false, null);
_selectedIndex = -1;
_visible = false;
}
private void ProcessNextChar(TriggerKey triggerKey)
{
if (_visible && _selectedIndex == -1)
{
if (_selectedIndex == -1)
if (triggerKey == TriggerKey.Left)
{
if (triggerPressed.Value == TriggerKey.Left)
{
_selectedIndex = (_characters.Length / 2) - 1;
}
if (triggerPressed.Value == TriggerKey.Right)
{
_selectedIndex = _characters.Length / 2;
}
if (triggerPressed.Value == TriggerKey.Space)
{
_selectedIndex = 0;
}
if (_selectedIndex < 0)
{
_selectedIndex = 0;
}
if (_selectedIndex > _characters.Length - 1)
{
_selectedIndex = _characters.Length - 1;
}
OnSelectCharacter?.Invoke(_selectedIndex, _characters[_selectedIndex]);
return false;
_selectedIndex = (_characters.Length / 2) - 1;
}
if (triggerPressed.Value == TriggerKey.Space)
if (triggerKey == TriggerKey.Right)
{
if (_selectedIndex < _characters.Length - 1)
{
++_selectedIndex;
}
else
{
_selectedIndex = 0;
}
_selectedIndex = _characters.Length / 2;
}
if (triggerPressed.Value == TriggerKey.Left && _selectedIndex > 0)
if (triggerKey == TriggerKey.Space)
{
--_selectedIndex;
_selectedIndex = 0;
}
if (triggerPressed.Value == TriggerKey.Right && _selectedIndex < _characters.Length - 1)
if (_selectedIndex < 0)
{
++_selectedIndex;
_selectedIndex = 0;
}
if (_selectedIndex > _characters.Length - 1)
{
_selectedIndex = _characters.Length - 1;
}
OnSelectCharacter?.Invoke(_selectedIndex, _characters[_selectedIndex]);
return false;
return;
}
return true;
}
private bool PowerAccent_KeyUp(object sender, KeyboardListener.RawKeyEventArgs args)
{
if (Enum.IsDefined(typeof(LetterKey), (int)args.Key))
if (triggerKey == TriggerKey.Space)
{
letterPressed = null;
_stopWatch.Stop();
if (_visible)
if (_selectedIndex < _characters.Length - 1)
{
if (_stopWatch.ElapsedMilliseconds < _settingService.InputTime)
{
/* Debug.WriteLine("Insert before inputTime - " + _stopWatch.ElapsedMilliseconds); */
// False start, we should output the space if it was the trigger.
if (_triggeredWithSpace)
{
WindowsFunctions.Insert(' ');
}
OnChangeDisplay?.Invoke(false, null);
_selectedIndex = -1;
_visible = false;
return false;
}
/* Debug.WriteLine("Insert after inputTime - " + _stopWatch.ElapsedMilliseconds); */
OnChangeDisplay?.Invoke(false, null);
if (_selectedIndex != -1)
{
WindowsFunctions.Insert(_characters[_selectedIndex], true);
}
_selectedIndex = -1;
_visible = false;
++_selectedIndex;
}
else
{
_selectedIndex = 0;
}
}
return true;
if (triggerKey == TriggerKey.Left && _selectedIndex > 0)
{
--_selectedIndex;
}
if (triggerKey == TriggerKey.Right && _selectedIndex < _characters.Length - 1)
{
++_selectedIndex;
}
OnSelectCharacter?.Invoke(_selectedIndex, _characters[_selectedIndex]);
}
public Point GetDisplayCoordinates(Size window)
@@ -182,14 +168,9 @@ public class PowerAccent : IDisposable
return Calculation.GetRawCoordinatesFromPosition(position, screen, window);
}
public char[] GetLettersFromKey(LetterKey letter)
{
return _settingService.GetLetterKey(letter);
}
public void Dispose()
{
_keyboardListener.Dispose();
_keyboardListener.UnInitHook();
GC.SuppressFinalize(this);
}

View File

@@ -7,6 +7,7 @@ namespace PowerAccent.Core.Services;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Enumerations;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
using PowerToys.PowerAccentKeyboardService;
using System.IO.Abstractions;
using System.Text.Json;
@@ -16,10 +17,12 @@ public class SettingsService
private readonly ISettingsUtils _settingsUtils;
private readonly IFileSystemWatcher _watcher;
private readonly object _loadingSettingsLock = new object();
private KeyboardListener _keyboardListener;
public SettingsService()
public SettingsService(KeyboardListener keyboardListener)
{
_settingsUtils = new SettingsUtils();
_keyboardListener = keyboardListener;
ReadSettings();
_watcher = Helper.GetFileWatcher(PowerAccentModuleName, "settings.json", () => { ReadSettings(); });
}
@@ -48,7 +51,10 @@ public class SettingsService
if (settings != null)
{
ActivationKey = settings.Properties.ActivationKey;
_keyboardListener.UpdateActivationKey((int)ActivationKey);
InputTime = settings.Properties.InputTime.Value;
_keyboardListener.UpdateInputTime(InputTime);
switch (settings.Properties.ToolbarPosition.Value)
{
case "Top center":
@@ -79,6 +85,8 @@ public class SettingsService
Position = Position.Center;
break;
}
_keyboardListener.UpdateInputTime(InputTime);
}
}
catch (Exception ex)
@@ -134,32 +142,27 @@ public class SettingsService
}
}
public char[] GetLetterKey(LetterKey letter)
{
return GetDefaultLetterKey(letter);
}
public static char[] GetDefaultLetterKey(LetterKey letter)
{
switch (letter)
{
case LetterKey.A:
case LetterKey.VK_A:
return new char[] { 'à', 'â', 'á', 'ä', 'ã', 'å', 'æ' };
case LetterKey.C:
case LetterKey.VK_C:
return new char[] { 'ć', 'ĉ', 'č', 'ċ', 'ç', 'ḉ' };
case LetterKey.E:
case LetterKey.VK_E:
return new char[] { 'é', 'è', 'ê', 'ë', 'ē', 'ė', '€' };
case LetterKey.I:
case LetterKey.VK_I:
return new char[] { 'î', 'ï', 'í', 'ì', 'ī' };
case LetterKey.N:
case LetterKey.VK_N:
return new char[] { 'ñ', 'ń' };
case LetterKey.O:
case LetterKey.VK_O:
return new char[] { 'ô', 'ö', 'ó', 'ò', 'õ', 'ø', 'œ' };
case LetterKey.S:
case LetterKey.VK_S:
return new char[] { 'š', 'ß', 'ś' };
case LetterKey.U:
case LetterKey.VK_U:
return new char[] { 'û', 'ù', 'ü', 'ú', 'ū' };
case LetterKey.Y:
case LetterKey.VK_Y:
return new char[] { 'ÿ', 'ý' };
}

View File

@@ -1,359 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma warning disable SA1310 // FieldNamesMustNotContainUnderscore
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace PowerAccent.Core.Tools;
internal class KeyboardListener : IDisposable
{
/// <summary>
/// Initializes a new instance of the <see cref="KeyboardListener"/> class.
/// Creates global keyboard listener.
/// </summary>
public KeyboardListener()
{
// We have to store the LowLevelKeyboardProc, so that it is not garbage collected by runtime
_hookedLowLevelKeyboardProc = LowLevelKeyboardProc;
// Set the hook
_hookId = InterceptKeys.SetHook(_hookedLowLevelKeyboardProc);
// Assign the asynchronous callback event
hookedKeyboardCallbackAsync = new KeyboardCallbackAsync(KeyboardListener_KeyboardCallbackAsync);
}
/// <summary>
/// Fired when any of the keys is pressed down.
/// </summary>
public event RawKeyEventHandler KeyDown;
/// <summary>
/// Fired when any of the keys is released.
/// </summary>
public event RawKeyEventHandler KeyUp;
/// <summary>
/// Hook ID
/// </summary>
private readonly IntPtr _hookId = IntPtr.Zero;
/// <summary>
/// Contains the hooked callback in runtime.
/// </summary>
private readonly InterceptKeys.LowLevelKeyboardProc _hookedLowLevelKeyboardProc;
/// <summary>
/// Event to be invoked asynchronously (BeginInvoke) each time key is pressed.
/// </summary>
private KeyboardCallbackAsync hookedKeyboardCallbackAsync;
/// <summary>
/// Raw keyevent handler.
/// </summary>
/// <param name="sender">sender</param>
/// <param name="args">raw keyevent arguments</param>
public delegate bool RawKeyEventHandler(object sender, RawKeyEventArgs args);
/// <summary>
/// Asynchronous callback hook.
/// </summary>
/// <param name="keyEvent">Keyboard event</param>
/// <param name="vkCode">VKCode</param>
/// <param name="character">Character</param>
private delegate bool KeyboardCallbackAsync(InterceptKeys.KeyEvent keyEvent, int vkCode, string character);
/// <summary>
/// Actual callback hook.
/// <remarks>Calls asynchronously the asyncCallback.</remarks>
/// </summary>
/// <param name="nCode">VKCode</param>
/// <param name="wParam">wParam</param>
/// <param name="lParam">lParam</param>
[MethodImpl(MethodImplOptions.NoInlining)]
private IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
if (wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_KEYDOWN ||
wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_KEYUP)
{
// Captures the character(s) pressed only on WM_KEYDOWN
var chars = InterceptKeys.VKCodeToString(
(uint)Marshal.ReadInt32(lParam),
wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_KEYDOWN);
if (!hookedKeyboardCallbackAsync.Invoke((InterceptKeys.KeyEvent)wParam.ToUInt32(), Marshal.ReadInt32(lParam), chars))
{
return (IntPtr)1;
}
}
}
return InterceptKeys.CallNextHookEx(_hookId, nCode, wParam, lParam);
}
/// <summary>
/// HookCallbackAsync procedure that calls accordingly the KeyDown or KeyUp events.
/// </summary>
/// <param name="keyEvent">Keyboard event</param>
/// <param name="vkCode">VKCode</param>
/// <param name="character">Character as string.</param>
private bool KeyboardListener_KeyboardCallbackAsync(InterceptKeys.KeyEvent keyEvent, int vkCode, string character)
{
switch (keyEvent)
{
// KeyDown events
case InterceptKeys.KeyEvent.WM_KEYDOWN:
if (KeyDown != null)
{
return KeyDown.Invoke(this, new RawKeyEventArgs(vkCode, character));
}
break;
// KeyUp events
case InterceptKeys.KeyEvent.WM_KEYUP:
if (KeyUp != null)
{
return KeyUp.Invoke(this, new RawKeyEventArgs(vkCode, character));
}
break;
default:
break;
}
return true;
}
public void Dispose()
{
InterceptKeys.UnhookWindowsHookEx(_hookId);
}
/// <summary>
/// Raw KeyEvent arguments.
/// </summary>
public class RawKeyEventArgs : EventArgs
{
/// <summary>
/// WPF Key of the key.
/// </summary>
#pragma warning disable SA1401 // Fields should be private
public uint Key;
#pragma warning restore SA1401 // Fields should be private
/// <summary>
/// Convert to string.
/// </summary>
/// <returns>Returns string representation of this key, if not possible empty string is returned.</returns>
public override string ToString()
{
return character;
}
/// <summary>
/// Unicode character of key pressed.
/// </summary>
private string character;
/// <summary>
/// Initializes a new instance of the <see cref="RawKeyEventArgs"/> class.
/// Create raw keyevent arguments.
/// </summary>
/// <param name="vKCode">VKCode</param>
/// <param name="character">Character</param>
public RawKeyEventArgs(int vKCode, string character)
{
this.character = character;
Key = (uint)vKCode; // User32.MapVirtualKey((uint)VKCode, User32.MAPVK.MAPVK_VK_TO_VSC_EX);
}
}
}
/// <summary>
/// Winapi Key interception helper class.
/// </summary>
internal static class InterceptKeys
{
public delegate IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam);
private const int WH_KEYBOARD_LL = 13;
/// <summary>
/// Key event
/// </summary>
public enum KeyEvent : int
{
/// <summary>
/// Key down
/// </summary>
WM_KEYDOWN = 256,
/// <summary>
/// Key up
/// </summary>
WM_KEYUP = 257,
/// <summary>
/// System key up
/// </summary>
WM_SYSKEYUP = 261,
/// <summary>
/// System key down
/// </summary>
WM_SYSKEYDOWN = 260,
}
public static IntPtr SetHook(LowLevelKeyboardProc proc)
{
using (Process curProcess = Process.GetCurrentProcess())
using (ProcessModule curModule = curProcess.MainModule)
{
return SetWindowsHookEx(WH_KEYBOARD_LL, proc, (IntPtr)0, 0);
}
}
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, UIntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
// Note: Sometimes single VKCode represents multiple chars, thus string.
// E.g. typing "^1" (notice that when pressing 1 the both characters appear,
// because of this behavior, "^" is called dead key)
[DllImport("user32.dll")]
#pragma warning disable CA1838 // Éviter les paramètres 'StringBuilder' pour les P/Invoke
private static extern int ToUnicodeEx(uint wVirtKey, uint wScanCode, byte[] lpKeyState, [Out, MarshalAs(UnmanagedType.LPWStr)] System.Text.StringBuilder pwszBuff, int cchBuff, uint wFlags, IntPtr dwhkl);
#pragma warning restore CA1838 // Éviter les paramètres 'StringBuilder' pour les P/Invoke
[DllImport("user32.dll")]
private static extern bool GetKeyboardState(byte[] lpKeyState);
[DllImport("user32.dll")]
private static extern uint MapVirtualKeyEx(uint uCode, uint uMapType, IntPtr dwhkl);
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
private static extern IntPtr GetKeyboardLayout(uint dwLayout);
[DllImport("User32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("User32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")]
private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
[DllImport("kernel32.dll")]
private static extern uint GetCurrentThreadId();
private static uint lastVKCode;
private static uint lastScanCode;
private static byte[] lastKeyState = new byte[255];
/// <summary>
/// Convert VKCode to Unicode.
/// <remarks>isKeyDown is required for because of keyboard state inconsistencies!</remarks>
/// </summary>
/// <param name="vKCode">VKCode</param>
/// <param name="isKeyDown">Is the key down event?</param>
/// <returns>String representing single unicode character.</returns>
public static string VKCodeToString(uint vKCode, bool isKeyDown)
{
// ToUnicodeEx needs StringBuilder, it populates that during execution.
System.Text.StringBuilder sbString = new System.Text.StringBuilder(5);
byte[] bKeyState = new byte[255];
bool bKeyStateStatus;
// Gets the current windows window handle, threadID, processID
IntPtr currentHWnd = GetForegroundWindow();
uint currentProcessID;
uint currentWindowThreadID = GetWindowThreadProcessId(currentHWnd, out currentProcessID);
// This programs Thread ID
uint thisProgramThreadId = GetCurrentThreadId();
// Attach to active thread so we can get that keyboard state
if (AttachThreadInput(thisProgramThreadId, currentWindowThreadID, true))
{
// Current state of the modifiers in keyboard
bKeyStateStatus = GetKeyboardState(bKeyState);
// Detach
AttachThreadInput(thisProgramThreadId, currentWindowThreadID, false);
}
else
{
// Could not attach, perhaps it is this process?
bKeyStateStatus = GetKeyboardState(bKeyState);
}
// On failure we return empty string.
if (!bKeyStateStatus)
{
return string.Empty;
}
// Gets the layout of keyboard
IntPtr hkl = GetKeyboardLayout(currentWindowThreadID);
// Maps the virtual keycode
uint lScanCode = MapVirtualKeyEx(vKCode, 0, hkl);
// Keyboard state goes inconsistent if this is not in place. In other words, we need to call above commands in UP events also.
if (!isKeyDown)
{
return string.Empty;
}
// Converts the VKCode to unicode
const uint wFlags = 1 << 2; // If bit 2 is set, keyboard state is not changed (Windows 10, version 1607 and newer)
int relevantKeyCountInBuffer = ToUnicodeEx(vKCode, lScanCode, bKeyState, sbString, sbString.Capacity, wFlags, hkl);
string ret = string.Empty;
switch (relevantKeyCountInBuffer)
{
// dead key
case -1:
break;
case 0:
break;
// Single character in buffer
case 1:
ret = sbString.Length == 0 ? string.Empty : sbString[0].ToString();
break;
// Two or more (only two of them is relevant)
case 2:
default:
ret = sbString.ToString().Substring(0, 2);
break;
}
// Save these
lastScanCode = lScanCode;
lastVKCode = vKCode;
lastKeyState = (byte[])bKeyState.Clone();
return ret;
}
}

View File

@@ -27,9 +27,10 @@ internal static class WindowsFunctions
}
// Letter
var inputsInsert = new User32.INPUT[1]
var inputsInsert = new User32.INPUT[]
{
new User32.INPUT { type = User32.INPUTTYPE.INPUT_KEYBOARD, ki = new User32.KEYBDINPUT { wVk = 0, dwFlags = User32.KEYEVENTF.KEYEVENTF_UNICODE, wScan = c } },
new User32.INPUT { type = User32.INPUTTYPE.INPUT_KEYBOARD, ki = new User32.KEYBDINPUT { wVk = 0, dwFlags = User32.KEYEVENTF.KEYEVENTF_UNICODE | User32.KEYEVENTF.KEYEVENTF_KEYUP, wScan = c } },
};
var temp2 = User32.SendInput((uint)inputsInsert.Length, inputsInsert, sizeof(User32.INPUT));
}