![]() |
|
URL dieses Artikels:
zu Ausgabe:
7.2002
Oberflächen dynamisch
XML für graphische Benutzerschnittstellen
von Karsten Trott
Als universelles und freidefinierbares Austauschformat hat XML eine starke Position im Client Server Bereich erreicht. Dieser Beitrag zeigt, dass XML auch im Bereich von Standalone-Anwendungen Einsatz finden kann. Als Beispielapplikation wird dabei eine XML-Datei zur Definition der graphischen Nutzerschnittstelle verwendet und erlaubt damit einfachste Änderungen der Applikation, die sogar auch durch den Endanwender erfolgen können.
Im Zeitalter der graphischen GUI-Builder (Netbeans, JBuilder & Co) stellt sich die Frage, warum man etwas anderes als die GUI-Builder verwenden sollte. Oft wünscht man sich jedoch eine flexiblere Möglichkeit, die Anordnung der Menüstrukturen beliebig ändern zu können ohne ständig mit den Wizards arbeiten zu müssen. Die generierten Strukturen der Wizards entbehren außerdem oft jeglicher Wiederverwendbarkeit. Auch ist das Ändern der Struktur bei Nutzung der graphischen Wizards zeitaufwendig und fehlerträchtig. Dass es auch anders geht, soll dieser Beitrag zeigen. Er stellt eine universelle Komponente vor, die die Erzeugung von Menüstrukturen und die Generierung der Toolbar einer graphischen Nutzerschnittstelle übernimmt. Dabei wird deren Erzeugung über eine XML-Datei gesteuert. Das bietet mehrere Vorteile. Zum einen ist XML relativ einfach zu erlernen und somit auch durch die Endanwender noch zu ändern, sodass diese die Oberfläche ihren Bedürfnissen entsprechend anpassen können. Das betrifft dabei sowohl die Menüeinträge als auch deren Keyboard Kürzel und auch die Anordnung kann nahezu beliebig geändert werden. Analoges gilt für die Anordnung der Toolbar-Buttons. Die Initialisierungsdatei kann dabei ebenfalls zur Internationalisierung der Nutzerschnittstelle verwendet werden (wobei man hier besser auf die entsprechenden Pakete der Java Bibliotheken zurückgreifen sollte). Ein weiterer Vorteil stellt das dynamische Hinzufügen (und Entfernen) von Einträgen und deren Aktionen dar (ist ebenfalls durch Endanwender möglich). Das erlaubt zum Beispiel das Erzeugen verschiedener Versionen mit unterschiedlichem Funktionsumfang. All dies kann ohne jegliche Neukompilierung der Applikation erfolgen. Alle benötigten Klassen und Strukturen werden dynamisch über das Reflection Interface geladen. Die einzigen dazu erforderlichen Komponenten sind ein XML-Parser (in diesem Beispiel wird der XML-Parser aus dem JAXP-Package von Sun verwendet, aber jeder andere SAX Parser ist geeignet und kann auch dynamisch geändert werden) und die Klasse InitParser, die im Folgenden vorgestellt werden soll. Auf die Installation des Parser soll hier nicht weiter eingegangen werden, da die benötigten Komponenten von java.sun.com werden können. Funktionsweise Es existieren drei grundsätzlich verschiedene Möglichkeiten, XML Dateien zu lesen und zu verarbeiten. Zum einen kann man die XML-Datei über das DOM-Interface komplett als Baum in den Speicher laden und dann innerhalb des Baums navigieren. Für große XML-Dateien ist diese Vorgehensweise allerdings nicht mehr praktikabel, da der Speicherverbrauch recht groß werden kann. Die zweite Variante ist die Verwendung eines SAX-Parsers, der sequentiell die XML-Datei liest und Aktionen durch so genannte Callback-Funktionen auslöst. Die eigentliche XML-Struktur wird dabei nicht durch den Parser im Speicher gehalten, sondern der Anwender muss sich selbst um die zu sichernden Informationen kümmern. Der Vorteil ist die extrem schnelle Abarbeitung und der deutlich geringere Speicherverbrauch. Deshalb wurde der InitParser auch als SAX-Parser realisiert. (Näheres zu den Unterschieden zwischen SAX- und DOM-Parsern kann im Java Magazin 4/2002, Seite 94ff nachgelesen werden). Auf die dritte Möglichkeit, ein XSL-Frontend zu verwenden, welches die XML-Beschreibung der graphischen Schnittstelle in eine Java-Sourcedatei überführt, die dann im nächsten Schritt kompiliert werden kann, soll hier nicht eingegangen werden. Diese Variante besitzt etliche Nachteile und kann wegen der fehlenden XSL-Engine und des fehlenden Compilers normalerweise nicht durch den Endanwender durchgeführt werden. Im Folgenden soll die Funktionsweise der Klasse, die das Parsen der XML-Datei und das Anlegen der benötigten GUI-Strukturen übernimmt, näher beschrieben werden. Die Klasse, die diese Aufgabe erledigt, ist der so genannte InitParser. Dieser erledigt folgende Aufgaben (in genau dieser Reihenfolge):
Ein komplettes Programm, das den InitParser benutzt, muss sich also um die Erzeugung der Menüstruktur und Toolbar keine Sorgen mehr machen, sondern holt sich diese nach dem Parsen der Initialisierungsdatei vom InitParser ab. Danach kann der InitParser zerstört werden (garbage collected), da seine Funktion für den weiteren Programmablauf nicht mehr benötigt wird. Die voll funktionsfähige Initialisierungsfunktion könnte also wie folgt aussehen:Sofern die Hashtable mit den Actionhandlern noch anderweitig verwendet werden soll, kann man sie in einer privaten Variable ablegen. Im Beispielprogramm ist ersichtlich, wie und warum man das macht. Interessanter jedoch wird die Applikation, wenn man dem Endanwender erlaubt, die graphische Schnittstelle nach seinen Wünschen zu ändern. Im Beispielprogramm Listing 1 ist dies demonstriert (kompletter Sourcecode auf beiliegender CD enthalten). Dort wird das erste Kommandozeilenargument (sofern vorhanden) als Dateinamen der Init-Datei interpretiert und an den InitParser übergeben. Es erlaubt dabei das einfache Testen verschiedenster Konfigurationen. In praktischen Anwendungen sollte dies allerdings auf das Lesen der Standard Resource-Datei aus dem Jar-Archive oder einer nutzerdefinierten Init-Datei aus seinem Home-Verzeichnis beschränkt sein. Listing 1 final public class Main extends JFrame {
Der InitParser besitzt einige interessante Erweiterungsmöglichkeiten, auf die später noch eingegangen wird. Zunächst soll die Struktur des gewählten XML-Formats forgestellt werden. Derzeit arbeitet der InitParser als nicht validierender Parser (sprich ohne entsprechende DTD-Beschreibung). Eine DTD würde das Parsen noch sicherer machen. Da es sich bei diesem Projekt aber um keine sicherheitsrelevanten Probleme handelt, dürfte das in der Praxis kein großes Problem darstellen. Inhalt der Init-DateiIn Listing 2 ist ein kurzes Beispiel einer Init-Datei ersichtlich, das die wesentlichen Elemente demonstriert. Eine XML-Datei besitzt genau ein Root-Tag, welches in der gewählten Variante den Namen Zunächst wäre da der optionale <steps> Tag. Auf ihn wird bei der Erklärung der Zusatzfunktionen eingegangen. Er hat für das eigentliche Erzeugen der Schnittstellen keine Bedeutung und wird nur bei der Fortschrittsanzeige benutzt. Anschließend versucht der Parser zunächst sämtliche Actionhandler des Programms dynamisch zu erzeugen. Alle zu erzeugenden Actions sind in das <actions>...</actions> Tag eingeschlossen. Die Action selbst wird danach mit einem Eine interne Callback-Funktion des InitParser wird bei jedem gefundenen Bei Erfolg wird die Action in einer speziellen Hashtable mit dem angebenen Namen als Key gespeichert. Das erlaubt eine Referenzierung der Actions in folgenden Tags über diesen Namen. Zum Beispiel wird die QuitAction unter dem Namen quit gespeichert. Die zu instanziierenden Icons werden derzeit aus einem Resource gelesen, die im Jar-Archive unter /images/xx.png sich befindet (wobei xx.png im icon-Attribute referenziert wird). Hier ist ebenfalls eine Erweiterung für die nächste Version angedacht, um das Laden der Icon-Grafiken flexibler zu gestalten. Rein praktisch wäre es sogar denkbar, über den Klassennamen einen Actionhandler zu erzeugen, der nicht zum eigentlichen Programm gehört, sondern aus anderen Applikation stammt. Das würde zum Beispiel das Einbinden externer Programmfunktionalität ermöglichen. Dieser Ansatz wurde aber im vorliegenden Programm nicht weiter verfolgt. In der vorliegenden Beispiel XML-Datei aus Listing 2 existieren vier Actionhandler (quit, showabout, dummy und check1), es können aber beliebig viele hinzugefügt werden. Es können auch Actionhandler hinzugefügt werden, auf die später nicht verwiesen wird. Diese werden aber trotzdem in der Hashtable der Actions abgelegt, da der Parser zu diesem Zeitpunkt nicht wissen kann, dass diese Actions keine Verwendung finden. Außerdem ist denkbar, dass man Actions über ihren Namen referenzieren will (z.B. in der Verwendung in Dialogen) und aus diesem Grund die Hashtable der Actionhandler als private Variable des Programms sichert. Zu beachten ist nur, das die Namen der Actionhandler eindeutig sind. Ansonsten werden beim Ablegen in der Hashtable ältere Referenzen überschrieben und damit unbrauchbar. Nach den Actionhandler wird die Menüstruktur aufgebaut. Dazu dienen die Tags <menubar>, <menu>, <menuitem> und <separator>. Im Beispiellisting 1 wird eine JMenuBar erzeugt, die dann ein erstes JMenu mit dem Namen Testmenu erhält. Anschließend werden diesem Menü zwei weitere Untermenüs hinzugefügt (Submenu und Submenu2) und dem Submenu2 anschließend ein JMenuItem, das als Actionhandler die zuvor geparste Action mit dem Namen dummy1 erhält. Nachfolgend werden ein Separator und weitere Menuitems auch als Menüs hinzugefügt. Die Schachtelungstiefe der Menüs ist dabei nicht begrenzt, ebenso wie die Benutzung der Separatoren. Derzeit werden nur zwei verschiedene Typen der Menuitem unterstützt. Das sind zum einen einfache JMenuItem Strukturen (siehe Beispiel für Dummy1) und zum anderen JCheckBoxMenuItem Strukturen, die durch ein zusätzliches Attribute: type=check markiert werden. Geplant ist für die nächste Release des InitParsers die direkte Unterstützung von JRadioButtonMenuItem. Alternativ besitzt der InitParser eine Methode, mit der auch beliebige und somit frei definierbare Menu Strukturen erzeugt werden können (dazu später mehr). Die Tags inklusive ihrer Attribute sind selbsterklärend. Das Attribute name bezeichnet dabei den sichtbaren Menueintrag, und action dabei den Namen eines vorher geparsten Actionhandlers. Falls ein Menueintrag angelegt wurde und mit dem jeweiligem Actionhandler verbunden ist, erhält der Menueintrag automatisch die im Actionhandler definierten Keyboard Shortcuts und die Tooltipinformation. Nachdem das Menu erzeugt worden ist, beginnt der Parser mit der Erzeugung der Toolbar. Dazu dienen ihm die Informationen, die im <toolbar> Tag eingeschlossen sind. Da Toolbars derzeit keine geschachtelten Strukturen erlauben, stellt sich deren Struktur als relativ einfach dar. Unterstützt wird derzeit der Typ button - geplant sind aber auch hier wiederum direkte Unterstützungen anderer Elemente. Auf der CD sind einige Beispiele enthalten, die intensiven Gebrauch der beschriebenen Elemente machen. Dem Leser wird empfohlen, diese Beispieldateien als Init-Dateien dem Beispielprogramm als Kommandozeilenargument zu übergeben und danach die Auswirkung auf die Applikation zu untersuchen: java -jar demo.jar initfile.xml. Da es relativ einfach ist, weitere Init-Dateien zu erzeugen, können alle Möglichkeiten des InitParsers ausgelotet werden. Zu beachten ist noch, dass bei der Verwendung von JCheckBoxMenuItem Strukturen die Actions nur als Actionhandler zum Einsatz kommen, nicht aber als ChangeListener! An einer Verbesserung der Anbindung spezieller Listener Klassen wird aber ebenfalls gearbeitet. Das Beispiel aus Listing 2 erzeugt dann eine Applikation wie in Abbildung 1 ersichtlich. ![]() Abb. 1: Beispielapplikation aus Listing 2 Listing 2 <?xml version="1.0" ?>Erweiterungen des InitParsers Es wurde versucht, den InitParser so flexibel wie möglich zu gestalten. Trotzdem ist es nie möglich alle Funktionalität einzubinden, zum anderem ist es auch nicht immer sinnvoll. Um den InitParser zu erweitern, existieren einige Funktionen, die als protected markiert worden sind. Auf diese Möglichkeiten der Erweiterung des InitParsers soll im Folgenden eingegangen werden. Der InitParser wurde bewusst entworfen, Erweiterungen durch Subklassen zu erzeugen. Dies hält zum einen den Parser kompakt, da nur eine einzige Klasse instanziiert wird (mit zwei weiteren internen Klassen) und der Erweiterung als Subklasse des InitParsers. Eine Erweiterung durch Delegeation an entsprechende Klassen macht auch relativ wenig Sinn, da im Normalfall immer nur eine einzige Builder-Klasse zum Einsatz käme, für die dann wieder entsprechende Interface Definitionen bereit gestellt werden müssten, was die Anwendung des InitParser unnötig kompliziert. Die Mehrzahl der potentiellen Anwender werden die meisten Erweiterungsmöglichkeiten sowieso nicht benötigen, da ihnen die Standardfunktionalität ausreichen wird. Aus diesem Grund kann auch auf ein Class-Diagram in UML-Notation verzichtet werden. Die Erweiterungen können benutzt werden, um z.B. eine Fortschrittsanzeige anzusteuern oder um benutzerdefinierte Actions oder Menüstrukturen zu erzeugen. Der InitParser selbst ruft nach jeder internen Aktion die Funktion progress() auf. Die default-Implementierung gibt dabei einfach einen . (dot) nach System.out aus. Wenn man dies nicht möchte oder eine andere Fortschrittsanzeige wünscht, kann man den Parser einmal ableiten und die Funktion überladen. Eine praktische Realisierung um die Ausgabe zu unterdrücken, könnte wie folgt aussehen: public class SilentParser extends InitParser {
Analog können andere Fortschrittsanzeigen erzeugt werden. Der <steps> Tag hilft dabei die maximale Anzahl an Aktionen abzuschätzen. Das ist nötig, da der Parser als SAX-Parser arbeitet und somit den Datenstrom nur einmal durchläuft. Beim Parsen des Eine weitere Möglichkeit, den InitParser zu erweitern, ist die weiter oben angesprochene Möglichkeit, benutzerdefinierte Aktionen oder Menuitems zu erzeugen. Dazu existieren die folgenden Funktionen: protected Action createActionFromClass(Class c)Die erste Funktion kann dabei benutzt werden, wenn die Actionhandler keinen Defaultkonstruktor besitzen oder anderen speziellen Bedingungen folgen. Praktisch erfolgt eine Nutzung oft, wenn die Aktion auf eine besondere Weise initialisiert werden soll oder die Aktion eine Referenz auf andere Objekte besitzen muss. Die zweite Funktion findet ihre Anwendung, wenn spezielle Menueinträge erzeugt werden sollen, die nicht nur aus Text, Icons und Keyboard-Shortcuts bestehen. Anwendung kann dies z.B. finden, wenn man ein JMenuItem konstruiert, das nur eine Farbe zeigt (zur Farbauswahl) oder wie aus Zeichenprogrammen bekannt unterschiedliche Strichstärken oder Stricharten grafisch zeigen soll. Die Funktionen müssen dabei nur die jeweiligen Typen zurückgeben, um die korrekte Verarbeitung im InitParser zu gewährleisten. Nachteile des InitParser-Prinzips Natürlich ist nicht alles Gold was glänzt und so hat die beschriebene Methodik auch ihre Nachteile. So werden bedingt durch das Funktionsprinzip Syntaxfehler in der Init-Datei erst zur Laufzeit erkannt. Analog kann es auch passieren, dass bestimmte Actionhandler nicht geladen werden können, da sie über das Reflection Interface nicht gefunden wurden. Das ist besonders bei der Verwendung intelligenter Werkzeuge von Nachteil, die beim Erzeugen des Jar-Archives nur die Klassen aufnehmen, die auch innerhalb des Sourcecodes referenziert werden (so genannte Jar-Optimierer). Normalerweise reduziert das die Größe des Jar-Archives, in diesem Fall werden aber auch alle Actionhandler-Klassen nicht in das Jar-Archive aufgenommen, da sie nicht im Sourcecode benutzt werden, sondern nur in der Init-Datei Erwähnung finden. Ebenso kompliziert wird es bei der Verwendung von Obfuscatoren, die Klassennamen und Methodennamen, sowie Variablennamen durch spezielle Kurzformen ersetzen, um das Dekompilieren der Java Klassen zu erschweren. Da alle Actionhandler-Klassen über das Reflection Interface instanziiert werden, ist es wichtig, dass die Klassennamen und die Funktionsnamen nicht durch Obfuscatoren geändert worden sind. Bei der Verwendung eines Obfuscators sollte man die Actionhandler-Klassen ausschließen. Praktisch lassen sich diese Probleme aber relativ einfach durch einen Testlauf der Applikation herausfinden. Sollte keinerlei Fehler während des Ladens der Applikation auftreten, konnten augenscheinlich alle Klassen erzeugt und referenziert werden. Etwas komplizierter ist es, die durch das Ändern der Init-Datei erzeugten Probleme zu finden. Diese können oft nicht auf den ersten Blick erkannt werden. So ist es relativ einfach möglich, zwei Actionhandler auf das gleichen Keyboard Kürzel zu binden. Welche Action dann später verwendet wird, ist nicht konsistent voraussagbar. Ebenso kann man durch Ändern der Attribute verwirrende Funktionalität erzeugen (z.B. indem man Quit auf den Actionhandler print bindet, was zwar keinen Sinn ergibt, aber durchaus machbar ist). Mit etwas Sorgfalt lassen sich diese Probleme aber weitgehend verhindern. Ausblick Der InitParser befindet sich zur Zeit in einem weiteren Entwicklungsschritt, um seine Funktionsvielfalt zu erweitern und weitere Möglichkeiten (wie JRadioButtonMenuItem zu integrieren). Geplant sind außerdem Verfahren, um Fonts und Colors über die Init-Datei zu erzeugen und den graphischen Strukturen entsprechend zuzuweisen. Außerdem sollen auch Informationen der kontextsensitiven Hilfe über die Init-Datei zuweisbar werden. Entsprechende Tags sind schon vorhanden und für diese Erweiterungen als reserviert anzusehen. Dieses Projekt soll auch anderen Programmierern Mut machen, XML zur Speicherung persistenter Datenstrukturen einzusetzen. Denkbar wäre auch eine Erweiterung des beschriebenen InitParsers, um Dialoge dynamisch aus XML-Beschreibungen zu erzeugen. |
||
|