Bei der Entwicklung eines Farbwahlprogramms mit Tastatursteuerung stellte sich die Aufgabe, von dem an die Farbpalette angeschlossenen KeyListener ein einziges Signal beim Loslassen einer Pfeiltaste zu erhalten, weil erst beim Loslassen die unter dem Palettencursor liegende Farbe als ausgewählt betrachtet werden sollte.
Als ich aber während des Gedrückthaltens einer Pfeiltaste permanent Loslass-Meldungen der Pfeiltaste erhielt, dachte ich zuerst an einen Fehler in meinem Programm (das ist die Sache mit der eigenen Nase..).
Als ich mir aber die Folge von Tastaturereignissen einmal genauer anschaute, bemerkte ich, dass beim stetigen Drücken einer Taste eine Folge von Loslass- und Drück-Ereignispaaren gesendet wurde.
Nachdem ich dann das Progamm testweise unter Windows laufen ließ, stellte ich überrascht fest, dass sich dort das ursprünglich auch unter Linux erwartete Verhalten zeigte: Das Drücken einer Taste lieferte solange Drück-Ereignisse, bis die Taste losgelassen wurde, was abschließend ein einziges Loslass-Ereignis lieferte.
Im Entwickler-Netzwerk von Java fand ich dann unter Bug ID: 4153069 eine Beschreibung dieses unterschiedlichen Verhaltens, ohne dabei jedoch eine befriedigende Lösung vorgestellt zu bekommen - schließlich sollte sich ja Java auch in Hinsicht auf die Tastatureingaben plattformunabhängig verhalten.
Das unter Linux verwendete Programm xev zur Analyse von Eingabeereignissen zeigte dann tatsächlich, dass - zumindest unter meinem OpenSUSE-Betriebssystem - beim Drücken einer Taste eine Serie von Ereignispaaren geliefert wird, wobei dem Drück-Ereignis stets ein Loslass-Ereignis ohne Zeitdifferenz vorausgeschickt wird.
Ich nahm dies zum Anlass, einen ersten Lösungsvorschlag für einen zeitgesteuerten KeyListener zu unterbreiten, der auch noch unter der o.g. Bug-Id einsehbar ist und der dieses eigenartige Sendeverhalten auf Betriebsystemebene berücksichtigt.
Allerdings war dieser Vorschlag nur ein Entwurf, der z.B. nicht das gleichzeitige Drücken mehrerer Tasten berücksichtigte.
Aktualisierung (28.2.2012): Mittlerweile ist der oben beschriebene Bug behoben, die Nützlichkeit der TimedKeyAdapter-Klasse aber bleibt bestehen.
Auf meine Vorstellung dieser Klasse vor mehreren Jahren bekam ich dann eine Antwort per E-Mail von Paul,
in welcher er mir seine Version
eines TimedKeyAdapters vorstellte, welche u.A. den Vorteil hatte, mehrere Tastatureingaben korrekt verarbeiten zu können. Ich konnte mit dem (damals noch so genannten) TimedKeyListener zwar auch mehrere
Tastaturdrücke verarbeiten, merkte jedoch, dass, wenn die Tasten sehr schnell gedrückt wurden, die erste Taste zwar als gedrückt, dann aber nicht als losgelassen registriert
wurde - sehr schlecht für ein Spiel, für den der Spiele-Modus dieser Klasse gedacht war.
Also setzte ich mich nochmal hin, dachte nach, und verstand den Fehler, den ich gemacht hatte: Ich verwendete nur einen einzigen Timer für alle registrierten Tastendrücke; sofern eine bestimmte Geschwindigkeit beim Drücken nicht unterschritten wurde, funktionierte dies auch. Falls aber jemand so schnell die Tasten drückte, wie es jemand tut, der wutschnaubend auf der Tastatur rumhämmert, weil sein Programm nicht mehr reagiert oder nicht das macht, was es machen soll, gab es das Problem der verschluckten Taste.
Die Lösung war - so, wie es auch Paul erkannte -, für jede Taste einen eigenen Timer zu verwenden.
Dies erforderte die Implementierung einer neuen inneren Klasse, welche den Code der Taste und ihren Timer enthielt. Ich wollte jedoch so kurze Wartezeiten wie möglich, weshalb ich
die Timer mit 1ms einrichtete. Auch sollten die Tastendrücke aus der internen Liste gelöscht werden, sobald die Taste als losgelassen erkannt wurde.
Zu guter Letzt benannte ich die Klasse von TimedKeyListener in TimedKeyAdapter um, weil sie bereits Methoden (wenn auch mit leerem Rumpf) implementierte und nicht nur - wie in
einer Schnittstelle z.B. - deklarierte.
Mit dem Applet linkerhand können Sie feststellen, wie sich auf ihrem Betriebsystem die jeweiligen Adpater bzw. der Standard-KeyListener von Java verhalten.
Aktualisierung (15.5.2012): Für bestimmte Tasten, wie z.B. die Zirkumflex-("Dach")-Taste unterhalb der Escape-Taste, liefert Java nur Loslass-Ereignisse, obwohl
das darunter liegende (Linux-)Betriebssystem auch für diese Tasten die dazu gehörigen Drück-Ereignisse sendet. Solche Tastendrücke werden ignoriert, sind aber dennoch über die
KeyTyped()-Methode auswertbar.
Hinzugefügt wurden die clear()-Methoden, welche es z.B. ermöglichen, die Liste der gespeicherten Tastendrücke zu löschen. Dies ist im Zusammenhang mit dem Spiele-Modus und der
Verwendung von Dialogen sinnvoll: Wird z.B. bei einem Druck der Flucht-(ESC)-Taste ein modaler Dialog geöffnet, so wartet der TimedKeyAdapter nach Schließen des Dialoges
noch auf das Loslassen der Escape-Taste, was verwirrenderweise ein zweimaliges Drücken dieser Taste notwendig macht, um den Dialog mittels dieser Taste erneut zu öffnen.
In solch einem Fall kann man wie folgt vorgehen:
if (event.getKeyCode() == KeyEvent.VK_ESCAPE)
{
clear(event.getExtendedKeyCode());
openDialog();
}
Hier nun der von mir geschriebene Quelltext der Klasse TimedKeyAdapter.java, der zumindest bei mir unter Windows XP und Ubuntu 10.04 LTS das gewünschte (plattformunabhängige) Verhalten bzgl. der Tastaturereignisse liefert:
import java.awt.event.*;
import java.util.HashMap;
import javax.swing.Timer;
public class TimedKeyAdapter
implements KeyListener
{
private final int timerDelay;
private final HashMap<Integer, TimedKey> map = new HashMap<>(4);
private boolean gameModus = false;
private class TimedKey
implements ActionListener
{
private final int code;
private final Timer releaseTimer = new Timer(timerDelay, this);
private KeyEvent releaseEvent;
private TimedKey(int code)
{
this.code = code;
map.put(code, this);
}
public void restartTimer(KeyEvent e)
{
releaseEvent = e;
releaseTimer.restart();
}
public void stopTimer()
{
releaseTimer.stop();
releaseEvent = null;
}
@Override
public void actionPerformed(ActionEvent e)
{
releaseTimer.stop();
map.remove(code);
KeyReleased(releaseEvent);
releaseEvent = null;
}
}
public TimedKeyAdapter()
{
this(false);
}
public TimedKeyAdapter(boolean gameModus)
{
this(gameModus, 1);
}
public TimedKeyAdapter(boolean gameModus, int timerDelay)
{
this.gameModus = gameModus;
this.timerDelay = timerDelay < 0 ? 1 : timerDelay;
}
public void clear()
{
map.clear();
}
public void clear(int extendedKeyCode)
{
map.remove(extendedKeyCode);
}
@Override
public void keyPressed(KeyEvent e)
{
int code = e.getExtendedKeyCode();
TimedKey tk = map.get(code);
if (null == tk) {
new TimedKey(code);
KeyPressed(e);
} else {
tk.stopTimer();
if (!gameModus) {
KeyPressed(e);
}
}
}
@Override
public void keyReleased(KeyEvent e)
{
TimedKey tk = map.get(e.getExtendedKeyCode());
if (null != tk) {
tk.restartTimer(e);
}
}
@Override
public void keyTyped(KeyEvent e)
{
KeyTyped(e);
}
public int getPressedCount()
{
return map.size();
}
public void KeyPressed(KeyEvent e)
{
}
public void KeyReleased(KeyEvent e)
{
}
public void KeyTyped(KeyEvent e)
{
}
}
Das Prinzip ist dabei folgendes: Ein nicht in der internen Liste vermerkter Tastendruck wird einmalig in dieser Liste vermerkt. Ein Loslassen dieser Taste startet den ihr zugeordneten Taktgeber (Timer) und das zugehörige Loslass-Ereignis wird zurückgehalten. Ein erneut empfangenes Tastendruckereignis stoppt den Timer und ein (wiederholtes) Loslassereignis startet ihn neu. Der Timer sendet - sofern also nicht vorher eine erneutes Loslass-Ereignis eingetroffen ist - nach Ablauf eine Meldung, welche das zurückgehaltene Loslass-Ereignis dann wirklich weiterreicht. Somit sendet also der Taktgeber das endgültig verwertbare Loslass-Ereignis.
Im Unterschied zum so genannten Spiele-Modus, bei dem wirklich nur der erste Druck und das letztmalige Loslassen einer Taste durchgereicht werden, werden im normalen Modus jedes Tastendruck-
aber auch hier nur das letztmalige Loslass-Ereignis durchgereicht.
Diese Methode funktioniert auch bei Betriebsystemen, die bei einem Tastendruck ausschließlich ein Drück-Ereignis liefern, da hier der Taktgeber stets erst mit Eintreffen des
abschließenden Loslass-Ereignisses ausgelöst wird.
Ich verwende für den Timer eine Wartezeit von 1 Millisekunde. Dies könnte u.U. eine zu kurze Zeit sein, möglicherweise dann, wenn die Wiederholrate betriebssystemseitig größer eingestellt ist.
Eingebunden wird diese Klasse z.B. folgendermaßen:
JButton button = new JButton();
button.addKeyListener(new TimedKeyAdapter(true) {
public void KeyPressed(KeyEvent evt) {
System.out.println("- " + e.getKeyCode());
}
public void KeyReleased(KeyEvent evt) {
System.out.println("+ " + e.getKeyCode());
}
public void KeyTyped(KeyEvent evt) {
System.out.println("# " + e.getKeyCode());
}
});
Wichtig ist dabei, dass statt der üblichen key*()-Methoden aussschließlich die neuen Key*()-Methoden verwendet werden, da nur diese die geplanten
Ereignisse liefern.
Die key*()-Methoden sollten als privat betrachtet und nicht verwendet werden.
Die Methode getPressedCount() liefert die Anzahl der gleichzeitig gedrückten Tasten (im Applet ist bei Verwendung der anderen KeyListenern keine Angabe zu sehen, da diese
keine entsprechende Methode zur Verfügung stellt).