java swing keypress autorepeat keyrelease

¿Cómo saber cuándo un usuario realmente ha lanzado una clave en Java?



swing keypress (10)

Bueno, usted dijo que es posible que el tiempo entre los eventos clave en caso de repetición de la tecla no sea negativo. Aun así, es probable que sea muy corto. Entonces, podría limitar este tiempo a un valor muy pequeño, y todo igual o inferior a lo que se considerará una repetición clave.

(Editado para mayor claridad)

Quiero detectar cuándo un usuario presiona y suelta una tecla en Java Swing, ignorando la función de repetición automática del teclado. También me gustaría un enfoque de Java puro, el trabajo en Linux, Mac OS y Windows.

Requisitos:

  1. Cuando el usuario presiona alguna tecla quiero saber qué tecla es esa;
  2. Cuando el usuario libera alguna clave, quiero saber qué clave es esa;
  3. Quiero ignorar las opciones de repetición automática del sistema: quiero recibir solo un evento de pulsación de tecla por cada pulsación de tecla y solo un evento de liberación de tecla por cada pulsación de tecla;
  4. Si es posible, usaría los elementos 1 a 3 para saber si el usuario tiene más de una tecla a la vez (es decir, presiona ''a'' y, sin soltarla, presiona "Enter").

El problema al que me estoy enfrentando en Java es que en Linux, cuando el usuario tiene alguna tecla, hay muchos eventos de KeyPress y keyRelease que se activan (debido a la función de repetición del teclado).

He intentado algunos enfoques sin éxito :

  1. Obtenga la última vez que ocurrió un evento clave: en Linux, parecen ser cero para la repetición de la clave, sin embargo, en Mac OS no lo son;
  2. Considere un evento solo si el código de clave actual es diferente del último, de esta manera el usuario no puede presionar dos veces la misma tecla en una fila;

Aquí está la parte básica (no funcional) del código:

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

Cuando un usuario tiene una tecla (es decir, ''p''), el sistema muestra:

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 ...

Al menos en Linux, la JVM continúa reenviando todos los eventos clave cuando se retiene una tecla. Para hacer las cosas más difíciles, en mi sistema (Kubuntu 9.04 Core 2 Duo) las marcas de tiempo cambian constantemente. La JVM envía una nueva versión clave y una nueva pulsación de tecla con la misma marca de tiempo. Esto hace que sea difícil saber cuándo se libera realmente una tecla.

¿Algunas ideas?

Gracias


Es posible que desee utilizar el mapa de acción del componente en el que está interesado. Este es un ejemplo que trata con una clave específica (BARRA ESPACIADORA) pero estoy seguro de que si lee la documentación puede modificarla para manejar genéricos Pulsaciones de teclas y lanzamientos.

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


Esta pregunta se duplica here .

En esa pregunta, se proporciona un enlace al desfile de errores de Sun , donde se sugiere alguna solución.

He implementado un hack como un AWTEventListener que se puede instalar al inicio de la aplicación.

Básicamente, observe que el tiempo entre el LIBERADO y el PRESSED subsiguiente es pequeño; en realidad, es 0 milis. Por lo tanto, puede usar eso como una medida: Mantenga presionado el RELEASED por algún tiempo, y si aparece un nuevo PRESSED inmediatamente, trague el RELEASED y simplemente maneje el PRESSED (y así obtendrá la misma lógica que en Windows, que obviamente es la forma correcta). Sin embargo, observe el ajuste de un milisegundo al siguiente (he visto que esto ocurre), por lo que use al menos 1 ms para verificar. Para tener en cuenta los retrasos y lo que no, unos 20-30 milisegundos probablemente no duelen.


Este enfoque almacena pulsaciones de teclas en un HashMap, restableciéndolas cuando se suelta la tecla. La mayor parte del código es cortesía de Elist en this post.

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

Puede usar el HashMap para verificar si se presiona una tecla determinada, o llamar a KeyboardInput2.allPressed() para imprimir cada tecla presionada.


Esto podría ser problemático. No puedo recordarlo con seguridad (ha pasado mucho tiempo), pero es probable que la función de tecla de repetición (que es manejada por el sistema operativo subyacente, no por Java) no proporcione suficiente información para que el desarrollador de JVM distinga esos Eventos clave del ''real''. (A propósito, trabajé en esto en el OS / 2 AWT en 1.1.x).

Desde el javadoc para KeyEvent:

