c# - Keyboard.GetKeyStates 误报,我如何真正知道是否按下了某个键?

标签 c# .net wpf winapi keyboard-events

Keyboard.GetKeyStates 似乎有一种方法可以返回错误按下的键,例如 Keyboard.GetKeyStates(Key.NumPad2) 可以返回 向下,切换,即使没有按下也是如此。

我已经能够在捕获 keyUp 事件的非常简单的 WPF 应用程序上重现这一点。 重现bug的方法如下:

  1. 按 NumPad2
  2. 按 LShift
  3. 发布 NumPad2
  4. 释放左移

从那时起,检查 NumPad2 的状态将始终产生 Down, Toggled,直到您再次按下并释放它。

不确定这是否重要,但我在 Windows 8.1 x64

上使用英语英国扩展键盘

原因似乎是 LShift-NumPad2 实际上等同于向下键(有道理),但 Windows 似乎没有意识到这意味着不再按下 NumPad2。

我的测试应用程序只是捕获 KeyDown 和 KeyUp,并向我显示每个事件的 KeyStates 变化以及每个事件后整个键盘的 KeyStates 列表(我将其与应用程序启动时的状态进行比较,以免用 NumLock 键和其他键的状态污染输出。

这是我在之前的测试中得到的输出:

MainWindow_OnKeyDown NumPad2: Down, Toggled -> Down, Toggled
KeyStates:
    NumPad2: Down, Toggled

MainWindow_OnKeyDown LeftShift: None -> Down, Toggled
KeyStates:
    NumPad2: Down, Toggled
    LeftShift: Down, Toggled

MainWindow_OnKeyUp LeftShift: Down, Toggled -> Toggled
KeyStates:
    NumPad2: Down, Toggled
    LeftShift: Toggled

MainWindow_OnKeyUp Down: None -> None
KeyStates:
    NumPad2: Down, Toggled
    LeftShift: Toggled

MainWindow_OnKeyUp LeftShift: Toggled -> None
KeyStates:
    NumPad2: Down, Toggled

所以你可以看到 NumPad2 键在第 3 步和第 4 步后显示为已按下,尽管我在第 3 步释放了它。

这是 xaml 的完整代码和隐藏代码,以防您想将其直接复制/粘贴到新项目中并想查看它的实际效果:

<Window x:Class="WpfKeyboardTester.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525"
        Loaded="MainWindow_OnLoaded"
        KeyDown="MainWindow_OnKeyDown"
        KeyUp="MainWindow_OnKeyUp"
        WindowStartupLocation="CenterScreen">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <ScrollViewer
            Grid.Row="0"
            Name="ScrollViewer"
            x:FieldModifier="private">
            <TextBox
                Name="TextBox"
                IsReadOnly="True"
                x:FieldModifier="private"/>
        </ScrollViewer>
        <Button
            Grid.Row="1"
            Click="Clear_OnClick">
            Clear
        </Button>
    </Grid>
</Window>

   public partial class MainWindow
   {
      private Dictionary<Key, KeyStates> _initialKeyStates;
      private Dictionary<Key, KeyStates> _keyStates;
      private Key _previousKeyDown;
      private Key _previousKeyUp;

      public MainWindow()
      {
         InitializeComponent();
      }

      private void MainWindow_OnLoaded(object sender, RoutedEventArgs e)
      {
         _keyStates = GetKeyStates();
         _initialKeyStates = _keyStates;
      }

      private void MainWindow_OnKeyDown(object sender, KeyEventArgs e)
      {
         if (e.Key != _previousKeyDown)
         {
            AppendKeyEventDescription("MainWindow_OnKeyDown", e);
            _previousKeyDown = e.Key;            
         }
      }

      private void MainWindow_OnKeyUp(object sender, KeyEventArgs e)
      {
         if (e.Key != _previousKeyUp)
         {
            AppendKeyEventDescription("MainWindow_OnKeyUp", e);
            _previousKeyUp = e.Key;
         }
      }

      private void Clear_OnClick(object sender, RoutedEventArgs e)
      {
         TextBox.Text = string.Empty;
      }

      private static Dictionary<Key, KeyStates> GetKeyStates()
      {
         return Enum.GetValues(typeof (Key))
            .Cast<Key>()
            .Distinct()
            .Where(x => x != Key.None)
            .ToDictionary(
               x => x,
               Keyboard.GetKeyStates);
      }

      private void AppendKeyEventDescription(string eventName, KeyEventArgs e)
      {
         if (TextBox.Text != string.Empty)
            TextBox.Text += "\n\n";
         TextBox.Text +=
            eventName + " " + e.Key + ": " + _keyStates[e.Key] + " -> " + e.KeyStates;

         _keyStates = GetKeyStates();

         var changedKeys = _keyStates.Keys
            .Where(key => _keyStates[key] != _initialKeyStates[key])
            .ToArray();

         if (changedKeys.Any())
         {
            TextBox.Text += changedKeys.Aggregate(
               "\nKeyStates:",
               (text, key) => text + "\n\t" + key + ": " + _keyStates[key]);
         }
      }
   }

我探索了其他几种调用 Win32 API 的方法:

而且它们都有完全相同的问题(这并不奇怪,因为我认为它们的内部工作原理与它们的 .net 等效 Keyboard.GetKeyStates 是共享的)。

现在我正在寻找一种方法,可以在任何时候真正知道是否真的按下了 NumPad2 键...

最佳答案

所以这个问题归结为 Windows 报告按键的方式中的错误。

Windows 在物理键盘上创建了一个抽象层,称为虚拟键盘。虚拟键盘监听物理键盘事件并使用它来维护键的状态,稍后可以通过 Windows API 调用(其中 User32.dll 调用 GetKeyState、GetAsyncKeyState 和 GetKeyboardState)或包装这些 API 调用的 .NET 调用来检索这些键的状态(System.Windows.Input.Keyboard 命名空间中的那些)。

在这种情况下,它会收到 NumPad2 的 KeyDown 事件,但会收到 Down 的 KeyUp 事件(这是 NumPad2 的 shifted 版本),因此 NumPad2 的状态在这些调用期间保持按下状态很关心。

基本上,我发现的解决方法都是不依赖这些调用。

以下是我发现的一些可行的解决方法:

<强>1。使用 Windows API 直接连接到物理键盘事件。

想法是使用 SetWindowsHookEx Win32 API 调用来使用物理键盘的 Hook 。

这种方法的主要问题是 API 只提供事件,不提供状态。您必须维护键盘上所有键的状态才能使其正常工作。

<强>2。使用 DirectX。

我找到的另一个解决方案是使用 DirectInput,它是 DirectX 的一部分。这增加了对 DirectX 的依赖(如果您的应用程序在 Windows Vista 之前不支持任何东西,这很好)。此外,除了第三方库之外,无法从托管代码直接访问 DirectX(以前有一个 Microsoft .NET 包装器围绕旧版本的 DirectX 作为 DirectX API 的一部分,但它已经过时并且不适用于最新版本.NET)。 您可以:

  • 使用第三方 .NET 库(我已经使用一个这样的库 SharpDX 创建了一个有效的概念证明,它似乎提供了与运行它的机器上安装的 DirectX 版本无关的优势)。
  • 为此特定用途使用 C++ DirectX API 创建 C++ 库。

关于c# - Keyboard.GetKeyStates 误报,我如何真正知道是否按下了某个键?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26159752/

相关文章:

c# - 嵌套类 .GetType()

.net - Windows.Forms.Timer作用域

.net - 安排 Azure 媒体服务中的实时流媒体 channel 在特定时间以编程方式启动

c# - WPF : Clear TextBox text after enter key press

wpf - XAML缩放问题

c# - 给Listview的特定行添加背景色

c# - 添加项目到集合的更新 UI 已更改

c# - 检查结果是否来自 SQL 中的特定表

c# - 在 MemoryStream 中拆分 mp3 音频的正确方法是什么?

.net - 如何在 WCF 服务中将密码与我的证书 (X.509) 一起发送?