我必须指出,编写必须依赖于低级概念(硬件/机器细节)的高级代码(WPF 应用程序)绝不是一个好主意。高级应用程序应该依赖于低级数据输出的高级抽象(输入)。您总是希望在高级和低级或软件和硬件之间添加额外的抽象层/接口。
自定义映射
您当然可以自己进行映射。使用 Win32 API 可以让您获取当前活动的输入语言并处理键盘布局的低级概念。
首先选择应用程序的基本布局,例如zh-CN。然后根据支持的布局语言创建查找表。
该表的每个条目都是另一个用作转换表的表:从当前布局到应用程序的内部基本布局。
为了简化查找表的创建,您可以通过仅关注受支持的键来创建最小表。
您可以创建从文件(例如 XML 或 JSON)到查找表的转换表。这为已部署的应用程序提供了简单的扩展性。
private Dictionary<int, Dictionary<Key, Key>> LanguageLayouts { get; set; }
public MainWindow()
{
InitializeComponent();
this.LanguageLayouts = new Dictionary<int, Dictionary<Key, Key>>
{
{
CultureInfo.GetCultureInfo("fr-FR").LCID, new Dictionary<Key, Key>
{
{Key.A, Key.Q},
{Key.Z, Key.W},
{Key.E, Key.E},
{Key.R, Key.R},
{Key.T, Key.T},
{Key.Y, Key.Y}
}
}
};
}
private void OnPreviewKeyUp(object sender, KeyEventArgs e)
{
int lcid = GetCurrentKeyboardLayout().LCID;
if (this.LanguageLayouts.TryGetValue(lcid, out Dictionary<Key, Key> currentLayoutMap))
{
if (currentLayoutMap.TryGetValue(e.Key, out Key convertedKey))
{
HandlePressedKey(convertedKey);
}
}
}
[DllImport("user32.dll")] static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hwnd, IntPtr proccess);
[DllImport("user32.dll")] static extern IntPtr GetKeyboardLayout(uint thread);
public CultureInfo GetCurrentKeyboardLayout()
{
IntPtr foregroundWindow = GetForegroundWindow();
uint foregroundProcess = GetWindowThreadProcessId(foregroundWindow, IntPtr.Zero);
int keyboardLayout = GetKeyboardLayout(foregroundProcess).ToInt32() & 0xFFFF;
return new CultureInfo(keyboardLayout);
}
由于键盘扫描码是一个低级概念(硬件 操作系统),因此您必须使用 Win32 API 来挂钩 Windows 消息循环并过滤击键消息。
Windows 键盘驱动程序通常使用scancode set 1 代码表将扫描代码转换为实际的键值。
Windows API 扫描码是基于十进制的,即表示为整数,而参考表中的代码是基于十六进制记录的。
HwndSource 是 WPF 就绪的包装器,提供对 Win32 窗口句柄的访问。
请注意,HwndSource 实现了 IDisposable。
您可以通过调用HwndSource.AddHook 注册HwndSourceHook 类型的回调来监听Windows 消息循环。
也不要忘记通过调用HwndSource.RemoveHook 来取消注册回调。
要处理击键消息,您必须监视WM_KEYUP 和WM_KEYDOWN 消息的循环。有关键盘输入消息的列表,请参阅 Keyboard Input Notifications。
请参阅 System-Defined Messages 了解所有系统消息的概述,例如 Clipborad 消息。
private async void OnLoaded(object sender, RoutedEventArgs e)
{
var hwndSource = PresentationSource.FromVisual(this) as HwndSource;
if (hwndSource != null)
{
hwndSource.AddHook(MainWindow.OnWindowsMessageReceived);
}
// Define filter constants (see docs for message codes)
private const int WM_KEYDOWN = 0x0100;
private const int WM_KEYUP = 0x0101;
private static IntPtr OnWindowsMessageReceived(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
switch (msg)
{
// Equivalent of Keyboard.KeyDown event (or UIElement.KeyDown)
case MainWindow.WM_KEYDOWN:
{
// Parameter 'wParam' is the Virtual-Key code.
// Convert the Win32 Virtual-Key into WPF Key
// using 'System.Windows.Input.KeyInterop'.
// The Key is the result of the virtual code that is already translated to the current layout.
// While the scancode remains the same, the Key changes according to the keyboard layout.
Key key = KeyInterop.KeyFromVirtualKey(wParam.ToInt32());
// TODO::Handle Key
// Parameter 'lParam' bit 16 - 23 represents the decimal scancode.
// See scancode set 1 for key to code mapping (note: table uses hex codes!): https://www.win.tue.nl/~aeb/linux/kbd/scancodes-10.html).
// Use bit mask to get the scancode related bits (16 - 23).https://www.win.tue.nl/~aeb/linux/kbd/scancodes-10.html)
var scancode = (lParam.ToInt64() >> 16) & 0xFF;
//TODO::Handle scancode
break;
}
// Equivalent of Keyboard.KeyUp event (or UIElement.KeyUp)
case MainWindow.WM_KEYUP:
{
// Parameter 'wParam' is the Virtual-Key code.
// Convert the Win32 Virtual-Key into WPF Key
// using 'System.Windows.Input.KeyInterop'.
// The Key is the result of the virtual code that is already translated to the current layout.
// While the scancode remains the same, the Key changes according to the keyboard layout.
Key key = KeyInterop.KeyFromVirtualKey(wParam.ToInt32());
// TODO::Handle Key
// Parameter 'lParam' bit 16 - 23 represents the decimal scancode.
// See scancode set 1 for key to code mapping (note: table uses hex codes!): https://www.win.tue.nl/~aeb/linux/kbd/scancodes-10.html).
// Use bit mask to get the scancode related bits (16 - 23).
var scancode = (lParam.ToInt64() >> 16) & 0xFF;
//TODO::Handle scancode
break;
}
}
return IntPtr.Zero;
}
private void Initialize()
{
// Keep a strong reference to the delegate.
// Otherwise it will get garbage collected (Win32 API won't keep a reference to the delegate).
this.CallbackDelegate = OnKeyPressed;
// Argument '2' (WH_KEYBOARD) defines keystroke message monitoring (message filter).
// Argument 'OnKeyPressed' defines the callback.
SetWindowsHookEx(2, this.CallbackDelegate, IntPtr.Zero, AppDomain.GetCurrentThreadId());
}
protected delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);
private HookProc CallbackDelegate { get; set; }
[DllImport("user32.dll")]
protected static extern IntPtr SetWindowsHookEx(int code, HookProc func, IntPtr hInstance, int threadID);
[DllImport("user32.dll")]
static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
private IntPtr OnKeyPressed(int code, IntPtr wParam, IntPtr lParam)
{
// Parameter 'wParam' is the Virtual-Key code.
// Convert the Win32 Virtual-Key into WPF Key
// using 'System.Windows.Input.KeyInterop'
Key key = KeyInterop.KeyFromVirtualKey(wParam.ToInt32());
// TODO::Handle Key
// Parameter 'lParam' bit 16 - 23 represents the decimal scancode.
// See scancode set 1 for key to code mapping (note: table uses hex codes!): https://www.win.tue.nl/~aeb/linux/kbd/scancodes-10.html).
// Use bit mask to get the scancode related bits (16 - 23).
var scancode = (lParam.ToInt64() >> 16) & 0xFF;
// TODO::Handle scancode
// Let other callbacks handle the message too
return CallNextHookEx(IntPtr.Zero, code, wParam, lParam);
}