Die neue Fokusarchitektur des JDK 1.4

Im Visier des Anwenders

Manfred Hardt

Selbst in Zeiten von Drag&Drop bleibt die Option einer tastaturgestützten Bedienung für viele Anwendungssysteme ein ergonomisches Muss. Wer mit der Implementierung von Swing/AWT-Benutzerschnittstellen beschäftigt ist, sollte sich immer mit dieser Anforderung auseinandersetzen, kennt aber gleichzeitig auch die Tücken im Umgang mit dem Eingabefokus. Der Version 1.4 des JDKs wird nun ein überarbeitetes Fokusmanagement spendiert, das viele Vereinfachungen verspricht. Dieser Artikel zeigt aus pragmatischer Sicht, was sich geändert hat und wie die Vorteile der neuen Architektur genutzt werden können.

Praktische Prüfung

Als kniffliges Unterfangen bei der Entwicklung von Benutzerschnittstellen erweist sich häufig die Kontrolle des Eingabefokus. Abstrakt gesprochen geht es dabei um die Markierung derjenigen grafischen Komponente, die aktuell Tastatureingaben entgegennimmt. Auch Java hat diesbezüglich seine Stolpersteine [1]. Welcher GUI-Entwickler hat nicht schon einmal vor dem Problem gestanden, während der Verarbeitung eines Focus lost-Events Informationen zu der grafischen Komponente ermitteln zu müssen, an die der Fokus übertragen wurde? Bislang war diese Referenzierung der Gegenseite mit Bordmitteln des JDKs nicht direkt zu lösen. Workarounds waren zwar möglich, aber bedeuteten immer, dass ein zusätzlicher Verwaltungszyklus zur Laufzeit notwendig wurde.
Selbst eine Strategie für die Ermittlung der aktuell fokussierten Komponente blieb lange dem Einfallsreichtum des Anwendungsentwicklers überlassen. Ein Konsultieren von javax.swing.FocusManager usw. blieb ohne Ertrag. Eine Lösung durch die Hintertür bot bislang die Klasse javax.swing.SwingUtilities mit findFocusOwner(), was sich in der Praxis allerdings nicht immer als voll funktionstüchtig erwies. Zudem deutet schon das Auslagern einer so wichtigen Funktionalität in eine Hilfsklasse auf den eher experimentellen Charakter der Implementierung hin.
Nicht zuletzt war gerade das Fokus-Management oftmals ein großes Hindernis für die reale Plattformunabhängigkeit von Java. Die Portierung einer GUI-intensiven Anwendung von einem Betriebssystem auf ein anderes war nicht selten mit Ernüchterung verbunden, da grafische Komponenten ihren gewohnten Dienst versagten. Die Eigenarten der Fokusbehandlung verschiedener nativer Fenstersysteme waren im Detail nicht immer trivial und unter einen Hut zu bringen.
Diese Unhandlichkeiten und Inkonsistenzen sowie eine stetig wachsende Liste von Bugreports [2] erklärte man bei Sun nun (endlich) als guten Grund dafür, die bestehende Fokusarchitektur zu analysieren und fit für die Zukunft zu machen [3].

Was war, …

