Keyboard.GetKeyStates 似乎有一种方法可以返回错误按下的键,例如,即使没有按下,Keyboard.GetKeyStates(Key.NumPad2) 也会返回 Down, Toggled

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

  • 按 NumPad2
  • 按 LShift
  • 发布 NumPad2
  • 释放 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
    

    因此,您可以看到 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 调用的方法:
  • GetKeyState
  • GetKeyboardState
  • GetAsyncKeyStates

  • 而且他们都有完全相同的问题(这并不奇怪,因为我认为他们的内部工作无论如何都与他们的 .net 等效 Keyboard.GetKeyStates 共享)。

    现在我正在寻找的是一种随时可以真正知道 NumPad2 键是否真的被按下的方法......

    最佳答案

    因此,问题归结为 Windows 报告按键方式的错误。

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

    在这种情况下,它接收 NumPad2 的 KeyDown 事件,但接收 Down(这是 NumPad2 的移位版本)的 KeyUp 事件,因此就这些调用而言,NumPad2 的状态保持按下状态。

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

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

    1. 使用 Windows API 直接 Hook 物理键盘事件。

    这个想法是使用 SetWindowsHookEx Win32 API 调用连接到物理键盘。

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

    2. 使用 DirectX。

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

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