Los eventos "Tecla presionada" y "tecla liberada" son de nivel inferior y dependen de la plataforma y el diseño del teclado. Se generan cada vez que se presiona o suelta una tecla, y son la única forma de conocer las teclas que no generan entrada de caracteres (p. Ej., Teclas de acción, teclas modificadoras, etc.). La tecla que se presiona o suelta se indica mediante el método getKeyCode, que devuelve un código de tecla virtual.

Como recuerdo de haber hecho esto en OS / 2 (que en ese momento todavía tenía solo la combinación de 2 eventos arriba / abajo del manejo del teclado, como las versiones anteriores de Windows, no la versión de 3 eventos arriba / abajo / char que obtienes en más versiones modernas), no informé los eventos KeyReleased de manera diferente si la tecla solo se mantenía presionada y los eventos se generaban automáticamente; pero sospecho que OS / 2 ni siquiera me informó esa información (no puedo recordarlo con seguridad). Utilizamos la JVM de referencia de Windows de Sun como nuestra guía para el desarrollo de nuestro AWT, por lo que sospecho que si fuera posible reportar esta información allí, al menos la habría visto.


Guarde la marca de tiempo del evento ( arg0.when() ) en keyReleased . Si el siguiente evento keyPressed es para la misma clave y tiene la misma marca de tiempo, es un autorepeat.

Si mantiene presionadas varias teclas, X11 solo repara automáticamente la última tecla presionada. Entonces, si mantienes presionadas las teclas ''a'' y ''d'' verás algo como:

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


He encontrado una solución a este problema sin confiar en el tiempo (que, según algunos usuarios, no es necesariamente el 100% del tiempo), sino al presionar teclas adicionales para anular la repetición de la tecla.

Para ver lo que quiero decir, intente mantener pulsada una tecla y luego pulsar otra mitad de la secuencia. La repetición se detendrá. Parece que, al menos en mi sistema, los impactos clave emitidos por Robot también tienen este efecto.

Para una implementación de ejemplo, probada en Windows 7 y Ubuntu, consulte:

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

Además, gracias a la solución de Endre Stolsvik por mostrarme cómo hacer un oyente de eventos global. Apreciado.


He encontrado una solución que funciona sin esperar en caso de que tenga algo como un bucle de juego en marcha. La idea es almacenar los eventos de lanzamiento. Luego, puede verificar contra ellos tanto dentro del bucle de juego como dentro del controlador de teclas presionadas. Por "(un) registrar una clave" me refiero a los verdaderos eventos de presionar / liberar que deben ser procesados ​​por la aplicación. ¡Tenga cuidado de la sincronización al hacer lo siguiente!

  • en eventos de lanzamiento: almacenar el evento por clave; de lo contrario no hagas nada!
  • en eventos de prensa: si no hay un evento de liberación almacenado, esta es una nueva prensa -> registrarlo; si hay un evento almacenado dentro de 5 ms, esto es una repetición automática -> eliminar su evento de lanzamiento; de lo contrario, tenemos un evento de lanzamiento almacenado que no ha sido borrado por el bucle del juego, pero - (usuario rápido) haz lo que quieras, por ejemplo, anular el registro-registro
  • en su bucle: verifique los eventos de lanzamiento almacenados y trate los que tienen más de 5 ms como lanzamientos verdaderos; desregistrarlos manejar todas las claves registradas

También he refinado el truco stolsvik para evitar la repetición de los eventos KEY_PRESSED y KEY_TYPED, con este refinamiento funciona correctamente en Win7 (debería funcionar en cualquier lugar, ya que realmente observa eventos KEY_PRESSED / KEY_TYPED / KEY_RELEASED).

¡Aclamaciones! 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 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); } } }


¿Qué es lo que no entiendo de todas las sugerencias elaboradas pero cuestionables? ¡La solución es tan simple! (Pasó por alto la parte clave de la pregunta de OP: "en Linux, cuando el usuario tiene alguna tecla, hay muchos eventos de KeyPress y keyRelease que se están disparando")

En su evento keyPress, verifique si el KeyCode ya está en un Set <Integer>. Si es así, debe ser un evento autorepeat. Si no lo está, póngalo y digiéralo. En su evento keyRelease, elimine a ciegas el keyCode del Conjunto, suponiendo que la declaración de OP sobre muchos eventos keyRelease es falsa. En Windows, solo obtengo varios KeyPress, pero solo un KeyRelease.

Para abstraer esto un poco, puedes crear un envoltorio que pueda llevar KeyEvents, MouseEvents y MouseWheelEvents y tenga una marca que ya dice que keyPress es solo un autorepeat.