171 Copyright 1996-1998 by Axel T. Schreiner. All Rights Reserved. Eine konfigurierbare Tastatur — calc/Keyboard.java Keyboard.NUMBERS Keyboard.CALC0 Keyboard.CALC1 Keyboard dient zum Experimentieren mit einem Keyboard-Objekt. Die Applikation endet durch Betätigen von Q. Ein Keyboard-Objekt ist eine frei belegbare Tastatur. Jede Taste kann mit der Maus oder durch Tippen bedient werden und liefert dann einen ActionEvent. Welche getippten Zeichen eine Taste auslösen und was als actionCommand verschickt wird, ist bei Konstruktion eines Keyboard-Objekts konfigurierbar; als Voreinstellung gilt der Inhalt der Tasten. Einige Layouts sind vordefiniert; die Unicode-Zeichen werden allerdings unter Linux/Motif nicht dargestellt. Keyboard demonstriert viele Feinheiten, die man beim Aufbau neuer Grafikobjekte berücksichtigen muß. Liest man die Quellen von Component und Container, könnte man durch Ersetzen undokumentierter Methoden Keyboard möglicherweise ‘‘eleganter’’ implementieren, aber hier wird eine Strategie verfolgt, die nur mit dokumentierten Methoden auskommt. 172 Beispiele und Testprogramm Eine Tastatur wird aus einer String-Matrix konstruiert. Einige sind vordefiniert: {apps/calc/Keyboard.java} import java.awt.*; import java.awt.event.*; /** A class to simulate a rectangular keyboard with buttons and keypresses */ public class Keyboard extends Panel implements FocusListener, KeyListener { /** pad with numbers */ public static final String[][] NUMBERS [] = {{{ "1"}, {"2"}, { "3"}}, {{ "4"}, {"5"}, { "6"}}, {{ "7"}, {"8"}, { "9"}}, {{ "Q"}, {"0"}}}; /** pad with numbers and operators */ public static final String[][] CALC0 [] = {{{ "1"}, {"2"}, { "3"}, {"+"}, {"-"}}, {{ "4"}, {"5"}, { "6"}, {"\327","*x","*"}, {"\367","/:","/"}}, {{ "7"}, {"8"}, { "9"}, {null}, {"=", "=\r\n"}}, {{ "Q"}, {"0"}, { "C"}}}; /** pad with numbers, operators, and parentheses */ public static final String[][] CALC1 [] = {{{ "1"}, {"2"}, { "3"}, {"("}, {")"}}, {{ "4"}, {"5"}, { "6"}, {"+"}, {"-"}}, {{ "7"}, {"8"}, { "9"}, {"\327","*x","*"}, {"\367","/:","/"}}, {{ "Q"}, {"0"}, {"\261", "~", "_"}, {"%"}, {"=", "=\r\n"}}}; /** mostly trivial test program: keyboard for calculator */ public static void main (String args []) { Keyboard kb = new Keyboard(NUMBERS); kb.addActionListener(new ActionListener() { public void actionPerformed (ActionEvent e) { String s = e.getActionCommand(); System.out.println(s); if (s.equals("Q")) System.exit(0); } }); Frame f = new Frame(); f.add("Center", kb); f.pack(); f.show(); } {} erste Zeile der String-Matrix definiert die Breite der Tastatur. Jeder Eintrag der Matrix Die muß einen String für die Taste enthalten. Ein zweiter String kann die getippten Zeichen festlegen, die die Taste auslösen sollen, ein dritter String kann das actionCommand explizit festlegen. 173 Konstruktion {apps/calc/Keyboard.java} /** @param board matrix of keys, row 0 determines width, each entry is label [, chars [, actionCommand]] where chars can be pressed instead of clicking label */ public Keyboard (String[][] board []) { super(new GridLayout(board.length, board[0].length, 3,3)); for (int row = 0; row < board.length; ++ row) for (int col = 0; col < board[0].length; ++ col) if (col >= board[row].length || board[row][col][0] == null) add(new Label("")); else { Key b = new Key(board[row][col][0], board[row][col].length > 1 ? board[row][col][1] : null); if (board[row][col].length > 2 && board[row][col][2] != null) b.setActionCommand(board[row][col][2]); add(b); addKeyListener(b); } lastFocus = getComponent(0); } {} Für jeden Matrixeintrag wird entweder eine Label ohne Beschriftung oder ein Key erzeugt, dabei limitiert die erste Matrixzeile die Breite des GridLayout der Tastatur. Falls vorhanden, erhält ein Key einen zweiten String im Matrixeintrag als zweites Argument bei der Konstruktion — dieser String enthält die getippten Zeichen, die den Key auslösen. Ein dritter String wird als actionCommand eingetragen. Das Keyboard ist Container und KeyListener für alle Key-Objekte. Fokus Keyboard interessiert sich für alle getippten Tasten. Für externe Component-Objekte sollte man das Keyboard als FocusListener eintragen, dann wird der Fokus immer von dort gestohlen und der zuletzt benutzten Key zugewiesen: {apps/calc/Keyboard.java} /** tracks last key to have had focus */ protected Component lastFocus; /** if this is FocusListener, steal focus from them */ public void focusGained (FocusEvent e) { lastFocus.requestFocus(); } /** ignored */ public void focusLost (FocusEvent e) { } {} 174 KeyEvent-Management In X11 würde man über Beschleuniger als Ressourcen einzelne Tasten der Tastatur mit Knöpfen auf dem Schirm verbinden; diese würden sich dann bei Tastendruck ‘‘bewegen’’. Im AWT 1.1 wird jeder Event nur noch einer Component zugestellt, folglich muß man hier jedes getippte Zeichen von jedem Key aus erkennen können, obgleich nur jeder Key selbst weiß, welche Zeichen er als äquivalent betrachtet. Eine Lösung besteht darin, das Keyboard als KeyListener aller Key-Objekte zu verwenden und jedes getippte Zeichen vom Keyboard an alle Key-Objekte als KeyListener des Keyboard-Objekts zuzustellen. Das Keyboard könnte dann auch noch gezielt für externe Component-Objekte als KeyListener dienen. {apps/calc/Keyboard.java} /** this multicasts keyPressed to each Key */ protected KeyListener keys; /** manage keys list */ public void addKeyListener (KeyListener l) { keys = AWTEventMulticaster.add(keys, l); } public void removeKeyListener (KeyListener l) { keys = AWTEventMulticaster.remove(keys, l); } /** forward keyTyped to each Key */ public void keyTyped (KeyEvent e) { if (keys != null) keys.keyTyped(e); } /** ignored */ public void keyPressed (KeyEvent e) { } public void keyReleased (KeyEvent e) { } {} konstruiert eine lineare Liste von Objekten, die als jede Art von *Listener verwendet werden können. Die Konstruktion ist speziell darauf ausgelegt, die Methoden add*Listener und remove*Listener zu implementieren. AWTEventMulticaster Schickt man einen Event an die Liste, wird er an alle Elemente verteilt. Es scheint nicht möglich zu sein, einen Button per Programm optisch zu betätigen — die Tastatursimulation ist also wenig ansprechend. 175 ActionEvent-Management deutet an, wie man einen Verteiler für Events einrichtet. Da jeder Key seinen ActionEvent an alle ActionListener des Keyboard-Objekts verteilen soll, richtet man einen weiteren Verteiler speziell für ActionEvent ein: keys {apps/calc/Keyboard.java} /** each Key multicasts actionPerformed to each registered ActionListener */ protected ActionListener actions; /** manage actions list */ public void addActionListener (ActionListener l) { actions = AWTEventMulticaster.add(actions, l); } public void removeActionListener (ActionListener l) { actions = AWTEventMulticaster.remove(actions, l); } {} Key stammt von Button ab, muß aber einen ActionEvent über die actions im Keyboard verteilen: Key {apps/calc/Keyboard.java} /** A component class to model a key on the keyboard */ public class Key extends Button implements KeyListener { protected String chars; // any of these also triggers action public Key (String label, String chars) { super(label); this.chars = chars; this.addKeyListener(Keyboard.this); // start analyzing keypress enableEvents(AWTEvent.ACTION_EVENT_MASK); // register for action } /** multicasts action to actions multicaster */ protected void processActionEvent (ActionEvent e) { if (actions != null) actions.actionPerformed(e); lastFocus = this; } {} Aus Effizienzgründen erhält man einen Event nur, wenn die zugehörige Maske gesetzt ist. Da addActionListener() vernünftigerweise nicht verwendet wird, würde Key einen ActionEvent nicht empfangen, deshalb setzt man die Maske mit enableEvents() . Ein Event kommt bei processEvent() an und wird an process*Event() übergeben. Von dort werden normalerweise die eigenen *Listener angesprochen. 176 keyTyped Als KeyListener muß Key passende Zeichen als ActionEvent verteilen: {apps/calc/Keyboard.java} /** if keyChar is in chars [or label]: maps keypress to action */ public void keyTyped (KeyEvent e) { if ((chars != null ? chars : getLabel()).indexOf(e.getKeyChar()) >= 0) { processActionEvent(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, getActionCommand())); e.consume(); lastFocus = this; requestFocus(); } } /** ignored */ public void keyPressed (KeyEvent e) { } public void keyReleased (KeyEvent e) { } } } {} consume() setzt ein Bit, das bei manchen Events verhindert, daß sie an weitere *Listener verteilt werden. Fokus-Probleme Es zeigt sich, daß die Fokussierung eine Vielzahl von Portabilitätsproblemen aufwirft. Die hier gewählte Strategie beruht auf folgenden Beobachtungen: Prinzipiell sollte man jeder Component mit requestFocus() die Tastatur zuordnen können. Praktisch muß die Component aber sichtbar sein, um überhaupt Events empfangen zu können: Setzt man requestFocus() auf Keyboard, empfängt man bei Windows keine Zeichen. Der erste Fokus in einem Fenster wird ziemlich zufällig gewählt: Windows setzt offenbar auf die zuerst eingefügte Component, Linux auf die erste, die normalerweise Zeichen empfängt — auch wenn das dann etwa ein nicht editierbares TextField ist. Gibt es für ein nicht editierbares TextField einen KeyListener, kann man fokussieren und tippen, aber das gibt beep-Klänge unter Linux/Motif. Setzt man in processActionEvent() den Fokus mit Keyboard.this.requestFocus(), dann flackert das ganze Keyboard bei Knopfdruck. Hält man den Fokus immer auf einem Key-Objekt, wird es zwar optisch ausgezeichnet, aber wenigstens funktioniert das System.