Как узнать, когда пользователь действительно выпустил ключ на Java?

(Отредактировано для ясности)

Я хочу определить, когда пользователь нажимает и отпускает ключ в Java Swing, игнорируя функцию автоматического повтора клавиатуры. Я также хотел бы, чтобы чистый Java-подход работал на Linux, Mac OS и Windows.

Требования:

  1. Когда пользователь нажимает какую-то клавишу, я хочу знать, что это за ключ;
  2. Когда пользователь выпускает какой-то ключ, я хочу знать, что это за ключ;
  3. Я хочу игнорировать параметры автоматического повторного запуска системы: я хочу получить только одно событие нажатия клавиши для каждого нажатия клавиши и только одно событие для выпуска ключа для каждого выпуска ключа;
  4. Если возможно, я бы использовал пункты с 1 по 3, чтобы узнать, держит ли у пользователя более одного ключа за раз (т. Е. Она нажимает «а», и, не выпуская его, она нажимает «Enter»).

Проблема, с которой я сталкиваюсь в Java, заключается в том, что в Linux, когда пользователь имеет некоторый ключ, происходит много событий keyPress и keyRelease (из-за функции повторения клавиатуры).

Я пробовал некоторые подходы без успеха :

  1. Получите последний раз, когда произошло ключевое событие – в Linux они кажутся нулевыми для повторения ключей, однако в Mac OS они не являются;
  2. Рассмотрите событие только в том случае, если текущий ключевой код отличается от последнего – таким образом, пользователь не может дважды нажать один и тот же ключ в строке;

Вот основная (не рабочая) часть кода:

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 отправляет новый ключ и новый ключ с той же меткой времени. Это затрудняет понимание того, когда ключ действительно выпущен.

Есть идеи?

благодаря

Это может быть проблематично. Я точно не помню (это было долгое время), но, скорее всего, функция повторного ключа (которая обрабатывается базовой операционной системой, а не Java) не предоставляет достаточной информации для разработчика JVM, чтобы отличить эти дополнительные ключевые события от «реального». (Кстати, я работал над этим в OS / 2 AWT в 1.1.x).

Из javadoc для KeyEvent:

События с нажатыми клавишами и клавишами являются более низкими и зависят от платформы и раскладки клавиатуры. Они генерируются всякий раз, когда нажата или выпущена клавиша, и это единственный способ узнать о клавишах, которые не генерируют ввод символов (например, клавиши действий, ключи-модификаторы и т. Д.). Ключ, нажатый или высвобождаемый, обозначается методом getKeyCode, который возвращает код виртуального ключа.

Как я помню из этого, в OS / 2 (который в то время оставался только 2-игровым вкусом клавиатуры, как более старые версии Windows, а не 3-event-up / down / char, который вы получаете больше современные версии), я не сообщал о событиях KeyReleased по-разному, если бы ключ был просто сдержан, а события были автоматически сгенерированы; но я подозреваю, OS / 2 даже не сообщил мне эту информацию (не помню точно). Мы использовали справочную JVM для Windows от Sun в качестве нашего руководства по разработке нашего AWT – поэтому я подозреваю, что если бы можно было сообщить об этой информации, я бы, по крайней мере, видел это на их конце.

Этот вопрос дублируется здесь .

В этом вопросе дается ссылка на парад ошибок Sun , где предлагается некоторое обходное решение.

Я сделал взлом, реализованный как AWTEventListener, который можно установить в начале приложения.

В принципе, обратите внимание, что время между RELEASED и последующим PRESSED невелико – на самом деле это 0 миллис. Таким образом, вы можете использовать это как меру: удерживайте RELEASED в течение некоторого времени, и если новый PRESSED появится сразу после этого, проглотите RELEASED и просто обработайте PRESSED (И таким образом вы получите ту же логику, что и в Windows, что, очевидно, является правильным способом). Тем не менее, следите за переносом от одной миллисекунды к следующей (я видел, как это произошло) – так что нужно проверить не менее 1 мс. Для учета лагов и гадостей примерно 20-30 миллисекунд, вероятно, не повредит.

