17

(为清楚起见进行了编辑)

我想检测用户何时按下并释放 Java Swing 中的键,忽略键盘自动重复功能。我还想要一个纯 Java 方法在 Linux、Mac OS 和 Windows 上工作。

要求:

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

我在 Java 中面临的问题是,在 Linux 下,当用户按住某个键时,会触发许多 keyPress 和 keyRelease 事件(因为键盘重复功能)。

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

  1. 获取最后一次按键事件发生的时间——在 Linux 中,按键重复的次数似乎为零,但在 Mac OS 中则不然;
  2. 仅当当前 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 发送具有相同时间戳的键新版本和新按键。这使得很难知道何时真正释放了密钥。

有任何想法吗?

谢谢

4

10 回答 10

5

这可能是有问题的。我记不太清了(已经很久了),但可能是重复键特性(由底层操作系统处理,而不是 Java)没有为 JVM 开发人员提供足够的信息来区分那些额外的来自“真实”的关键事件。(顺便说一下,我在 1.1.x 的 OS/2 AWT 中进行了这项工作)。

来自 KeyEvent 的 javadoc:

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

我记得在 OS/2 中执行此操作(当时它仍然只有 2-event up/down 风格的键盘处理,就像旧版本的 Windows,而不是 3-event up/down/char 风格你得到更多现代版本),如果只是按住键并且事件自动生成,我没有报告 KeyReleased 事件有任何不同;但我怀疑 OS/2 甚至没有向我报告该信息(无法确定)。我们使用 Sun 的 Windows 参考 JVM 作为开发 AWT 的指南 - 所以我怀疑是否可以在那里报告这些信息,我至少会在他们的最后看到它。

于 2009-09-22T14:17:31.960 回答
4

这个问题在这里重复。

在那个问题中,给出了Sun bug parade的链接,其中建议了一些解决方法。

我已经实现了一个可以在应用程序启动时安装的 AWTEventListener的 hack 。

基本上,观察 RELEASED 和随后 PRESSED 之间的时间很短——实际上,它是 0 毫秒。因此,您可以将其用作衡量标准:按住 RELEASED 一段时间,如果紧接着出现新的 PRESSED,则吞下 RELEASED 并处理 PRESSED(因此您将获得与 Windows 相同的逻辑,这显然是正确的方法)。但是,请注意从一毫秒到下一毫秒的换行(我已经看到这种情况发生) - 所以至少使用 1 毫秒来检查。考虑到滞后和诸如此类的问题,大约 20-30 毫秒可能不会受到伤害。

于 2010-05-04T09:57:08.920 回答
3

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

干杯! 雅库布

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);
  }
 }

}
于 2011-01-13T10:48:27.450 回答
2

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

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

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

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

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

于 2012-07-17T22:14:01.877 回答
1

I've found a solution that does without waiting in case you have something like a game loop going. The idea is storing the release events. Then you can check against them both inside the game loop and inside the key pressed handler. By "(un)register a key" I mean the extracted true press/release events that should be processed by the application. Take care of synchronization when doing the following!

  • on release events: store the event per key; otherwise do nothing!
  • on press events: if there is no stored release event, this is a new press -> register it; if there is a stored event within 5 ms, this is an auto-repeat -> remove its release event; otherwise we have a stored release event that hasn't been cleared by the game loop, yet -> (fast user) do as you like, e.g. unregister-register
  • in your loop: check the stored release events and treat those that are older than 5 ms as true releases; unregister them; handle all registered keys
于 2011-02-05T22:48:54.167 回答
1

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

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

a down
a up
a down
d down
d up
d down
d up
a up
于 2009-09-22T14:34:45.233 回答
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);
    }
}
于 2009-09-22T17:08:44.353 回答
0

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

于 2009-09-21T21:54:46.087 回答
0

这种方法将按键存储在 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<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()以打印每个按下的键。

于 2013-10-20T17:16:57.647 回答
0

我对所有详细但有问题的建议没有得到什么?解决办法就是这么简单!(忽略了OP问题的关键部分:“在Linux下,当用户持有某个键时,会触发许多keyPress和keyRelease事件”)

在您的 keyPress 事件中,检查 keyCode 是否已经在 Set<Integer> 中。如果是,它必须是一个自动重复事件。如果不是,把它放进去消化它。在您的 keyRelease 事件中,盲目地从 Set 中删除 keyCode - 假设 OP 关于许多 keyRelease 事件的声明是错误的。在 Windows 上,我只得到几个 keyPresses,但只有一个 keyRelease。

为了稍微抽象一下,您可以创建一个包装器,该包装器可以携带 KeyEvents、MouseEvents 和 MouseWheelEvents,并具有一个已经表明 keyPress 只是一个自动重复的标志。

于 2014-11-01T12:59:24.957 回答