如何知道用户何时真正发布了Java中的密钥?

(为清晰起见编辑)

我想检测用户何时按下并释放Java Swing中的键,忽略键盘自动重复function。 我也希望在Linux,Mac OS和Windows上使用纯Java方法。

要求:

  1. 当用户按下某个键时,我想知道它是什么键;
  2. 当用户释放一些密钥时,我想知道它是什么密钥;
  3. 我想忽略系统自动重复选项:我希望每次按键时只接收一个按键事件,每个按键释放只需一个按键释放事件;
  4. 如果可能的话,我会使用第1至第3项来知道用户是否一次持有多个键(即,她点击’a’并且没有释放它,她点击“Enter”)。

我在Java中面临的问题是,在Linux下,当用户持有一些密钥时,有许多keyPress和keyRelease事件被触发(因为键盘重复function)。

我尝试了一些没有成功的方法

  1. 获取关键事件的最后一次发生 – 在Linux中,它们似乎为零重复,但是,在Mac OS中它们不是;
  2. 仅当当前keyCode与最后一个keyCode不同时才考虑事件 – 这样用户就不能连续两次击中相同的键;

这是代码的基本(非工作)部分:

import java.awt.event.KeyListener; public class Example implements KeyListener { public void keyTyped(KeyEvent e) { } public void keyPressed(KeyEvent e) { System.out.println("KeyPressed: "+e.getKeyCode()+", ts="+e.getWhen()); } public void keyReleased(KeyEvent e) { System.out.println("KeyReleased: "+e.getKeyCode()+", ts="+e.getWhen()); } } 

当用户持有密钥(即“p”)时,系统显示:

 KeyPressed: 80, ts=1253637271673 KeyReleased: 80, ts=1253637271923 KeyPressed: 80, ts=1253637271923 KeyReleased: 80, ts=1253637271956 KeyPressed: 80, ts=1253637271956 KeyReleased: 80, ts=1253637271990 KeyPressed: 80, ts=1253637271990 KeyReleased: 80, ts=1253637272023 KeyPressed: 80, ts=1253637272023 ... 

至少在Linux下,JVM会在保持密钥时重新发送所有关键事件。 为了使事情变得更加困难,在我的系统(Kubuntu 9.04 Core 2 Duo)上,时间戳不断变化。 JVM使用相同的时间戳发送密钥新版本和新密钥。 这使得很难知道密钥何时真正发布。

有任何想法吗?

谢谢

这可能会有问题。 我不记得肯定(已经很长时间了),但重复键function(由底层操作系统而不是Java处理)可能无法为JVM开发人员提供足够的信息来区分这些附加function来自“真实”的关键事件。 (顺便说一下,我在OS / 2 AWT中以1.1.x的方式工作过)。

从KeyEvent的javadoc:

“按下按键”和“按键发布”事件是较低级别的,取决于平台和键盘布局。 每当按下或释放键时都会生成它们,并且这是查找不生成字符输入的键的唯一方法(例如,操作键,修改键等)。 按下或释放的键由getKeyCode方法指示,该方法返回虚拟键代码。

我记得在OS / 2中做到这一点(当时仍然只有2个事件的上/下键盘处理风格,比如旧版本的Windows,而不是3个事件的上/下/ char味道你得到更多现代版本),如果按键被按下并且事件自动生成,我没有任何不同地报告KeyReleased事件; 但我怀疑OS / 2甚至没有向我报告这些信息(不能记得肯定)。 我们使用Sun的Windows参考JVM作为我们开发AWT的指南 – 所以我怀疑是否有可能在那里报告这些信息,我至少在他们的结尾看到了它。

这个问题在这里重复。

在这个问题中,给出了一个指向Sun bug游行的链接,其中提出了一些解决方法。

我已经将hack实现为AWTEventListener,可以在应用程序的开头安装。

基本上,观察RELEASED和后续PRESSED之间的时间很短 – 实际上,它是0毫秒。 因此,您可以使用它作为度量:保持RELEASED一段时间,如果新的PRESSED紧随其后,则吞下RELEASED并处理PRESSED(因此您将获得与Windows相同的逻辑,这显然是正确的方法)。 但是,请注意从一毫秒到下一秒的换行(我已经看到过这种情况) – 因此请至少使用1 ms进行检查。 考虑到滞后和诸如此类的东西,大约20-30毫秒可能不会受到伤害。

