混淆按键事件之谜

huangapple go评论82阅读模式
英文:

Case of the confounding key press caper

问题

# 背景

为屏幕录制开发一个基本的、开源的键盘和鼠标在屏幕上显示的桌面应用程序,名为[KmCaster][1]:

[![屏幕录制预览][2]][2]

该应用程序使用[JNativeHook][3]库来接收全局键盘和鼠标事件,因为Swing的[Key][4]和[Mouse][5]监听器只能接收针对应用程序本身的事件。

# 问题

当应用程序失去焦点时,用户界面显示间歇性的按键操作,而不是每次按键操作。然而,控制台显示应用程序已接收到每次按键操作。

# 代码

一个简短、自包含、可编译的示例:

```java
import org.jnativehook.GlobalScreen;
import org.jnativehook.NativeHookException;
import org.jnativehook.keyboard.NativeKeyEvent;
import org.jnativehook.keyboard.NativeKeyListener;

import javax.swing.*;

import static java.util.logging.Level.OFF;
import static java.util.logging.Logger.getLogger;
import static javax.swing.SwingUtilities.invokeLater;
import static org.jnativehook.GlobalScreen.*;
import static org.jnativehook.keyboard.NativeKeyEvent.getKeyText;

public class Harness extends JFrame implements NativeKeyListener {

  private final JLabel mLabel = new JLabel("Hello, world");
  private int mCount;

  public void init() {
    getContentPane().add(mLabel);

    setDefaultCloseOperation(EXIT_ON_CLOSE);
    setLocationRelativeTo(null);
    setAlwaysOnTop(true);
    pack();
    setVisible(true);
  }

  @Override
  public void nativeKeyPressed(final NativeKeyEvent e) {
    final var s = getKeyText(e.getKeyCode());
    System.out.print(s + " " + (++mCount % 10 == 0 ? "\n" : ""));

    invokeLater(() -> mLabel.setText(s));
  }

  public static void main(final String[] args) throws NativeHookException {
    disableNativeHookLogger();
    registerNativeHook();

    final var harness = new Harness();
    addNativeKeyListener(harness);

    invokeLater(harness::init);
  }

  private static void disableNativeHookLogger() {
    final var logger = getLogger(GlobalScreen.class.getPackage().getName());
    logger.setLevel(OFF);
    logger.setUseParentHandlers(false);
  }

  @Override
  public void nativeKeyReleased(final NativeKeyEvent e) {}

  @Override
  public void nativeKeyTyped(final NativeKeyEvent e) {}
}

上面的代码生成一个小窗口,在运行时演示了这个问题:

混淆按键事件之谜

确保在任何其他窗口中输入,以查看演示应用程序中令人困惑的按键丢失。

环境

  • OpenJDK版本 "14.0.1" 2020-04-14,64位
  • XFCE
  • Arch Linux
  • JNativeHook 2.1.0

细节

JNativeHook在它自己的线程中运行,但使用 invokeLater(或 invokeAndWait ?)应该在Swing的事件线程上发出UI更新。

disableNativeHookLogger() 的调用并不相关,它只是在运行演示时保持控制台清洁。

控制台输出

应用程序具有焦点时的控制台输出如下:

Shift I Space A M Space I N S I 
D E Space T H E Space A P P 
L I C A T I O N Period

应用程序失去焦点时的控制台输出如下:

Shift I Space A M Space O U T S
I D E Space T H E Space A P
P L I C A T I O N Period 

因此,清楚地看到在调用 nativeKeyPressed 时不会丢失任何键盘事件,无论应用程序是否具有焦点。也就是说,无论是 JNativeHook 还是通过 JNI 进行的事件冒泡似乎都不是问题的根本原因。

问题

需要做出什么更改,以便在应用程序是否具有焦点的情况下,JLabel 的文本都会更新以显示每次按键操作?

想法

一些有帮助的想法包括:

  • 调用 getDefaultToolkit().sync(); 来显式刷新渲染管线。
  • 在标签上调用 paintImmediately(getBounds())

第一项似乎有很大的差异,但仍然可能会丢失一些按键(尽管可能是因为我输入得太快)。阻止渲染管线合并绘制请求可以避免丢失按键是有道理的。

研究

与这个问题相关的资源:


<details>
<summary>英文:</summary>

# Background

Developing a rudimentary, open-source keyboard and mouse on-screen display desktop application for screen casting, called [KmCaster][1]:

[![Screen Casting Preview][2]][2]

The application uses the [JNativeHook][3] library to receive global keyboard and mouse events, because Swing&#39;s [Key][4] and [Mouse][5] listeners are restricted to receiving events directed at the application itself.

# Problem

When the application loses focus, the user interface shows intermittent key presses, rather than every key press. Yet the console shows that the application has received every key press.

# Code

A short, self-contained, compileable example:

&lt;!-- language: java --&gt;

    import org.jnativehook.GlobalScreen;
    import org.jnativehook.NativeHookException;
    import org.jnativehook.keyboard.NativeKeyEvent;
    import org.jnativehook.keyboard.NativeKeyListener;
    
    import javax.swing.*;
    
    import static java.util.logging.Level.OFF;
    import static java.util.logging.Logger.getLogger;
    import static javax.swing.SwingUtilities.invokeLater;
    import static org.jnativehook.GlobalScreen.*;
    import static org.jnativehook.keyboard.NativeKeyEvent.getKeyText;

    public class Harness extends JFrame implements NativeKeyListener {
    
      private final JLabel mLabel = new JLabel( &quot;Hello, world&quot; );
      private int mCount;
    
      public void init() {
        getContentPane().add( mLabel );
    
        setDefaultCloseOperation( EXIT_ON_CLOSE );
        setLocationRelativeTo( null );
        setAlwaysOnTop( true );
        pack();
        setVisible( true );
      }
    
      @Override
      public void nativeKeyPressed( final NativeKeyEvent e ) {
        final var s = getKeyText( e.getKeyCode() );
        System.out.print( s + &quot; &quot; + (++mCount % 10 == 0 ? &quot;\n&quot; : &quot;&quot;) );
    
        invokeLater( () -&gt; mLabel.setText( s ) );
      }
    
      public static void main( final String[] args ) throws NativeHookException {
        disableNativeHookLogger();
        registerNativeHook();
    
        final var harness = new Harness();
        addNativeKeyListener( harness );
    
        invokeLater( harness::init );
      }
    
      private static void disableNativeHookLogger() {
        final var logger = getLogger( GlobalScreen.class.getPackage().getName() );
        logger.setLevel( OFF );
        logger.setUseParentHandlers( false );
      }

      @Override
      public void nativeKeyReleased( final NativeKeyEvent e ) {}
    
      @Override
      public void nativeKeyTyped( final NativeKeyEvent e ) {}
    }

The code above produces a small window that, when run, demonstrates the problem:

[![Harness Screenshot][6]][6]

Be sure to type into any other window to see the perplexing loss of key presses within the demo application.

# Environment

* OpenJDK version &quot;14.0.1&quot; 2020-04-14, 64-bit
* XFCE
* Arch Linux
* JNativeHook 2.1.0

# Details

JNativeHook runs in its own thread, but using `invokeLater` (or `invokeAndWait`?) should issue the UI update on Swing&#39;s event thread.

The call to `disableNativeHookLogger()` isn&#39;t relevant, it merely keeps the console clean when running the demo.

# Console Output

Here is the console output when the application has focus:

&lt;!-- language: plain --&gt;

    Shift I Space A M Space I N S I 
    D E Space T H E Space A P P 
    L I C A T I O N Period

Here is the console output when the application loses focus:

&lt;!-- language: plain --&gt;

    Shift I Space A M Space O U T S
    I D E Space T H E Space A P
    P L I C A T I O N Period 

So it&#39;s clear that no keyboard events are missing when `nativeKeyPressed` is called, regardless of whether the application has focus. That is, neither JNativeHook nor its event bubbling via JNI appears to be the culprit.

# Question

What needs to change so that the `JLabel` text is updated for every key press regardless of whether the application has focus?

# Ideas

Some items that help include:

* Call `getDefaultToolkit().sync();` to flush the rendering pipeline explicitly.
* Call `paintImmediately( getBounds() )` on the label.

The first item seems to make a huge difference, but some keys still appear to be missing (although it could be that I&#39;m typing too quickly). It makes sense that preventing the rendering pipeline from merging paint requests avoids loss of key strokes.

# Research

Resources related to this issue:

* https://pavelfatin.com/low-latency-painting-in-awt-and-swing/

  [1]: https://github.com/DaveJarvis/kmcaster
  [2]: https://i.stack.imgur.com/wBDzs.png
  [3]: https://github.com/kwhat/jnativehook/
  [4]: https://docs.oracle.com/javase/9/docs/api/java/awt/event/KeyListener.html
  [5]: https://docs.oracle.com/javase/9/docs/api/java/awt/event/MouseListener.html
  [6]: https://i.stack.imgur.com/HiU3G.png

</details>


# 答案1
**得分**: 1

使用默认工具包调用`sync()`:

```java
      @Override
      public void propertyChange( final PropertyChangeEvent e ) {
        invokeLater(
            () -> {
              update( e );
    
              // 防止折叠多个绘图事件。
              getDefaultToolkit().sync();
            }
        );
      }

查看完整代码

英文:

Call sync() using the default toolkit:

<!-- language: java -->

  @Override
  public void propertyChange( final PropertyChangeEvent e ) {
    invokeLater(
        () -&gt; {
          update( e );

          // Prevent collapsing multiple paint events.
          getDefaultToolkit().sync();
        }
    );
  }

See the full code.

huangapple
  • 本文由 发表于 2020年7月27日 07:44:56
  • 转载请务必保留本文链接:https://go.coder-hub.com/63106947.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定