Die neue Architektur wird die Handhabung des Fokusmanagements nicht vollständig in Frage stellen. Es wird Erweiterungen und partielle Änderungen geben [4]. Deshalb lohnt es sich, grundlegende Strukturen des aktuellen Fokusmanagements zu rekapitulieren [5].
Im Kern stand bisher die Klasse javax.swing.FocusManager, die auf spezielle Tastaturereignisse wartet und entsprechend den Fokus zwischen Komponenten weiterreicht (für gewöhnlich TAB, um den Fokus zur nächsten Komponente vorzurücken bzw. Shift-TAB für eine gegenteilige Bewegung). FocusManager selbst implementiert keine Transferstrategie (landläufig als Tab order bezeichnet). Die betroffenen Methoden sind abstrakt. Eine entsprechende Standardimplementierung hält javax.swing.DefaultFocusManager bereit. Den Schnittstellenumfang dieser Klasse darf man zu Recht als spartanisch bezeichnen. Insbesondere fällt auf, dass eine Methode fehlt, die die aktuell fokussierte Komponente ermittelt. Dem erfahrenen Entwickler bietet Swing darüber hinaus die Option, eine eigene Ableitung von FocusManager zu nutzen. Allerdings sollte die Komplexität dieses Vorhabens keinesfalls unterschätzt werden.
Weiterhin sind die Klassen java.awt.Component bzw. javax.swing.JComponent maßgeblich beteiligt. Sie bieten Funktionalitäten, mit denen

  • eine grafische Komponente explizit den Fokus anfordern kann (requestFocus()),
  • ermittelt werden kann, ob eine wohlbekannte Komponente den Fokus inne hat (hasFocus()) oder
  • der Fokus an eine andere Komponente weitergereicht werden kann (transferFocus()).

Mit Hilfe der Methode setRequestFocusEnabled() lässt sich die Anforderung des Fokus durch eine Komponente begrenzen. Das Ausschließen von Komponenten vom Fokussieren ist nur durch ein umständliches Überschreiben der Methode isFocusTraversable() zu erreichen.
Der eigentliche Transfer des Fokus obliegt immer der Strategie des aktuellen Fokusmanagers. Ein bewusstes Weiterreichen des Fokus von einer Komponente auf eine bestimmte andere wird nicht unterstützt. So ist es fast unmöglich, die Strategie zur Laufzeit zu manipulieren. Die Tab-Order wird beim GUI-Design durch die Reihenfolge des Addierens der Komponenten auf den Container vorgegeben. Die Position der Komponente auf dem Container hat keine Auswirkungen. Zwar kann die Tab-Order zur Buildtime noch mittels setNextFocusableComponent() manipuliert werden, aber dieses Vorgehen ist erstens aufwändig und zweitens fehleranfällig, wenn später weitere Komponenten hinzugefügt werden müssen. Zudem ist gerade bei der Verwendung von GUI-Builder-Tools die Reihenfolge des Addierens der Komponenten ein Glücksspiel, das ein händisches Nachsortieren im Code oft unumgänglich macht. Insbesondere wenn ein GUI-Builder-Tool nach jeder Designänderung zum Neusortieren des Codes neigt, kann diese Mühe schnell zur Sisyphus-Arbeit werden.
Zu guter Letzt ist das Interface java.awt.event.FocusListener zu betrachten. Dieser Listener bietet einer Komponente die Möglichkeit zu ermitteln, dass sie den Fokus erhalten hat (java.awt.event.FocusEvent.FOCUS_GAINED) oder abgeben musste (java.awt.event.FocusEvent.FOCUS_LOST). In diesem Zusammenhang belegt ein Blick in die Klasse java.awt.event.FocusEvent die oben angeführte Schwäche bei der Bestimmung der Gegenseite eines Fokustransfers, denn diese Information wird per se mit einem solchen Event nicht mitgeliefert.

… was wird?

