【问题标题】:Keyboard.GetKeyStates false positive, how do I actually know if a key is pressed?Keyboard.GetKeyStates 误报,我怎么知道是否按下了键?
【发布时间】:2014-10-02 11:24:13
【问题描述】:

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 实际上等同于 Down 键(有道理),但 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版本,包括(x64/x32)和.NET版本。例如,类似的问题出现在 Windows 98 中,但在 Win XP 中不存在。
  • 键盘状态由系统维护每个线程,只要线程从输入队列中检索消息。以这种方式维护的状态会受到可访问性增强(例如粘滞键)的影响。如果要确定密钥的物理状态,则需要进入系统的状态管理之下。例如,使用DirectInput,您可以查询硬件驱动程序以获取原始输入数据。
  • @IInspectable 非常有用的见解。您应该将您的评论扩展到答案。
  • @IInspectable 我对 DirectX 完全不熟悉。我刚刚花了一定时间尝试从我的项目中引用 DirectInput,但我不知道如何!
  • 这是您必须处理的 Windows 怪癖。从键盘生成的扫描码到您看到的虚拟键码的转换完全基于当前键盘状态。没有“记忆”先前生成的虚拟键码。如果您想知道某个键是否已关闭,则只需检索其状态,不要自己缓冲该状态。 GetAsyncKeyState() 值得注意,很少合适。

标签: c# .net wpf winapi keyboard-events


【解决方案1】:

所以问题归结为 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 调用来使用物理键盘的挂钩。

这种方法的主要问题是 API 只提供事件,不提供状态。您必须保持自己的键盘所有键状态才能正常工作。

2。使用 DirectX。

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-01-20
    相关资源
    最近更新 更多