混淆按键事件之谜

huangapple 未分类评论63阅读模式
英文:

Case of the confounding key press caper

问题

  1. # 背景
  2. 为屏幕录制开发一个基本的、开源的键盘和鼠标在屏幕上显示的桌面应用程序,名为[KmCaster][1]:
  3. [![屏幕录制预览][2]][2]
  4. 该应用程序使用[JNativeHook][3]库来接收全局键盘和鼠标事件,因为Swing的[Key][4]和[Mouse][5]监听器只能接收针对应用程序本身的事件。
  5. # 问题
  6. 当应用程序失去焦点时,用户界面显示间歇性的按键操作,而不是每次按键操作。然而,控制台显示应用程序已接收到每次按键操作。
  7. # 代码
  8. 一个简短、自包含、可编译的示例:
  9. ```java
  10. import org.jnativehook.GlobalScreen;
  11. import org.jnativehook.NativeHookException;
  12. import org.jnativehook.keyboard.NativeKeyEvent;
  13. import org.jnativehook.keyboard.NativeKeyListener;
  14. import javax.swing.*;
  15. import static java.util.logging.Level.OFF;
  16. import static java.util.logging.Logger.getLogger;
  17. import static javax.swing.SwingUtilities.invokeLater;
  18. import static org.jnativehook.GlobalScreen.*;
  19. import static org.jnativehook.keyboard.NativeKeyEvent.getKeyText;
  20. public class Harness extends JFrame implements NativeKeyListener {
  21. private final JLabel mLabel = new JLabel("Hello, world");
  22. private int mCount;
  23. public void init() {
  24. getContentPane().add(mLabel);
  25. setDefaultCloseOperation(EXIT_ON_CLOSE);
  26. setLocationRelativeTo(null);
  27. setAlwaysOnTop(true);
  28. pack();
  29. setVisible(true);
  30. }
  31. @Override
  32. public void nativeKeyPressed(final NativeKeyEvent e) {
  33. final var s = getKeyText(e.getKeyCode());
  34. System.out.print(s + " " + (++mCount % 10 == 0 ? "\n" : ""));
  35. invokeLater(() -> mLabel.setText(s));
  36. }
  37. public static void main(final String[] args) throws NativeHookException {
  38. disableNativeHookLogger();
  39. registerNativeHook();
  40. final var harness = new Harness();
  41. addNativeKeyListener(harness);
  42. invokeLater(harness::init);
  43. }
  44. private static void disableNativeHookLogger() {
  45. final var logger = getLogger(GlobalScreen.class.getPackage().getName());
  46. logger.setLevel(OFF);
  47. logger.setUseParentHandlers(false);
  48. }
  49. @Override
  50. public void nativeKeyReleased(final NativeKeyEvent e) {}
  51. @Override
  52. public void nativeKeyTyped(final NativeKeyEvent e) {}
  53. }

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

混淆按键事件之谜

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

环境

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

细节

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

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

控制台输出

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

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

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

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

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

问题

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

想法

一些有帮助的想法包括:

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

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

研究

与这个问题相关的资源:

  1. <details>
  2. <summary>英文:</summary>
  3. # Background
  4. Developing a rudimentary, open-source keyboard and mouse on-screen display desktop application for screen casting, called [KmCaster][1]:
  5. [![Screen Casting Preview][2]][2]
  6. 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.
  7. # Problem
  8. 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.
  9. # Code
  10. A short, self-contained, compileable example:
  11. &lt;!-- language: java --&gt;
  12. import org.jnativehook.GlobalScreen;
  13. import org.jnativehook.NativeHookException;
  14. import org.jnativehook.keyboard.NativeKeyEvent;
  15. import org.jnativehook.keyboard.NativeKeyListener;
  16. import javax.swing.*;
  17. import static java.util.logging.Level.OFF;
  18. import static java.util.logging.Logger.getLogger;
  19. import static javax.swing.SwingUtilities.invokeLater;
  20. import static org.jnativehook.GlobalScreen.*;
  21. import static org.jnativehook.keyboard.NativeKeyEvent.getKeyText;
  22. public class Harness extends JFrame implements NativeKeyListener {
  23. private final JLabel mLabel = new JLabel( &quot;Hello, world&quot; );
  24. private int mCount;
  25. public void init() {
  26. getContentPane().add( mLabel );
  27. setDefaultCloseOperation( EXIT_ON_CLOSE );
  28. setLocationRelativeTo( null );
  29. setAlwaysOnTop( true );
  30. pack();
  31. setVisible( true );
  32. }
  33. @Override
  34. public void nativeKeyPressed( final NativeKeyEvent e ) {
  35. final var s = getKeyText( e.getKeyCode() );
  36. System.out.print( s + &quot; &quot; + (++mCount % 10 == 0 ? &quot;\n&quot; : &quot;&quot;) );
  37. invokeLater( () -&gt; mLabel.setText( s ) );
  38. }
  39. public static void main( final String[] args ) throws NativeHookException {
  40. disableNativeHookLogger();
  41. registerNativeHook();
  42. final var harness = new Harness();
  43. addNativeKeyListener( harness );
  44. invokeLater( harness::init );
  45. }
  46. private static void disableNativeHookLogger() {
  47. final var logger = getLogger( GlobalScreen.class.getPackage().getName() );
  48. logger.setLevel( OFF );
  49. logger.setUseParentHandlers( false );
  50. }
  51. @Override
  52. public void nativeKeyReleased( final NativeKeyEvent e ) {}
  53. @Override
  54. public void nativeKeyTyped( final NativeKeyEvent e ) {}
  55. }
  56. The code above produces a small window that, when run, demonstrates the problem:
  57. [![Harness Screenshot][6]][6]
  58. Be sure to type into any other window to see the perplexing loss of key presses within the demo application.
  59. # Environment
  60. * OpenJDK version &quot;14.0.1&quot; 2020-04-14, 64-bit
  61. * XFCE
  62. * Arch Linux
  63. * JNativeHook 2.1.0
  64. # Details
  65. JNativeHook runs in its own thread, but using `invokeLater` (or `invokeAndWait`?) should issue the UI update on Swing&#39;s event thread.
  66. The call to `disableNativeHookLogger()` isn&#39;t relevant, it merely keeps the console clean when running the demo.
  67. # Console Output
  68. Here is the console output when the application has focus:
  69. &lt;!-- language: plain --&gt;
  70. Shift I Space A M Space I N S I
  71. D E Space T H E Space A P P
  72. L I C A T I O N Period
  73. Here is the console output when the application loses focus:
  74. &lt;!-- language: plain --&gt;
  75. Shift I Space A M Space O U T S
  76. I D E Space T H E Space A P
  77. P L I C A T I O N Period
  78. 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.
  79. # Question
  80. What needs to change so that the `JLabel` text is updated for every key press regardless of whether the application has focus?
  81. # Ideas
  82. Some items that help include:
  83. * Call `getDefaultToolkit().sync();` to flush the rendering pipeline explicitly.
  84. * Call `paintImmediately( getBounds() )` on the label.
  85. 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.
  86. # Research
  87. Resources related to this issue:
  88. * https://pavelfatin.com/low-latency-painting-in-awt-and-swing/
  89. [1]: https://github.com/DaveJarvis/kmcaster
  90. [2]: https://i.stack.imgur.com/wBDzs.png
  91. [3]: https://github.com/kwhat/jnativehook/
  92. [4]: https://docs.oracle.com/javase/9/docs/api/java/awt/event/KeyListener.html
  93. [5]: https://docs.oracle.com/javase/9/docs/api/java/awt/event/MouseListener.html
  94. [6]: https://i.stack.imgur.com/HiU3G.png
  95. </details>
  96. # 答案1
  97. **得分**: 1
  98. 使用默认工具包调用`sync()`:
  99. ```java
  100. @Override
  101. public void propertyChange( final PropertyChangeEvent e ) {
  102. invokeLater(
  103. () -> {
  104. update( e );
  105. // 防止折叠多个绘图事件。
  106. getDefaultToolkit().sync();
  107. }
  108. );
  109. }

查看完整代码

英文:

Call sync() using the default toolkit:

<!-- language: java -->

  1. @Override
  2. public void propertyChange( final PropertyChangeEvent e ) {
  3. invokeLater(
  4. () -&gt; {
  5. update( e );
  6. // Prevent collapsing multiple paint events.
  7. getDefaultToolkit().sync();
  8. }
  9. );
  10. }

See the full code.

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

发表评论

匿名网友

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

确定