Die Neuerungen der Fokusarchitektur des JDK 1.4 können als bedarfsgerecht bezeichnet werden. Man erkennt sofort, dass die Hauptarbeit in der Lösung bekannter Unhandlichkeiten und Inkonsistenzen bestand. Dies gelingt unter anderem dadurch, dass man den Umfang des plattformabhängigen Programmcodes im Bereich des Fokusmanagements zu Gunsten eines generischeren APIs reduzieren konnte. Dieser leichtgewichtige Aufbau benötigt keine bzw. weniger native Datenstrukturen bzw. Peerklassen [6].
Die zukünftige Architektur definiert einige neue/erweiterte Konzepte und damit auch eine neue Terminologie (focus owner, focused window, focus traversal cycle etc.). Gehen wir hier stellvertretend auf die Idee des focus cycle root etwas genauer ein. Dieses Konzept ist keineswegs komplett neu, sondern findet bereits im JDK 1.3 seine Anwendung. Allerdings wird es erst mit dem JDK 1.4 zur vollständigen Reife ausgebaut. Mit einem focus cycle root ist ein Container gemeint, der einen eigenständigen Traversierungszyklus hat. Sobald eine Komponente dieses Containers den Fokus erhält, erfolgt eine Fokusübergabe nur noch an Komponenten des gleichen Containers. Damit der Fokus diesen Zyklus verlassen kann, ist eine up/down cycle-Anweisung nötig. Dieser Aufbau kann auch rekursiv sein, d.h. ein Container, der selbst focus cycle root ist, kann einen oder mehrere Container enthalten, die es ihrerseits ebenfalls sind.
Zur Verdeutlichung ein Beispiel (siehe Abb. 1 und die Beispielanwendung CycleRootDemo auf der beiliegenden Heft-CD). Ein Frame enthält zwei Textfelder und zwei Panels. Der Frame ist als Unterklasse von java.awt.Window bereits ein focus cycle root. Einem seiner Panels weisen wir diese Eigenschaft mittels setFocusCycleRoot(true) ebenfalls zu. Beim Traversieren der Komponenten läuft der Fokus nun über die Textfelder sowie die Elemente des ersten Panels. Werden die Elemente des als focus cycle root deklarierten Panels (in der Abpictureung rechts) erreicht, fällt auf, dass jedes weitere Traversieren nur noch in diesem Container stattfindet. Erst durch eine explizite upCycle-Operation (im Beispiel durch den Button auslösbar) kann dieser innere Traversierungszyklus wieder verlassen werden.

Abb. 1: Focus cycle root

Focus cycle root und die anderen Konzepte der neuen Fokusarchitektur werden im JDK 1.4 durch Modifikationen und Erweiterungen bestehender APIs umgesetzt. Im Folgenden werden die wichtigsten Änderungen im Einzelnen erläutert. Auf der beiliegenden CD finden sie einige kleine Anwendungen, die diverse hier besprochene Aspekte praktisch demonstrieren. Im Text wird auf die dazu korrespondierende Beispielanwendung hingewiesen. Experimentieren Sie damit und erweitern Sie diese bewusst einfach gehaltenen Demonstrationen.

Aufgebohrt

Bezogen auf den Umfang der API-Erweiterungen hat die Klasse java.awt.KeyboardFocusManager die Nase vorn. Sie übernimmt die Aufgaben der Klasse javax.swing.FocusManager, die nun in der Vererbungshierarchie unter KeyboardFocusManager gerutscht ist. Mit java.awt.DefaultKeyboardFocusManager liefert Sun auch hier eine Standardimplementierung.
Jeder KeyboardFocusManager ist auch immer eine Instanz von java.awt.KeyEventDispatcher und damit dafür verantwortlich, in seinem Kontext KeyEvents entgegenzunehmen und zu verteilen. Bevor also ein solches Event zu einer Komponente durchkommt, wird es im Allgemeinen vom KeyboardFocusManager behandelt.
Im Gegensatz zum alten FocusManager ist der neue KeyboardFocusManager weitaus auskunftsfreudiger. Dies lässt sich zum Einen durch direkte Abfrage bestimmter Eigenschaften über entsprechende Getter-Methoden wie z.B. getActiveWindow() oder zum Anderen durch Registrieren eines PropertyChangeListeners (siehe unten) nutzen. Darüber hinaus können Eigenschaften über Setter-Methoden verändert werden. Als ein Beispiel von vielen sei hier die Belegung der Fokusnavigationstasten genannt, die mit setDefaultFocusTraversalKeys() modifiziert werden kann. Zur Definition der Tasten bietet sich die ebenfalls neue Klasse java.awt.AWTKeyStroke an (siehe auch Beispielanwendung TraversalKeysDemo).
Neben der Durchführung von Fokusnavigationen, die aus Benutzereingaben resultieren, kann ein KeyboardFocusManager auch weiterhin dazu genutzt werden, den Fokus programmatisch zu setzen. Methoden wie focusNextComponent() sind schon vom alten FocusManager bekannt. Hierzu kommen nun Funktionalitäten zur Handhabung der bereits besprochenen focus cycle roots.

