Keyboard.GetKeyStates误报,我如何知道是否按下某个键?

时间:2014-10-02 11:24:14

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

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

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

  1. 按NumPad2
  2. 按LShift
  3. 发布NumPad2
  4. 发布LShift
  5. 从那时起,检查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键是否真的被按下的方式......

1 个答案:

答案 0 :(得分:0)

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

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

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

基本上我找到的解决方法都是不依赖这些电话。

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

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

我们的想法是使用SetWindowsHookEx Win32 API调用对物理键盘使用钩子。

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

<强> 2。使用DirectX。

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

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