键盘.GetKeyStates存在假阳性,我该如何确切知道一个键是否被按下?

4
Keyboard.GetKeyStates 可能会返回错误的按键状态,例如当调用 Keyboard.GetKeyStates(Key.NumPad2) 时,即使未按下该键,也可能返回 Down, Toggled 的结果。
我已经能够在一个非常简单的 WPF 应用程序中通过捕获 KeyUp 事件来复现此问题。要复现此 bug,请按照以下步骤进行操作:
1. 按下 NumPad2 键。 2. 按下 LShift 键。 3. 松开 NumPad2 键。 4. 松开 LShift 键。
从那时起,检查 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

从上面的步骤3和4可以看出,即使我在步骤3中释放了NumPad2键,它仍会显示为按下。

以下是完整的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版本,包括(x64/x32)和.NET版本。 例如,在Windows 98中存在类似的问题,但在Win XP中不存在。 - Alex Blokha
3
系统通过每个线程维护键盘状态,每当线程从输入队列中检索消息时。以这种方式维护的状态受到可访问性增强(例如粘滞键)的影响。如果要确定按键的物理状态,则需要深入系统状态管理。例如,使用DirectInput可以查询硬件驱动程序以获取原始输入数据。 - IInspectable
@IInspectable 很有用的见解。你应该把你的评论扩展成一个答案。 - Mike Strobel
@IInspectable 我完全不熟悉DirectX。我刚刚花了一定的时间尝试从我的项目中引用DirectInput,但我不知道该怎么做! - Evren Kuzucuoglu
这是一个Windows的怪癖,你必须处理它。从键盘生成的扫描码到您看到的虚拟键码的转换纯粹基于当前键盘状态。没有“记忆”先前生成的虚拟键码。如果您想知道某个键是否按下,请只检索其状态,不要自己缓冲该状态。GetAsyncKeyState()值得注意,很少适用。 - Hans Passant
1个回答

1
所以问题归结为Windows报告按键的方式存在错误。
Windows在物理键盘上创建了一个抽象层,称为虚拟键盘。虚拟键盘侦听物理键盘事件,并使用它来维护键的状态,稍后可以通过Windows API调用(其中包括User32.dll调用GetKeyState、GetAsyncKeyState和GetKeyboardState)或包装这些API调用的.NET调用(System.Windows.Input.Keyboard命名空间中的调用)检索。
在这种情况下,它接收到NumPad2的KeyDown事件,但是对于Down(NumPad2的shifted版本),它接收到KeyUp事件,因此就那些调用而言,NumPad2的状态保持按下状态。
基本上,我找到的解决方法都是不依赖于这些调用。
以下是一些我发现可行的解决方法:
1. 使用Windows API直接钩取物理键盘事件。
思路是使用SetWindowsHookEx Win32 API调用对物理键盘进行钩取。
此方法的主要问题是API仅提供事件,没有状态。您必须自己维护键盘所有键的状态才能使其正常工作。

2. 使用DirectX。

我找到的另一个解决方案是使用DirectInput,它是DirectX的一部分。这会增加对DirectX的依赖(如果您的应用程序不支持Windows Vista之前的任何版本,则没有问题)。此外,除了第三方库之外,托管代码无法直接访问DirectX(以前有一个Microsoft .NET包装器,作为DirectX API的一部分,但它已经过时,不能与最新版本的.NET一起使用)。 您可以选择:

  • 使用第三方.NET库(我使用一个这样的库创建了一个工作概念证明,SharpDX似乎提供了在运行的机器上对安装的DirectX版本不可知的优势)。
  • 为此特定用途创建一个使用C++ DirectX API的C++库。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接