Как узнать, когда пользователь действительно выпустил ключ в Java? - PullRequest
18 голосов
/ 22 сентября 2009

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

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

Требования:

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

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

Есть идеи?

Спасибо

Ответы [ 10 ]

4 голосов
/ 04 мая 2010

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

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

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

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

4 голосов
/ 22 сентября 2009

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

Из Javadoc для KeyEvent:

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

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

3 голосов
/ 13 января 2011

Я усовершенствовал хак для stolsvik, чтобы предотвратить повторение событий 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);
  }
 }

}
2 голосов
/ 18 июля 2012

Я нашел решение этой проблемы, не полагаясь на время (которое, по мнению некоторых пользователей, не обязательно соответствует 100% времени), а вместо этого, выполняя дополнительные нажатия клавиш для отмены повторения клавиши.

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

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

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

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

1 голос
/ 06 февраля 2011

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

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

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

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

a down
a up
a down
d down
d up
d down
d up
a up
0 голосов
/ 01 ноября 2014

Что я не получаю от всех сложных, но сомнительных предложений? Решение - , поэтому просто! (Упущен из виду ключевой вопрос OP: «В Linux, когда пользователь удерживает какую-то клавишу, происходит много событий keyPress и keyRelease, запускаемых»)

В вашем событии keyPress проверьте, находится ли keyCode уже в наборе . Если это так, это должно быть событие автоповтора. Если это не так, вставьте его и переварите. В вашем событии keyRelease вслепую удалите keyCode из Set - при условии, что оператор OP о многих событиях keyRelease имеет значение false. В Windows я получаю только несколько нажатий клавиш, но только одну клавишу выпуска.

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

0 голосов
/ 20 октября 2013

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

0 голосов
/ 22 сентября 2009

Возможно, вы захотите использовать карту действий интересующего вас компонента. Вот пример, который имеет дело с конкретным ключом (пробел), но я уверен, что если вы прочитаете документацию, вы сможете изменить ее для обработки общих нажатий и отпусканий клавиш.

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);
    }
}
0 голосов
/ 22 сентября 2009

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

...