Я усовершенствовал 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 (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% времени), но вместо этого, выдавая дополнительные нажатия клавиш, чтобы переопределить повторение ключа.

Чтобы понять, что я имею в виду, попробуйте держать ключ, а затем ударить по другому среднему streamу. Повтор останавливается. Похоже, что в моей системе, по крайней мере, ключевые хиты, выпущенные Robot, также имеют этот эффект.

Пример реализации, протестированный в Windows 7 и Ubuntu, приведен ниже:

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

Кроме того, благодаря решению Эндра Столсвика, чтобы показать мне, как сделать глобального слушателя событий! Оценил.

Сохраните arg0.when() времени события ( arg0.when() ) в keyReleased . Если следующее событие keyPressed предназначено для одного и того же ключа и имеет такую ​​же временную метку, это автоповтор.

Если вы удерживаете несколько клавиш, X11 только автоповторит последнюю нажатую клавишу. Итак, если вы удерживаете «a» и «d», вы увидите что-то вроде:

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

Я нашел решение, которое работает без ожидания, если у вас есть что-то вроде игрового цикла. Идея заключается в сохранении событий выпуска. Затем вы можете проверить их как внутри игрового цикла, так и внутри нажатого клавиши. «(Un) зарегистрировать ключ« Я имею в виду извлеченные истинные события для печати / выпуска, которые должны обрабатываться приложением. Позаботьтесь о синхронизации при выполнении следующего!

  • по событиям выпуска: сохранять событие за ключ; в противном случае ничего не делать!
  • на событиях в прессе: если нет сохраненного события релиза, это новое нажатие -> зарегистрировать его; если есть сохраненное событие в течение 5 мс, это автоматический повтор -> удалить его событие релиза; в противном случае у нас есть событие с сохраненной версией, которое не было очищено игровым циклом, но -> (быстрый пользователь), как вам нравится, например, unregister-register
  • в вашем цикле: проверьте сохраненные события релиза и обработайте те, которые старше 5 мс, как истинные релизы; аннулировать их; обрабатывать все зарегистрированные ключи

Ну, вы сказали, что возможно, что время между ключевыми событиями в случае повторения ключа будет неотрицательным. Тем не менее, это, вероятно, очень мало. Тогда вы могли бы порог на какое-то очень небольшое значение, и все равное или меньшее, чем это считалось ключевым повторением.

Возможно, вы захотите использовать карту действий интересующего вас компонента. Вот пример, который касается определенного ключа (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); } } 

Этот подход хранит нажатия клавиш в HashMap, сбрасывая их при отпускании клавиши. Большая часть кода любезно предоставлена Элистом в этом посте.

 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 . Если это так, это должно быть событие autorepeat. Если это не так, поместите его и переварите. В событии keyRelease слепо удалите keyCode из Set – если предположить, что утверждение OP о многих событиях keyRelease ложно. В Windows я получаю только несколько keyPresses, но только один keyRelease.

Чтобы немного отвлечься, вы можете создать обертку, которая может нести KeyEvents, MouseEvents и MouseWheelEvents, и имеет флаг, который уже говорит, что keyPress – это просто автопортрет.

  • JComboBox Выбор слушателя?
  • Какова цель использования менеджеров макетов Java?
  • Размеры значков кадров, используемых в Swing
  • Как нарисовать дерево, представляющее график подключенных узлов?
  • Эффект безопасного рабочего режима для Java-приложения
  • JTable, как изменить цвет BackGround
  • Выбор файла в панели с Swing
  • Нарисуйте строку в JPanel с нажатием кнопки на Java
  • Поворот изображения в java
  • Компоновка компонентов Swing: как добавить возможность добавления ActionListeners?
  • Как ограничить JTextField количеством томов
  • Давайте будем гением компьютера.