Komponenten und Container

Neben dem Kernelement des Fokusmanagements, dem KeyboardFocusManager, sind zusätzlich diverse fokusbezogene Funktionalitäten auf die betroffenen GUI-Komponenten selbst dezentralisiert. Dabei bleiben die oben genannten Möglichkeiten zur Anforderung (requestFocus()) und zur Abgabe (transferFocus()) des Focus natürlich erhalten. Bei requestFocus() wird allerdings deutlich darauf hingewiesen, dass plattformabhängige Effekte nicht ausgeschlossen werden können. Stattdessen sollte besser requestFocusInWindow() verwendet werden. Die Methode hasFocus() bekommt mit isFocusOwner() einen Aliasnamen (hasFocus() ist laut vorliegendem API nicht deprecated!).
Eine Komponente kann nun direkt als fokussierbar bzw. nicht fokussierbar (setFocusable()) benannt werden. Das umständliche Verfahren mittels Überschreiben der Methode isFocusTraversable() wird damit unnötig. Im Zusammenhang mit Containern ist sicherlich die Eigenschaft, dass diese nun, wie oben schon angedeutet, als focus cycle root deklariert werden können (setFocusCycleRoot()), für den praktischen Einsatz am bedeutendsten.

Wo geht’s lang?

Einen offenen Designansatz hat man für die Tab-Order-Strategie gewählt. War diese Strategie vorher tief im FocusManager versteckt, geht man nun den Weg des Strategy-Patterns und lagert den eigentlichen Algorithmus in eine eigene Klasse der Art java.awt.FocusTraversalPolicy aus. Der Ordnungsalgorithmus ist damit nicht mehr durch die Reihenfolge des Addierens der Komponenten auf den Container begrenzt. Verschiedene Klassen bieten unterschiedliche Algorithmen an und können gegeneinander ausgetauscht werden.
AWT/Swing liefern bereits einige FocusTraversalPolicies. So verkörpert java.awt.ContainerOrderFocusTraversalPolicy das wohl bekannte Verfahren, bei dem die Ordnung durch die Reihenfolge im Array bestimmt ist, das von Container.getComponents() geliefert wird. Mit javax.swing.SortingFocusTraversalPolicy erhält man ein Werkzeug, die Komponenten anhand eines Comparator vergleichbar zu machen und damit in eine gewünschte Ordnung zu bringen. Sinnvoll erscheint auch der Ansatz von javax.swing.LayoutFocusTraversalPolicy. Diese Strategie teilt den Container in Zeilen und Spalten, um eine Ordnung aufgrund der damit verfügbaren Koordinaten herzustellen.
Abpictureung 2 zeigt ein Beispiel für die Anwendung einer SortingFocusTraversalPolicy (siehe auch Beispielanwendung TraversalPolicyDemo). Die vier Felder sind mit editierbaren Gewichtungen versehen. Die verwendete TraversalPolicy liefert als nächste zu fokussierende Komponente immer das Feld mit der geringsten Gewichtung, hier eins. Ändert man im Feld eins stehend dessen Gewichtung z.B. auf 10, erfolgt der nächste Fokuswechsel in das Feld zwei.

Abb. 2: SortingFocusTraversalPolicy

Sollte diese Sammlung Ihnen noch nicht die richtige Strategie bieten, können Sie natürlich Ihre eigene FocusTraversalPolicy entwickeln und mit setFocusTraversalPolicy(FocusTraversalPolicy policy) entweder einem einzelnen Container oder direkt dem KeyboardFocusManager zuordnen.

Hört, hört!

