【问题标题】:How to get keyboard scancode of pressed Key?如何获取按键的键盘扫描码?
【发布时间】:2020-12-18 05:54:46
【问题描述】:

我制作了一个 WPF 应用程序,其中热键是根据它们在键盘上的物理位置而不是它们代表的字母来选择的。 因为不同的用户使用不同的布局(qwerty、azerty 等),所以我的应用必须足够智能才能与布局无关。

我指定 QWERTY 作为参考布局。

例如,法国用户使用 AZERTY。因此,QWERTY 中的 Q 键是 AZERTY 中的 A

所以为了不可知论,我想我应该做以下操作:

// Represents, in QWERTY, the top-left letter key.
Key myKey = Key.Q; 

// ScanCode represent the physical key on the keyboard.
var scanCode = GetScanCodeFromKey(myKey, new CultureInfo("en-US")); 

// Would be "A" if the user uses AZERTY.
Key agnosticKey = getKeyFromScanCode(scanCode, CultureInfo.CurrentCulture);

这似乎可行,但我找不到可以执行 GetScanCodeFromKeygetKeyFromScanCode 的函数。

我找到的唯一相关帖子是Is it possible to create a keyboard layout that is identical to the keyboard used? 但不幸的是,它似乎是为 win32 而不是 WPF 制作的。 MapVirtualKeyEx 无法从 WPF 访问。

【问题讨论】:

  • 所以你是在告诉你的法国用户,他们用来输入 a 的左上键实际上是一个 q?为什么要这么做?这不仅忽略了 Windows 设计原则,而且似乎很容易混淆。
  • @Andy 正如我所提到的,重要的是键盘上键的物理位置。例如,想象一下您实现了一个钢琴键盘。您将第一个键映射到第一个音符。关键字母完全无关紧要。
  • 读取键位置而不是实际按下的键似乎没有多大意义。如果您所做的只是处理按下的键,则布局完全不相关,例如热键。当“Q”退出应用程序时,无论用户的布局如何,它都是“Q”。您(应用程序)只需要输入“Q”。用户无论是使用法语、英语、德语还是希腊语键盘,都只关心“Q”。当希腊用户问法国人按哪个热键时,他们并不关心他们是否使用不同的布局。法国用户回答“Q”。
  • 他不必要求使用过的布局来将键转换为“P”(或其他任何内容)。这就是 WPF 没有 API 支持这一点的原因。重要的是输入而不是物理键的位置。真的没有意义。

标签: wpf


【解决方案1】:

我必须指出,编写必须依赖于低级概念(硬件/机器细节)的高级代码(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 扫描码是基于十进制的,即表示为整数,而参考表中的代码是基于十六进制记录的。

基于扫描码的解决方案 #1:System.Windows.Interop.HwndSource(推荐)

HwndSource 是 WPF 就绪的包装器,提供对 Win32 窗口句柄的访问。

请注意,HwndSource 实现了 IDisposable
您可以通过调用HwndSource.AddHook 注册HwndSourceHook 类型的回调来监听Windows 消息循环。
也不要忘记通过调用HwndSource.RemoveHook 来取消注册回调。

要处理击键消息,您必须监视WM_KEYUPWM_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;
}

基于扫描码的解决方案 #2:SetWindowsHookExA

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);
}

【讨论】:

  • 谢谢。这是我想要避免做的事情,因为我必须为每个现有布局创建这样的映射。当然,我当然会将自己限制在常用的 5-6 个范围内。但说真的,没有什么办法可以得到那些该死的关键扫描码吗?
  • 当然有。我提供了一个解决方案来获取按键的扫描码。由于扫描码是一个低级概念(硬件 操作系统),您必须使用 Win32 API 来访问 Windows 消息循环。
  • BionicCode 提供的第二种解决方案是您应该使用的。 @BionicCode,您能否用一个使用示例来补充您的答案,比如说在 Window.PreviewKeyDown (...) 处理程序中?
  • @EldHasp 这是一个 Win32 键盘挂钩。它是 Windows 消息的低级版本,由InputManager 转换为 WPF 键盘事件,可以通过观察Keyboard 事件来处理。这些相同的生成事件也被委托给UIElement。我已更新代码 - 请再次检查。现在第一个扫描码解决方案展示了如何在第二个扫描码解决方案处理按键事件时区分不同的按键事件(向上和向下键)。
  • @EldHasp 第一个扫描码解决方案是Keyboard.KeyUpKeyboard.KeyDown 的低级等价物。该示例显示了如何为这些事件(窗口消息)注册处理程序。
猜你喜欢
  • 2014-09-19
  • 1970-01-01
  • 2013-06-19
  • 2015-03-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-22
相关资源
最近更新 更多