我已经改进了stolsvik hack以防止重复KEY_PRESSED和KEY_TYPED事件,这个改进它在Win7下正常工作(应该在任何地方工作,因为它真正注意KEY_PRESSED / KEY_TYPED / KEY_RELEASED事件)。

干杯! 的Jakub

 package com.example; import java.awt.AWTEvent; import java.awt.Component; import java.awt.EventQueue; import java.awt.Toolkit; import java.awt.event.AWTEventListener; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.swing.Timer; /** * This {@link AWTEventListener} tries to work around for KEY_PRESSED / KEY_TYPED/ KEY_RELEASED repeaters. * * If you wish to obtain only one pressed / typed / released, no repeatings (ie, when the button is hold for a long time). * Use new RepeatingKeyEventsFixer().install() as a first line in main() method. * * Based on xxx * Which was done by Endre Stølsvik and inspired by xxx (hyperlinks stipped out due to stackoverflow policies) * * Refined by Jakub Gemrot not only to fix KEY_RELEASED events but also KEY_PRESSED and KEY_TYPED repeatings. Tested under Win7. * * If you wish to test the class, just uncomment all System.out.println(...)s. * * @author Endre Stølsvik * @author Jakub Gemrot */ public class RepeatingKeyEventsFixer implements AWTEventListener { public static final int RELEASED_LAG_MILLIS = 5; private static boolean assertEDT() { if (!EventQueue.isDispatchThread()) { throw new AssertionError("Not EDT, but [" + Thread.currentThread() + "]."); } return true; } private Map _releasedMap = new HashMap(); private Set _pressed = new HashSet(); private Set _typed = new HashSet(); public void install() { Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK); } public void remove() { Toolkit.getDefaultToolkit().removeAWTEventListener(this); } @Override public void eventDispatched(AWTEvent event) { assert event instanceof KeyEvent : "Shall only listen to KeyEvents, so no other events shall come here"; assert assertEDT(); // REMEMBER THAT THIS IS SINGLE THREADED, so no need // for synch. // ?: Is this one of our synthetic RELEASED events? if (event instanceof Reposted) { //System.out.println("REPOSTED: " + ((KeyEvent)event).getKeyChar()); // -> Yes, so we shalln't process it again. return; } final KeyEvent keyEvent = (KeyEvent) event; // ?: Is this already consumed? // (Note how events are passed on to all AWTEventListeners even though a // previous one consumed it) if (keyEvent.isConsumed()) { return; } // ?: KEY_TYPED event? (We're only interested in KEY_PRESSED and // KEY_RELEASED). if (event.getID() == KeyEvent.KEY_TYPED) { if (_typed.contains(keyEvent.getKeyChar())) { // we're being retyped -> prevent! //System.out.println("TYPED: " + keyEvent.getKeyChar() + " (CONSUMED)"); keyEvent.consume(); } else { // -> Yes, TYPED, for a first time //System.out.println("TYPED: " + keyEvent.getKeyChar()); _typed.add(keyEvent.getKeyChar()); } return; } // ?: Is this RELEASED? (the problem we're trying to fix!) if (keyEvent.getID() == KeyEvent.KEY_RELEASED) { // -> Yes, so stick in wait /* * Really just wait until "immediately", as the point is that the * subsequent PRESSED shall already have been posted on the event * queue, and shall thus be the direct next event no matter which * events are posted afterwards. The code with the ReleasedAction * handles if the Timer thread actually fires the action due to * lags, by cancelling the action itself upon the PRESSED. */ final Timer timer = new Timer(RELEASED_LAG_MILLIS, null); ReleasedAction action = new ReleasedAction(keyEvent, timer); timer.addActionListener(action); timer.start(); ReleasedAction oldAction = (ReleasedAction)_releasedMap.put(Integer.valueOf(keyEvent.getKeyCode()), action); if (oldAction != null) oldAction.cancel(); // Consume the original keyEvent.consume(); //System.out.println("RELEASED: " + keyEvent.getKeyChar() + " (CONSUMED)"); return; } if (keyEvent.getID() == KeyEvent.KEY_PRESSED) { if (_pressed.contains(keyEvent.getKeyCode())) { // we're still being pressed //System.out.println("PRESSED: " + keyEvent.getKeyChar() + " (CONSUMED)"); keyEvent.consume(); } else { // Remember that this is single threaded (EDT), so we can't have // races. ReleasedAction action = (ReleasedAction) _releasedMap.get(keyEvent.getKeyCode()); // ?: Do we have a corresponding RELEASED waiting? if (action != null) { // -> Yes, so dump it action.cancel(); } _pressed.add(keyEvent.getKeyCode()); //System.out.println("PRESSED: " + keyEvent.getKeyChar()); } return; } throw new AssertionError("All IDs should be covered."); } /** * The ActionListener that posts the RELEASED {@link RepostedKeyEvent} if * the {@link Timer} times out (and hence the repeat-action was over). */ protected class ReleasedAction implements ActionListener { private final KeyEvent _originalKeyEvent; private Timer _timer; ReleasedAction(KeyEvent originalReleased, Timer timer) { _timer = timer; _originalKeyEvent = originalReleased; } void cancel() { assert assertEDT(); _timer.stop(); _timer = null; _releasedMap.remove(Integer.valueOf(_originalKeyEvent.getKeyCode())); } @Override public void actionPerformed(@SuppressWarnings("unused") ActionEvent e) { assert assertEDT(); // ?: Are we already cancelled? // (Judging by Timer and TimerQueue code, we can theoretically be // raced to be posted onto EDT by TimerQueue, // due to some lag, unfair scheduling) if (_timer == null) { // -> Yes, so don't post the new RELEASED event. return; } //System.out.println("REPOST RELEASE: " + _originalKeyEvent.getKeyChar()); // Stop Timer and clean. cancel(); // Creating new KeyEvent (we've consumed the original). KeyEvent newEvent = new RepostedKeyEvent( (Component) _originalKeyEvent.getSource(), _originalKeyEvent.getID(), _originalKeyEvent.getWhen(), _originalKeyEvent.getModifiers(), _originalKeyEvent .getKeyCode(), _originalKeyEvent.getKeyChar(), _originalKeyEvent.getKeyLocation()); // Posting to EventQueue. _pressed.remove(_originalKeyEvent.getKeyCode()); _typed.remove(_originalKeyEvent.getKeyChar()); Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(newEvent); } } /** * Marker interface that denotes that the {@link KeyEvent} in question is * reposted from some {@link AWTEventListener}, including this. It denotes * that the event shall not be "hack processed" by this class again. (The * problem is that it is not possible to state * "inject this event from this point in the pipeline" - one have to inject * it to the event queue directly, thus it will come through this * {@link AWTEventListener} too. */ public interface Reposted { // marker } /** * Dead simple extension of {@link KeyEvent} that implements * {@link Reposted}. */ public static class RepostedKeyEvent extends KeyEvent implements Reposted { public RepostedKeyEvent(@SuppressWarnings("hiding") Component source, @SuppressWarnings("hiding") int id, long when, int modifiers, int keyCode, char keyChar, int keyLocation) { super(source, id, when, modifiers, keyCode, keyChar, keyLocation); } } } 

我已经找到了解决这个问题的方法,而不依赖于计时(根据一些用户的说法,100%的时间不一定是这样),而是通过发出额外的按键来覆盖密钥重复。

要明白我的意思,请尝试按住一个键然后再击中另一个中游。 重复将停止。 看来,至少在我的系统上,机器人发出的关键命​​中也有这种效果。

有关在Windows 7和Ubuntu中测试的示例实现,请参阅:

http://elionline.co.uk/blog/2012/07/12/ignore-key-repeats-in-java-swing-independently-of-platform/

此外,感谢Endre Stolsvik的解决方案,向我展示如何做一个全球事件监听器! 赞赏。

keyReleased保存事件的时间戳( arg0.when() )。 如果下一个keyPressed事件是针对同一个键且具有相同的时间戳,则它是自动重复。

如果您按住多个键,X11只会自动重复按下最后一个键。 所以,如果你按下“a”和“d”,你会看到类似的东西:

 a down a up a down d down d up d down d up a up 

我找到了一个没有等待的解决方案,以防你有类似游戏循环的事情。 想法是存储发布事件。 然后你可以在游戏循环内部和按下的键处理程序内检查它们。 通过“(un)注册密钥”我的意思是应该由应用程序处理的提取的真正的按下/释放事件。 执行以下操作时请注意同步!

  • 发布事件:按键存储事件; 否则什么都不做!
  • 关于新闻事件:如果没有存储的发布事件,这是一个新的按 – >注册它; 如果在5毫秒内存储了一个事件,这是一个自动重复 – >删除它的释放事件; 否则我们有一个尚未被游戏循环清除的存储释放事件,但是 – >(快速用户)可以随意执行,例如取消注册 – 注册
  • 在你的循环中:检查存储的释放事件,并将那些超过5毫秒的事件视为真正的版本; 取消注册; 处理所有注册的密钥

好吧,你说在密钥重复的情况下,关键事件之间的时间可能是非负的。 即便如此,它可能很短。 然后,您可以将此时间阈值设置为某个非常小的值,并且等于或低于它的所有内容都被视为关键重复。

您可能想要使用您感兴趣的组件的操作映射。这是一个处理特定键(空格键)的示例,但我确信如果您阅读文档,您可以修改它以处理通用按键和释放。

 import java.awt.Dimension; import java.awt.event.ActionEvent; import java.beans.PropertyChangeListener; import javax.swing.Action; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.KeyStroke; public class Main { public static void main(String[] args) { JFrame f = new JFrame("Test"); JPanel c = new JPanel(); c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( KeyStroke.getKeyStroke("SPACE"), "pressed"); c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( KeyStroke.getKeyStroke("released SPACE"), "released"); c.getActionMap().put("pressed", new Action() { public void addPropertyChangeListener( PropertyChangeListener listener) { } public Object getValue(String key) { return null; } public boolean isEnabled() { return true; } public void putValue(String key, Object value) { } public void removePropertyChangeListener( PropertyChangeListener listener) { } public void setEnabled(boolean b) { } public void actionPerformed(ActionEvent e) { System.out.println("Pressed space at "+System.nanoTime()); } }); c.getActionMap().put("released", new Action() { public void addPropertyChangeListener( PropertyChangeListener listener) { } public Object getValue(String key) { return null; } public boolean isEnabled() { return true; } public void putValue(String key, Object value) { } public void removePropertyChangeListener( PropertyChangeListener listener) { } public void setEnabled(boolean b) { } public void actionPerformed(ActionEvent e) { System.out.println("Released space at "+System.nanoTime()); } }); c.setPreferredSize(new Dimension(200,200)); f.getContentPane().add(c); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.pack(); f.setVisible(true); } } 

此方法将键按压存储在HashMap中,并在释放键时重置它们。 在这篇文章中,大多数代码都是Elist的礼貌。

 import java.awt.KeyEventDispatcher; import java.awt.KeyboardFocusManager; import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.HashMap; import java.util.Set; public class KeyboardInput2 { private static HashMap pressed = new HashMap(); public static boolean isPressed(int key) { synchronized (KeyboardInput2.class) { return pressed.get(key); } } public static void allPressed() { final Set templist = pressed.keySet(); if (templist.size() > 0) { System.out.println("Key(s) logged: "); } for (int key : templist) { System.out.println(KeyEvent.getKeyText(key)); } } public static void main(String[] args) { KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new KeyEventDispatcher() { @Override public boolean dispatchKeyEvent(KeyEvent ke) { synchronized (KeyboardInput2.class) { switch (ke.getID()) { case KeyEvent.KEY_PRESSED: pressed.put(ke.getKeyCode(), true); break; case KeyEvent.KEY_RELEASED: pressed.remove(ke.getKeyCode()); break; } return false; } } }); } } 

您可以使用HashMap检查是否按下了某个键,或者调用KeyboardInput2.allPressed()来打印每个按下的键。

我没有得到所有精心设计但有问题的建议? 解决方案非常简单! (忽略了OP问题的关键部分:“在Linux下,当用户持有一些密钥时,有许多keyPress和keyRelease事件被触发”)

在keyPress事件中,检查keyCode是否已在Set 中。 如果是,则必须是自动重复事件。 如果不是,请将其放入并消化。 在你的keyRelease事件中,盲目地从Set中删除keyCode – 假设OP关于许多keyRelease事件的陈述是假的。 在Windows上,我只获得了几个keyPresses,但只有一个keyRelease。

要稍微抽象一下,你可以创建一个可以携带KeyEvents,MouseEvents和MouseWheelEvents的包装器,并且有一个标志已经说明keyPress只是一个自动重复。