Auch im Umfeld der Listener und Events bietet die Überarbeitung der Fokusarchitektur einige Neuerungen. Ein oben angesprochenes Problem löst eine einfache Erweiterung von java.awt.event.FocusEvent. Instanzen dieser Klasse beinhalten nun durch Hinzunahme der Methode getOppositeComponent() eine Informationsquelle zur Gegenseite eines Fokustransfers. Wird einem Objekt der Erhalt bzw. die Abtretung des Fokus mittels eines Focuslisteners gemeldet, lassen sich nun ohne zusätzlichen Aufwand sowohl Quelle als auch Senke dieser Aktion bestimmen (siehe auch Beispielanwendung OppositeSideDemo).
Bislang gab es auf einigen Betriebssystemplattformen und deren Fenstersystemen ein Problem, wenn eine von java.awt.Window abgeleitete Klasse, die kein Frame oder Dialog war (lightweighted), aktiviert wurde. Diese erhielt keine Nachricht über ihre Aktivierung, da die Peerklassen einiger nativer Fenstersysteme dem FocusManager den Event WINDOW_ACTIVATED unterschlugen. Die Folge war, dass diese Fenster bei der Zuteilung des Fokus ins Straucheln kamen, da nur aktive Fenster fokussierte Komponenten haben dürfen. Um dieses Verhalten zu umgeben, werden solche Ereignisse ab JDK 1.4 über den KeyboardFocusManager synthetisiert, d.h. der KeyboardFocusManager wacht über die richtige Abfolge von Aktivierung und Fokussierung.
In diesem Zusammenhang entstand nun als Nebenprodukt das neue Interface java.awt.event.WindowFocusListener, das über den Fokuserhalt- bzw. die Fokusabgabe eines Fensters benachrichtigt. Hierzu sind die Methoden windowGainedFocus() und windowLostFocus() zu nutzen. Will man sich Arbeit ersparen, kann man auch java.awt.event.WindowAdapter nutzen, da diese Klasse das Interface implementiert.
Durchaus zu begrüßen ist die Gelegenheit, durch die Registrierung von Property- und VetoListener an einer Komponente oder einem KeyboardFocusManager Fokusaktivitäten beobachten zu können. Diese Listener benachrichtigen über die Änderung diverser Eigenschaften, wie z.B. focus owner, focused Window etc., und erlauben es, ein Veto gegen diese Modifikationen einzulegen (siehe auch Beispielanwendung PropertyListenerDemo). Aber Vorsicht, man sollte diese Konzepte nicht missverstehen und infolge dessen zweckentfremden. So könnte z.B. jemand auf den Gedanken kommen, ein Verifizieren von Benutzereingaben mit diesen Listenern aufzubauen, was durchaus machbar scheint. Dies leistet aber javax.swing.InputVerifier für die meisten Anwendungsfälle bereits out of the box.

Fazit

Die neue Fokusarchitektur des JDK 1.4 kommt gut durchdacht und ideal ergänzt daher. Zusammen mit anderen Konzepten wie z.B. InputVerifier, hat der Entwickler nun brauchbare Werkzeuge an der Hand, um Benutzerschnittstellen noch robuster und ergonomischer zu implementieren. Allein der Weg hierher bleibt zu hinterfragen. Lange türmten sich berechtigte Kritiken in der BugParade. Zu oft haben Entwickler ihre eigenen Workarounds schaffen müssen. Deshalb sei die Frage erlaubt: Warum nicht gleich so, Sun ?

Links und Literatur

[1] The AWT Focus Subsystem
java.sun.com/j2se/1.4/docs/api/java/awt/doc-files/FocusSpec.html

[2] BugParade
developer.java.sun.com/developer/bugParade/bugs/4290675.html

[3] AWT Enhancements in the JavaTM 2 SDK, v1.4
java.sun.com/j2se/1.4/docs/guide/awt/AWTChanges.html

[4] Focus API changes
java.sun.com/j2se/1.4/docs/guide/awt/1.4/focusAPIChanges.html

[5] Mark McCulley – Focus on Swing
www.javaworld.com/javaworld/

[6] Java AWT: Lightweight UI Framework
java.sun.com/j2se/1.4/docs/guide/

Geschrieben von
Manfred Hardt
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.