Как узнать, когда пользователь действительно выпустил ключ на Java?
(Отредактировано для ясности)
Я хочу определить, когда пользователь нажимает и отпускает ключ в Java Swing, игнорируя функцию автоматического повтора клавиатуры. Я также хотел бы, чтобы чистый подход Java работал на Linux, Mac OS и Windows.
Требования:
- Когда пользователь нажимает какую-то клавишу, я хочу знать, что это за ключ;
- Когда пользователь выпускает какой-либо ключ, я хочу знать, что это за ключ,
- Я хочу игнорировать параметры автоматического повтора системы: я хочу получить только одно событие нажатия клавиши для каждого нажатия клавиши и всего лишь одно событие релиза ключа для каждого выпуска ключа;
- Если возможно, я бы использовал пункты с 1 по 3, чтобы узнать, держит ли у пользователя более одного ключа за раз (т.е. она попадает в "а", и, не выпуская его, она ударяет "Enter").
Проблема, с которой я сталкиваюсь в Java, заключается в том, что в Linux, когда пользователь имеет некоторый ключ, происходит много событий keyPress и keyRelease (из-за функции повторения клавиатуры).
Я пробовал некоторые подходы с без успеха:
- Получите последний раз, когда произошло ключевое событие - в Linux они, похоже, равны нулю для повторения ключей, однако в Mac OS они не являются;
- Рассмотрите событие, только если текущий ключевой код отличается от последнего - таким образом, пользователь не может дважды нажать один и тот же ключ в строке;
Вот основная (не рабочая) часть кода:
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 отправляет новый ключ и новый ключ с той же меткой времени. Это затрудняет понимание того, когда ключ действительно освобожден.
Любые идеи?
Спасибо
Ответы
Ответ 1
Это может быть проблематично. Я точно не помню (это было долгое время), но, скорее всего, функция повторяющегося ключа (которая обрабатывается базовой операционной системой, а не Java) не предоставляет достаточной информации для разработчика JVM, чтобы отличить эти дополнительные ключевые события от "реального". (Я работал над этим в OS/2 AWT в 1.1.x, кстати).
Из javadoc для KeyEvent:
События с нажатыми клавишами и клавишами являются более низкими и зависят от платформы и раскладки клавиатуры. Они генерируются всякий раз, когда клавиша нажата или отпущена, и это единственный способ узнать о клавишах, которые не генерируют ввод символов (например, клавиши действий, ключи-модификаторы и т.д.). Клавиша, нажатая или отпускаемая, указывается методом getKeyCode, который возвращает виртуальный key code.
Как я помню из этого в OS/2 (который в то время по-прежнему имел только 2-событие вверх/вниз вкус клавиатуры, как более старые версии Windows, а не 3-событие вверх/вниз/ char вкус, который вы получаете в более современных версиях), я не сообщал о событиях KeyReleased по-разному, если бы ключ был просто сдержан, а события были автоматически сгенерированы; но я подозреваю, OS/2 даже не сообщил мне эту информацию (не помню точно). Мы использовали справочную JVM для Windows от Sun в качестве нашего руководства по разработке нашего AWT - поэтому я подозреваю, что если бы можно было сообщить эту информацию, я бы, по крайней мере, видел это на их конце.
Ответ 2
Этот вопрос дублируется здесь.
В этом вопросе дается ссылка на Параметр ошибки Sun, где предлагается некоторое обходное решение.
Я сделал взлом, реализованный как AWTEventListener, который можно установить в начале приложения.
В принципе, обратите внимание, что время между RELEASED и последующим PRESSED невелико - на самом деле это 0 миллис. Таким образом, вы можете использовать это как меру: удерживайте RELEASED в течение некоторого времени, и если новый PRESSED появится сразу после этого, проглотите RELEASED и просто обработайте PRESSED (И таким образом вы получите ту же логику, что и в Windows, что, очевидно, является правильным способом). Тем не менее, следите за переносом от одной миллисекунды к следующей (я видел, как это произошло) - так что нужно проверить не менее 1 мс. Для учета лагов и гадостей примерно 20-30 миллисекунд, вероятно, не повредит.
Ответ 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);
}
}
}
Ответ 4
Сохранить метку времени события (arg0.when()
) в keyReleased
. Если следующее событие keyPressed
для одного и того же ключа и имеет одну и ту же метку времени, это автоповтор.
Если вы удерживаете несколько клавиш, X11 только автоповторит последнюю нажатую клавишу. Итак, если вы удерживаете "a" и "d", вы увидите что-то вроде:
a down
a up
a down
d down
d up
d down
d up
a up
Ответ 5
Я нашел решение, которое работает без ожидания, если у вас есть что-то вроде игрового цикла. Идея заключается в сохранении событий выпуска. Затем вы можете проверить их как внутри игрового цикла, так и внутри нажатого клавиши. "(Un) зарегистрировать ключ" Я имею в виду извлеченные истинные события для печати/выпуска, которые должны обрабатываться приложением. Позаботьтесь о синхронизации при выполнении следующих действий:
- в событиях выпуска: сохранение события за ключ; в противном случае ничего не делать!
- на событиях в прессе: если нет сохраненного события релиза, это новое нажатие → зарегистрировать его; если есть сохраненное событие в течение 5 мс, это автоматический повтор → удалить его событие релиза; в противном случае у нас есть сохраненное событие релиза, которое не было очищено игровым циклом, но → (быстрый пользователь), как вам нравится, например. UNREGISTER-регистр
- в вашем цикле: проверьте сохраненные события выпуска и рассмотрите те, которые старше 5 мс, как истинные релизы; аннулировать их; обрабатывать все зарегистрированные ключи
Ответ 6
Я нашел решение этой проблемы, не полагаясь на синхронизацию (которая, по мнению некоторых пользователей, не обязательно соответствует 100% времени), но вместо этого, выдавая дополнительные нажатия клавиш, чтобы переопределить повторение ключа.
Чтобы понять, что я имею в виду, попробуйте держать ключ, а затем ударить по другому среднему потоку. Повтор останавливается. Похоже, что в моей системе по крайней мере ключевые хиты, выпущенные Robot, также имеют этот эффект.
Для примера реализации, протестированной в Windows 7 и Ubuntu, см.:
http://elionline.co.uk/blog/2012/07/12/ignore-key-repeats-in-java-swing-independently-of-platform/
Кроме того, благодаря решению Endre Stolsvik для показа мне, как сделать глобальный слушатель событий! Оценил.
Ответ 7
Хорошо, вы сказали, что возможно, что время между ключевыми событиями в случае повторения ключа будет неотрицательным. Тем не менее, это, вероятно, очень мало. Вы могли бы затем на этот раз поронуть какое-то очень маленькое значение, и все равное или меньшее, чем это считается ключевым повторением.
Ответ 8
Возможно, вы захотите использовать карту действий интересующего вас компонента. Вот пример, который касается определенного ключа (SPACE BAR), но я уверен, что если вы прочтете документацию, вы сможете ее изменить для обработки типичных нажатий клавиш и выпусков.
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);
}
}
Ответ 9
В этом подходе хранятся нажатия клавиш в 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()
для печати каждой нажатой клавиши.
Ответ 10
Что я не получу обо всех сложных, но сомнительных предложениях? Решение так просто! (Оглядываясь на ключевую часть вопроса OP: "в Linux, когда пользователь держит какой-то ключ, происходит много событий keyPress и keyRelease" )
В событии keyPress проверьте, есть ли keyCode уже в Set <Integer> . Если это так, это должно быть событие autorepeat. Если это не так, поместите его и переварите. В событии keyRelease слепо удалите keyCode из Set - если предположить, что оператор OP о многих событиях keyRelease является ложным. В Windows я получаю только несколько keyPresses, но только один keyRelease.
Чтобы немного отвлечься, вы можете создать оболочку, которая может нести KeyEvents, MouseEvents и MouseWheelEvents и имеет флаг, который уже говорит, что keyPress - это просто автопортрет.