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):
  • Parsen aller Actionhandler, Erzeugen der benötigten Klassen und Ablegen dieser in einer Hashtable anhand des angegebenen Namens
  • Parsen aller Menüstrukturen und Hinzufügen der Actionhandler zu den entsprechenden Menüeinträgen
  • Parsen der Toolbar-Information (wiederum mit dem Hinzufügen der Actionhandler)

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 {
...
public Main(String[] args) {
super(Demo App);
if (args.length != 0) {
parseXmlData(args[0]);
} else {
parseXmlData(null);
}
...
}

public void createInterface(String initDataFileName) {
InputStream stream = null;
Hashtable actions = new Hashtable();
if (initDataFileName == null) {
// we use the file from the jar-archive
stream = Main.class.getResourceAsStream("all.xml");
} else {
// or we use the file from the command line args
try {
stream = new FileInputStream(initDataFileName);
}
catch (IOException ioexception) {
ioexception.printStackTrace();
}
}
if (stream == null) {
throw new RuntimeException("init xml file not found");
}
InitParser initParser = new InitParser(actions,stream);
initParser.parse();

// set the menu abr and the toolbar
JMenuBar menuBar = initParser.getMenuBar();
setJMenuBar(menuBar);
JToolBar toolBar = initParser.getToolBar();
getContentPane().add(toolBar,BorderLayout.NORTH);

try {
stream.close();
}
catch (IOException ex) {
// exception will be ignored here
}
}

public static void main(String[] args) {
new Main(args);
}
}

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-Datei
In 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 besitzt. Er umschließt alle anderen Tags. Sofern der Parser das entsprechende Ende Tag erreicht (</tool>) wird das Parsen beendet und der Parser gibt die Kontrolle an die aufrufende Funktion zurück. Unterhalb des root-Knotens existieren verschiedene Unterkategorien, die auch in der hier angegebenen Reihenfolge benutzt werden sollten, da sie aufeinander aufbauen.

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 Tag definiert. Jede einzelne Action besitzt die Attribute name, class, key, mods, tooltip und icon, die aber nicht alle zwingend nötig sind.
Eine interne Callback-Funktion des InitParser wird bei jedem gefundenen Tag aufgerufen und der InitParser versucht eine Klasse mit dem angegebenen Klassennamen dynamisch zu instantiieren. Dabei versucht der InitParser einen Konstruktor der Klasse aufzurufen, der keine Argumente besitzt. Besitzt die Klasse einen solchen Konstruktor nicht (oder mussten die Actionhandler-Klassen erweitert werden und besitzen nur noch Konstruktoren mit Parametern), dann muss der Parser erweitert werden. Mehr dazu siehe weiter unten.
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" ?>
<tool>
<steps num="20" />

<actions>
<action name="quit" class="de.magictcs.actions.QuitAction"
key="q" mods="control" tooltip="Exits the application." />
<action name="showabout" class="de.magictcs.actions.ShowAbout"
tooltip="Shows about dialog" />
<action name="dummy1" class="de.magictcs.actions.Dummy1"
key="d" mods="shift" tooltip="Invoke Dummy1 action"
icon="dummy1.png" />
<action name="check1" class="de.magictcs.actions.Check1"
tooltip="Click to enable/Disable" />
<!-- add more actions here -->
</actions>

<menubar>
<menu name="Testmenu">
<menu name="Submenu">
<menu name="Submenu2">
<menuitem name="Dummy1" action="dummy1" />
</menu>
</menu>
<separator />
<menuitem name="Log Output" action="check1" type="check" />
<separator />
<menuitem name="Quit" action="quit" />
</menu>
<menu name="Help">
<menuitem name="About..." action="showabout" />
</menu>
</menubar>

<toolbar>
<toolbutton name="Dummy1" action="dummy1" type="button" />
<separator />
<!-- add more here -->
</toolbar>

</tool>

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 {
...
protected void progress() {
// does nothing yet
}
}

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 Tags wird die Funktion protected void setProgressSteps(int steps); aufgerufen, die in der default-Implementierung nichts weiter tut, aber in Subklassen durchaus an Bedeutung gewinnen kann (z.B. durch Setzen der Min und Max Werte eines JProgressBar Objektes). Aktionen sind dabei im Wesentlichen die Erzeugung eines einzelnen Actionhandlers, die Erzeugung eines Menus, eines Menuitems, eines Submenus oder eines Toolbar Buttons. Praktisch reicht es oft auch, in Tag einfach die Anzahl der Zeilen der Init-Datei anzugeben, da oft pro Zeile eine Aktion definiert ist und es keinen Unterschied macht, ob man die maximale Anzahl an Schritten erreicht. Die Fortschrittsanzeige würde dann einfach z.B. nur bis 80% steigen, was aber kein großes Problem darstellen dürfte.

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)
protected JMenuItem createMenuItem(String name,String type)

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.

© 2004 Software & Support Verlag GmbH. Vervielfältigung nur mit Genehmigung des Verlags. Fragen?