(为清晰起见编辑)
我想检测用户何时在Java Swing中按下并释放键,忽略键盘自动重复功能。我还想在Linux,Mac OS和Windows上使用纯Java方法。
要求:
我在Java中面临的问题是,在Linux下,当用户持有一些密钥时,有许多keyPress和keyRelease事件被触发(因为键盘重复功能)。
我尝试了一些没有成功的方法:
以下是代码的基本(非工作)部分:
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使用相同的时间戳发送密钥新版本和新密钥。这使得很难知道密钥何时真正发布。
有什么想法吗?
由于
答案 0 :(得分:4)
这可能会有问题。我无法确定(已经很长时间了),但重复键功能(由底层操作系统而不是Java处理)可能无法为JVM开发人员提供足够的信息来区分这些附加功能来自“真实”的关键事件。 (顺便说一句,我在OS / 2 AWT中以1.1.x的方式处理过这个问题。)
来自KeyEvent的javadoc:
“按下按键”和“按键释放”事件是较低级别的,取决于平台和键盘布局。无论何时按下或释放按键都会生成它们,并且这是查找不生成字符输入的按键(例如,动作按键,修改键等)的唯一方法。按下或释放的键由getKeyCode方法指示,该方法返回虚拟键代码。
我记得在OS / 2中这样做(当时仍然只有像旧版Windows一样的键盘处理的2事件上/下风味,而不是3事件上/下/ char风味你进入更现代的版本),如果只是按下键并自动生成事件,我没有任何不同地报告KeyReleased事件;但我怀疑OS / 2甚至没有向我报告这些信息(不能记得肯定)。我们使用Sun的Windows参考JVM作为我们开发AWT的指南 - 所以我怀疑是否有可能在那里报告这些信息,我至少已经看到了它。
答案 1 :(得分:4)
此问题重复here。
在该问题中,给出了Sun bug parade的链接,其中提出了一些解决方法。
我已将a hack作为AWTEventListener实现,可以在应用程序的开头安装。
基本上,请注意RELEASED和后续PRESSED之间的时间很短 - 实际上,它是0毫秒。因此,您可以使用它作为度量:保持RELEASED一段时间,如果新的PRESSED紧随其后,则吞下RELEASED并处理PRESSED(因此您将获得与Windows相同的逻辑,这显然是正确的方法)。但是,请注意从一毫秒到下一秒的换行(我已经看到过这种情况) - 因此请至少使用1毫秒进行检查。为了解释滞后和诸如此类的事情,大约20-30毫秒可能不会受到伤害。
答案 2 :(得分:3)
我已经改进了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 (i.e., 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<Integer, ReleasedAction> _releasedMap = new HashMap<Integer, ReleasedAction>();
private Set<Integer> _pressed = new HashSet<Integer>();
private Set<Character> _typed = new HashSet<Character>();
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);
}
}
}
答案 3 :(得分:2)
我已经找到了解决这个问题的方法,而不依赖于计时(根据某些用户的说法,100%的时间不一定是这样),而是通过发出额外的按键来覆盖密钥重复。
要明白我的意思,请尝试按住一个键,然后点击另一个中游。重复将停止。看来,至少在我的系统上,机器人发出的关键命中也会产生这种影响。
对于示例实现,在Windows 7和Windows 7中进行了测试。 Ubuntu,见:
http://elionline.co.uk/blog/2012/07/12/ignore-key-repeats-in-java-swing-independently-of-platform/
另外,感谢Endre Stolsvik的解决方案,向我展示如何做一个全球事件监听器!赞赏。
答案 4 :(得分:1)
将事件的时间戳(arg0.when()
)保存在keyReleased
中。如果下一个keyPressed
事件是针对相同的密钥并且具有相同的时间戳,则它是自动重复的。
如果按住多个键,X11仅自动重复按下最后一个键。所以,如果你按下“a”和“d”,你会看到类似的东西:
a down
a up
a down
d down
d up
d down
d up
a up
答案 5 :(得分:1)
我找到了一个没有等待的解决方案,以防你有类似游戏循环的事情。想法是存储发布事件。然后你可以在游戏循环内部和按下键的处理程序内部检查它们。通过“(un)注册密钥”我指的是应该由应用程序处理的提取的真正的按下/释放事件。执行以下操作时请注意同步!
答案 6 :(得分:0)
嗯,你说在密钥重复的情况下,关键事件之间的时间可能是非负的。即便如此,它可能很短。然后,您可以将此时间阈值设置为某个非常小的值,并且等于或低于它的所有内容都被视为关键重复。
答案 7 :(得分:0)
您可能想要使用您感兴趣的组件的操作映射。这是一个处理特定键(空格键)的示例,但我确信如果您阅读文档,您可以修改它处理通用键按下和释放。
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);
}
}
答案 8 :(得分:0)
此方法将键按压存储在HashMap中,并在释放键时重置它们。 大多数代码都是Elist帖子中的this礼貌。
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<Integer, Boolean> pressed = new HashMap<Integer, Boolean>();
public static boolean isPressed(int key) {
synchronized (KeyboardInput2.class) {
return pressed.get(key);
}
}
public static void allPressed() {
final Set<Integer> 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()
来打印每个按下的键。
答案 9 :(得分:0)
我没有得到所有精心设计但有问题的建议?解决方案是所以简单!(忽略了OP问题的关键部分:&#34;在Linux下,当用户持有一些密钥时,有很多keyPress和keyRelease事件被解雇&#34;)
在你的keyPress事件中,检查keyCode是否已经在Set&lt; Integer&gt;中。如果是,则必须是自动重复事件。如果不是,请将其放入并消化。在你的keyRelease事件中,盲目地从Set中删除keyCode - 假设OP关于许多keyRelease事件的声明是错误的。在Windows上,我只获得了几个keyPresses,但只有一个keyRelease。
要稍微抽象一下,你可以创建一个可以携带KeyEvents,MouseEvents和MouseWheelEvents的包装器,并且有一个标志已经说明keyPress只是一个自动重复。