Skript zu Benutzungsoberflächen
Transcrição
Skript zu Benutzungsoberflächen
Benutzungsoberflächen Skript V0.3 vom 11.10.2010 Prof. Dr.-Ing. Holger Vogelsang SWT/JFace und RCP Don’t panic Inhaltsverzeichnis 1 Einleitung 1.1 Organisation der Vorlesung . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Vorbereitungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Kassifikation von Anwendungen 2.1 Einteilung nach Architekturen . . . . . . . . 2.2 Einteilung nach Zielplattformen . . . . . . . 2.3 Übersicht . . . . . . . . . . . . . . . . . . . 2.4 Architekturen von Fat-Client-Anwendungen 2.5 Interaktionseigenschaften . . . . . . . . . . 3 SWT und JFace 3.1 Grundlagen . . . . . . . . . . . . 3.2 Layout-Management . . . . . . . 3.3 SWT-Widgets . . . . . . . . . . . 3.4 Menüs und Toolbar-Leisten . . . 3.5 Basisklassen . . . . . . . . . . . 3.6 Ereignisbehandlung . . . . . . . 3.7 Model-View-Controller mit JFace 3.8 Weitere Container . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 5 5 . . . . . 7 7 9 10 10 12 . . . . . . . . 14 16 25 50 54 63 67 79 100 4 Anbindung an die Geschäftslogik 101 5 Deklarative Beschreibungen 102 6 Eclipse Rich Client Platform 103 7 Multithreading in Java 7.1 Threads erzeugen und starten . . . . . . . . 7.2 Threads beenden und löschen . . . . . . . . 7.3 Zugriff auf gemeinsam genutzte Ressourcen 7.4 Synchronisation von Threads . . . . . . . . . 7.5 Threads als Daemons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 104 105 106 107 108 Inhaltsverzeichnis 8 Abbildungsverzeichnis 109 9 Literaturverzeichnis 112 10 Stichwortverzeichnis 114 1 Einleitung Diese Veranstaltung behandelt grafische Benutzungsoberflächen auf Basis der Eclipse Rich Client Platform. Ergonomische Aspekte werden nicht betrachtet, da es zu diesem Thema eine eigenständige Vorlesung gibt. 1.1 Organisation der Vorlesung Neben der Klausur kann auch eine Programmieraufgabe gelöst werden. Durch Abgabe einer korrekten Lösung erhalten Sie Bonuspunkte für die Klausur. Nähere Informationen sind auf der Webseite zur Vorlesung im Ilias unter dem FAQ zu finden. 1.2 Vorbereitungen 1.2.1 Installation des JDK unter Windows Zur reinen Ausführung von Java-Programmen genügt das JRE (Java Runtime Environment). Darin fehlen aber beispielsweise wichtige Werkzeuge wie der Compiler. Hier in der Vorlesung sollen eigene Programme erstellt werden. Daher muss das JDK (Java Development Kit, manchmal auch SDK genannt) verwendet werden. 1.2.2 Integrierte Entwicklungsumgebungen Die Anwendung des „nackten“ JDKs ist nicht sehr komfortabel. Daher bietet sich die Verwendung einer integrierten Entwicklungumgebung an. Da in der Vorlesung die Eclipse Rich Client Platform eingesetzt wird, bietet es sich an, Eclipse auch als IDE zu verwenden. Bitte wählen Sie auf den Pool-Rechnern als Projektverzeichnis ein Verzeichnis auf dem Netzlaufwerk. Zur lokalen Installation auf einem privaten Rechner laden Sie von http://www.eclipse.org/downloads die Version „Eclipse for RCP and RAP Developers“ herunter. Sie beinhaltet bereits alle notwendigen Plugins für die Vorlesung. Die ZIP-Datei wird einfach in ein eigenes Verzeichnis extrahiert. Damit Sie später die Oberflächen nicht manuell erstellen müssen, bietet sich die Verwendung eines GUI-Editors an. Eclipse liefert mit dem VEP (Visual Editor Plugin) eine eigene Implementierung, die aber nicht immer im neuesten Eclipse funktionsfähig ist. Statt dessen kann für private Zwecke der Jigloo GUI-Editor (http://www.cloudgarden.com/jigloo/) oder noch besser 1.2 Vorbereitungen der WindowBuilder Pro (http://code.google.com/webtoolkit/tools/wbpro/ index.html) von Google als Eclipse-Plugin bezogen werden. Auf den Seiten befinden sich auch die Installationsanleitungen. 1.2.3 Eclipse Rich Client Platform Im Laufe der Vorlesung wird sich zeigen, dass die Eclipse Rich Client Platform am besten separat installiert wird. Dazu wird unter http://download.eclipse.org/eclipse/ downloads/ die Eclipse-Version ausgewählt (am besten „Latest Release“) und auf der folgenden Seite das „Platform SDK“ für die verwendete Betriebssystem-Version. Bitte achten Sie darauf, dass inzwischen neben 32-Bit- auch 64-Bit-Betriebssysteme unterstützt werden. Wählen Sie Variante, die zu Ihrer installierten Java-Version passt. Die Plattform wird ebenso wie die IDE in ein Verzeichnis entpackt. Es darf sich dabei nicht um dasselbe Verzeichnis wie die IDE handeln. 1.2.4 Aufbau dieses Skriptes Das Skript beinhaltet eine Einführung in alle wichtigen Themen, die zur Lösung der Aufgabe notwendig sind. Weiterhin betrachtet es viele Beispiele ausführlicher als die PowerPoint-Unterlagen der Vorlesung. So wird beispielsweise im Skript deutlich mehr Bezug auf das verwendete API genommen. 1. Einleitungskapitel: Dieses lesen Sie jetzt gerade. 2. Klassifikation von Anwendungen: Wie unterscheiden sich Anwendungen, bezogen auf die Benutzungsoberfläche? 3. Einführung in SWT: Hier finden Sie eine Einführung in SWT, die alle wichtigen Konzepte vorstellt. Parallel dazu sollten immer sowohl die JDK-Dokumentation als auch die Online-Hilfe aus Eclipse griffbereit sein. 4. Einführung in JFace: JFace setzt auf SWT auf und bietet eine höhere Abstraktion von den einzelnen Fensterelementen. So wird beispielsweise das MVC-Muster unterstützt. 5. Anbindung an die Geschäftslogik: Dieses Kapitel beschreibt die Trennung von Anwendungslogik und Benutzungsoberfläche. 6. Deklarative Beschreibungen: Hier wird gezeigt, wie sich Oberflächen deklarativ mit XML-Dialekten beschrieben lassen. 7. Rich Client Platform: Es wird die Idee des Frameworks vorgestellt. Am Ende des Dokumentes befindet sich in Kapitel 7 noch eine Einführung in das Multithreading unter Java. Abgeschlossen wird das Skript mit einem Abbildungs-, Literatursowie einem Stichwortverzeichnis. 6 2 Kassifikation von Anwendungen Anwendungen lassen sich sehr unterschiedlich in Kategorien einteilen. Da wir uns mit grafischen Benutzungsoberflächen befassen, erfolgt hier zunächst die Einordnung anhand der Architekturmerkmale für die Oberflächenkonstruktuktion. 2.1 Einteilung nach Architekturen In der Literatur sind die verwendeten Begriffe nicht so einheitlich zu finden, wie es die Aufstellung hier vermuten lässt. 2.1.1 Thin Client Es handelt sich dabei beispielsweise um eine Ajax-Anwendung, die in einem Browser ausgeführt wird. Charakteristisch sind: Start über eine URL Eine lokale Installation auf dem Client ist nicht erforderlich, da ein halbwegs aktueller Browser immer vorhanden sein sollte. Die Geschäftslogik befindet sich überwiegend auf dem Server. Die Kommunikation erfolgt nach dem Request/Response-Prinzip. Somit kann nur der Client den Server kontaktieren. Inzwischen existieren auch neuere Ansätze (z.B. mit Apache Comet), die es erlauben, den Client auch vom Server aus zu kontaktieren. Browser (HTML, JavaScript) Server (Application-Server, Datenbank, …) Abbildung 2.1: Kommunikation bei einem Thin-Client 2.1 Einteilung nach Architekturen 2.1.2 Rich Thin Client Die Anwendung läuft wie bei einem Thin Client im Browser ab. Allerdings wird hier nicht mehr auf die reinen Browser-Techniken gesetzt. Statt dessen führt ein Plugin im Browser die Anwendung aus. Dabei kann es sich beispielsweise um Java-Applets oder FlashProgramme handeln. So stehen wesentlich mächtigere Sprachmittel als bei reinen Thin Clients zur Verfügung. Eigenschaften: Start über eine URL Eine lokale Installation auf dem Client ist nicht erforderlich. Das Plugin kann automatisch durch den Browser nachinstalliert werden Die Geschäftslogik befindet sich überwiegend auf dem Server. Die Kommunikation erfolgt auch hier in der Regel nach dem Request/ResponsePrinzip. Browser (HTML, JavaScript, Flash, Applet) Server (Application-Server, Datenbank, …) Abbildung 2.2: Kommunikation bei einem Rich Thin Client 2.1.3 Rich Fat Client Ein Rich Fat Client (oder auch kurz Rich Client) ist eine Anwendung, die großenteils auf dem Client abläuft. Sie basiert auf einer Plattform wie z.B. Eclipse RCP oder NetBeans Platform. Merkmale: Es ist eine lokale Installation auf dem Client erforderlich. Java bietet mit Java Web Start einen eleganten Mechanismus, die Java-Anwendung durch einen Browser nachzuinstallieren. Damit ist nur noch die manuelle Einrichtung eines JRE auf dem Client erforderlich. Die Plattform kann zusätzliche Module vom Server nachladen. Weiterhin bieten viele Plattformen auch einen automatischen Update-Mechanismus, indem sie in bestimmten Abständen den Server nach neuen Modulversionen befragen und diese herunterladen. Die Geschäftslogik kann sich auf Client und Server befinden. Die Kommunikation zwischen Client und Server findet entweder nach dem Request/Response-Prinzip oder aber wirklich bidirektional statt. 8 2.2 Einteilung nach Zielplattformen Client (Eclipse RCP, …) Server (Application-Server, Datenbank, …) Abbildung 2.3: Kommunikation bei einem Rich Fat Client 2.1.4 Smart Client Der Begriff des Smart Clients ist häufig im Microsoft-Umfeld zu finden. Er bezieht sich fast immer auf Anwendungen, die auf .NET, ASP.NET, XAML usw. basieren. Eigenschaften: Der Start der Anwendung erfolgt aus dem Browser heraus über eine URL. Somit ist keine lokale Installation erforderlich. Die Geschäftslogik kann sich auf Client und Server befinden. 2.1.5 Fat Client (plattformunabhängig) Hierbei handelt es sich um eine „klassische“ Desktop-Anwendung. Sie wird beispielsweise in Java mit Swing oder SWT bzw. in C++ mit QT usw. erstellt. Wichtig ist also, dass sowohl die verwendeten Bibliotheken als auch der eigentliche Quelltext plattformunabhängig sind. Damit ist die Anwendung auf binärer Ebene zwar nicht direkt auf allen unterstützten Plattformen ausführbar, aus dem Quelltext lässt sich aber durch einfaches Übersetzen auf einer anderen Zielplattform ein lauffähiges Programm erzeugen. Charakteristisch sind: Der Start erfolgt lokal auf dem Client. Dazu ist auch eine lokale Installation erforderlich. Die Geschäftslogik ist überwiegend auf dem Client, teilweise aber auch auf dem Server vorhanden. 2.1.6 Fat Client (plattformabhängig) Prinzipiell gelten dieselben Aussagen wie für den plattformunabhängigen Fat Client. Als Besonderheit kommt aber hinzu, dass plattformabhängige Bibliotheken verwendet werden. So könnte die Benutzungsoberfläche mit Hilfe der MFC-Klassen von Microsoft erstellt worden sein, für die es keine entsprechende Implementierung auf anderen Systemen gibt. 2.2 Einteilung nach Zielplattformen Neben den klassischen Desktop-PCs und Laptops gibt es eine Reihe weiterer Hardwareplattformen, die eine Anwendung unterstützen kann. Mobile Geräte: Diese besitzen unterschiedliche Betriebssysteme wie Windows CE/Mobile, Symbian OS, Google Android, iOS usw. 9 2.4 Architekturen von Fat-Client-Anwendungen Unterschiedliche Leistungen: Nicht alle Geräte haben genügend Speicher oder ausreichend leistungsfähige Prozessoren. Auch die Größen der Displays können sich signifikant unterscheiden: Normale mobile Telefone, Smartphones, PDAs, TabletPCs, usw. Unterschiedliche Programmierumgebungen: Es gibt nicht auf allen Geräten einheitliche Entwicklungssprachen und -umgebungen: Java ME, C++, C#, usw. 2.3 Übersicht Die folgende Tabelle stellt die Vor- und Nachteile der verschiedenen Architekturen einander gegenüber. Die Einteilung ist nicht vollständig. Tabelle 2.1: Architekturvergleich Thin Client (Ajax) Rich Thin Client (Browser + Plugin) Rich Fat Client (Java) Microsoft Smart Client Java Fat Client Windows oder LinuX Fat Client ++ ++ / −1 Administration ++ ++ / − 2 −− − −− − − − − − Wartung (SW) − ++ ++ ++ ++ ++ Interaktion − ++ ++ ++ ++ ++ Performance − − ++ ++ ++ ++ GUI-Konsistenz − − ++ ++ ++ ++ Plattformunabh. ++ + ++ −− ++ −− − − ++ / −−3 − ++ / −−3 ++ / −−3 Server Server Client und Server Client und Server überwiegend auf dem Client überwiegend auf dem Client Installation Arbeit bei Serverausfall Applikationslogik 2.4 Architekturen von Fat-Client-Anwendungen Eine Fat-Client-Anwendung besteht typischerweise aus mehreren Schichten, von denen hier nur einige betrachtet werden sollen: Hardware: Diese Schicht beinhaltet die eigentliche Ein- und Ausgabehardware, über die mit der Anwendung kommuniziert wird. Betriebssystem: Das Betriebssystem greift über Gerätetreiber auf die Hardware zu und abtrahiert gleichzeitig von ihr. Das Fenstersystem wird häufig als Bestandteil des Betriebssystems angesehen. Es nutzt die Abstraktionen von der Hardware, um den Anwendungen Fenster zur Darstellung von Inhalten sowie Interaktionen mit den Fenstern anzubieten. 1 Eventuell notwendige Installation einer Laufzeitumgebung 2 Eventuell notwendige Einrichtung einer Laufzeitumgebung 3 Hängt von der Konzeption ab 10 2.4 Architekturen von Fat-Client-Anwendungen Benutzungsschnittstelle (Struktur): Sie ist für die Darstellung und Verwaltung des Inhalts in den Fenstern verantwortlich. Sie übernimmt auch die Anordnung der einzelnen Fensterelemente (z.B. Widgets) untereinander. Dialogsteuerung, Ein- und Ausgabe von Daten: Diese Schicht kontrolliert die Aktionen des Benutzers in Bezug auf deren Plausibilität. Sie prüft beispielsweise, ob gewisse Eingaben sinnvoll und korrekt sind. Weiterhin steuert sie die Benutzungsschnittstelle und die Geschäftslogik in der übergeordneten Schicht, indem sie auch die Daten in der Oberfläche und Geschäftslogik synchronisiert und Ereignisse weitermeldet. Anwendungskern (Geschäftslogik): Hier befindet sich die eigentliche Funktionalität der Anwendung. Sie wird teilweise von der Dialogsteuerung aufgerufen, wenn Ereignisse eingetreten sind. Anwendungskern (Geschäftslogik) Vorlesungsinhalt Dialogsteuerung, Ein-/Ausgabe von Daten Benutzungsschnittstelle (Struktur) Fenstersystem (Präsentation) Betriebssystem Tastaturtreiber Maustreiber Grafiktreiber Hardware Abbildung 2.4: Architektur einer Fat-Client-Anwendung Die Vorlesung beschäftigt sich im Wesentlichen mit der Dialogsteuerung sowie der Struktur der Benutzungsschnittstelle. Zusätzlich wird mit der Eclipse Rich Client Platform ein Framework betrachtet, das ein Gerüst für eine Fat-Client-Anwendung vorgibt. 11 2.5 Interaktionseigenschaften Das folgende Beispiel soll die einzelnen Schichten verdeutlichen: Struktur (Widgets und deren Anordnung) Geschäftslogik (Datum kleiner als aktuelles Datum) Ein- und Ausgabe (Daten aus dem Dialog) Präsentation (Farben, Zeichensätze, ...) Dialogsteuerung (entsperrt, wenn die Eingaben in Ordnung sind, löst Aktion aus) public class Customer { private String firstName; private String lastName; private int customerNumber; private boolean retired; private FamilyStatus familyStatus; private Date customerSince; // ... } Abbildung 2.5: Architekturschichten anhand eines Beispiels Durch die Struktur wird beschrieben, wie die einzelnen Widgets untereinander platziert sind und wie sich sich bei einer Größenänderung des Dialogs verhalten sollen. Das Aussehen der Widgets und der Fenster wird von der Präsentationsschicht des Fenstersystems übernommen. So kann ein einheitliches Erscheinungsbild aller Anwendungen in einem Fenstersystem erzielt werden. Die Eclipse Rich Client Platform zusammen mit SWT und JFace erlaubt, wie andere Toolkits auch, das Aussehen und Verhalten in gewissen Grenzen zu ändern. Die Dialogsteuerung kann beispielsweise die OK-Taste erst dann freigeben, wenn alle Eingaben in Ordung sind. Weiterhin löst sie durch einen Druck auf die Taste Aktionen aus. Die Dialogsteuerung kann nicht immer alle Eingaben selbst validieren, weil ihr dazu häufig Informationen fehlen, die nur in der Geschäftslogik zu finden sind. In dem Beispiel muss das eingegebene Datum bestimmte Bedingungen erfüllen, die von der Geschäftslogik festgelegt sind. Die im Dialog dargestellten oder veränderten Daten müssen der Geschäftslogik zur Verfügung gestellt werden bzw. stammen von ihr. Diese uni- oder bidirektionale Kommunikation wird von der Ein- und Ausgabeschicht übernommen. Im diesem Beispiel beinhaltet eine Java-Klasse die Daten des Dialogs. 2.5 Interaktionseigenschaften Ein Benutzer kann mit der Anwendung auf vielfältige Varianten kommunizieren, wenn diese das unterstützt: 1. Menü-Auswahl: Steuerung der Anwendung erfolgt durch Menü-Kommandos. 12 2.5 Interaktionseigenschaften 2. Formular-basiert: Die Daten werden in Formulare eingegeben. Diese Variante ist besonders gut für einfach strukturierte Daten geeignet. 3. Direkte Manipulation: Die Bearbeitung von Objekten erfolgt auf einer Arbeitsfläche durch Drag-und-Drop, kontextsensitive Menüs usw. Beispiele sind Drag-und Drop für Dateioperationen, UML-Editoren und Zeichenprogramme. Dafür gibt es bereits eine ganze Anzahl von Java-Bibliotheken wie Naked Objects http://www. nakedobjects.org/ und Eclipse GEF http://www.eclipse.org/gef/. 4. Sprachgesteuerte Interaktionen (natürliche Sprache, anwendungsspezifische Sprache): Dazu gehören Anweisungen auf der Kommandozeile, in Anwendungen eingebettete Skriptsprachen sowie die Erkennung gesprochener Befehle. In dieser Vorlesung spielen nur die Punkte 1 und 2 sowie etwas von Punkt 3 und 4 eine Rolle. 13 3 SWT und JFace Dieses Kapitel beschäftigt sich mit dem Standard Widget Toolkit („SWT“) sowie dem darauf aufsetzenden JFace, die beide zur Gestaltung der Benutzungsoberflächen verwendet werden. Anwendungskern (Geschäftslogik) Vorlesungsinhalt Dialogsteuerung, Ein-/Ausgabe von Daten Benutzungsschnittstelle (Struktur) Fenstersystem (Präsentation) Betriebssystem Tastaturtreiber Maustreiber Grafiktreiber Hardware Abbildung 3.1: Struktur der Oberfläche Warum werden hier aber gerade SWT und JFace verwendet? Es gibt ja auch Swing, das plattforumabhängig und im JDK schon enthalten ist, QT, Windows-Forms, und viele andere Toolkits. Zur Beantwortung der Frage ist zunächst ein Blick auf die Eigenschaften von SWT und JFace erforderlich: SWT wurde als Basis für die Entwicklung von Eclipse geschaffen. 3.1 Grundlagen SWT verwendet denselben Ansatz wie das „Abstract Window Toolkit“ (AWT). Dabei werden native Dialogelemente des Fenstersystems verwendet, soweit diese denn vorhanden sind. Ansonsten zeichnet SWT sie selbst. Somit gibt es innerhalb von SWT eine Bibliothek, die plattformabhängig ist. Die Programmierschnittstelle selbst ist plattformunabhängig. JFace bildet eine Abstraktionsschicht von SWT, indem es für viele Elemente den MVC-Ansatz anbietet und teilweise die Ereignisbehandlung vereinfacht. Darüber hinaus besitzt es eine Registry für Zeichensätze, Farben und Bilder sowie vordefinierte Dialoge und „Wizards“. Die folgende Aufstellung fasst die Vor- und Nachteile von SWT und JFace zusammen. + Native Dialogelemente sind häufig schneller als selbst gezeichnete. Sie passen sich auch der Darstellung des Fenstersystems an. Somit sieht eine SWT-Anwendung genauso aus wie jede andere Anwendung des Fenstersystems. + SWT hat innerhalb der Eclipse Rich Client Platform inzwischen einen Einzug in die Industrie gehalten. + Viele allgemeine Konzepte für Benutzungsoberflächen lassen sich anhand von SWT und JFace beispielhaft aufzeigen. + Es gibt hervorragende Entwicklungsumgebungen und Modellierungswerkzeuge, die teilweise kostenlos verfügbar sind. + Mit Hilfe der Rich Ajax Platform („RAP“) lassen sich aus bestehenden Fat-Clients relativ einfach Web-Anwendungen (Rich Thin Clients) erzeugen. + Es sind viele Erweiterungen verfügbar (z.B. GEF für grafische Editoren, . . . ). - Die Programmierschnittstelle des SWT ist teilweise nicht sehr objekt-orientiert ausgelegt. Insbesondere die häufige Verwendung von Konstanten statt zusätzlicher Klassen ist störend. - Es sind nicht so viele mächtige Dialogelemente wie in Swing vorhanden. - Teilweise ist keine Garbage-Collection möglich. Als Folge daraus müssen Betriebssystemressourcen manuell freigegeben werden. - Es werden keine Applets unterstützt. Das ist kein großer Mangel, da Applets in letzter Zeit immer seltener eingesetzt werden. Da SWT plattformabhängig ist, werden nicht automatisch alle Java-Plattformen unterstützt. Allerdings wurde SWT auf praktisch alle relevanten Fenstersysteme portiert: Windows 32 und 64 Bit: XP, Vista, 7 Linux 32 und 64 Bit: x86 (GTK), x86 (Motif), PPC (GTK), S390 (GTK) Solaris: x86 (GTK), Sparc (GTK) AIX: PPC (Motif) HP-UX: ia64 (Motif) Mac OS X: Carbon, Cocoa Verschiedene eingebettete Geräte (im Rahmen von eRCP) Anmerkung: Sie finden die Quelltexte aller Beispiele als Download auf der Homepage. In jedem Beispiel ist der Name der Quelltextdatei im Archiv angegeben. 15 3.1 Grundlagen 3.1 Grundlagen Dieser Abschnitt beschreibt die SWT-Grundlagen. Dazu gehören die Platzierung von GUI-Elementen („Widgets“) durch so genannte Layout-Manager sowie die allgemeine Verwendung der Widgets. 3.1.1 Die erste SWT-Anwendung Um einen ersten Eindruck vom Aufbau einer reinen SWT-Anwendung zu vermitteln, zeigt das folgende Beispiel ein kleines Programm, das nur einen Text in einer Taste darstellt (Quelltextdatei FirstSWTApplication.java). Die Seitenzahlen dienen nur der späteren Erklärung des Quelltextes. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class FirstSWTApplication { private Display display; private Shell shell; public FirstSWTApplication(Display display) { this.display = display; shell = new Shell(display); shell.setText("Titeltext"); shell.setSize(200, 100); } private void createGUI() { Button button = new Button(shell, SWT.PUSH); button.setText("Das ist ein Text"); button.setBounds(shell.getClientArea()); shell.pack(); } public void run() { createGUI(); shell.open(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) display.sleep(); } display.dispose(); } } public static void main(String[] args) { new FirstSWTApplication(Display.getDefault()).run(); } Damit ergeben sich die folgenden Bildschirmausgaben: Abbildung 3.2: Beispiel unter Windows 7, MacOS X und Linux (GTK) 16 3.1 Grundlagen Erste Erklärungen zum Quelltext: Zeile 3: Das Display-Objekt repräsentiert das Fenstersystem, in dem das Programm läuft. Zeile 4: Eine Shell ist ein „Top-Level“-Fenster des Fenstersystems. Später werden auch andere Elemente wie Dialoge vorgestellt. Zeil 14: Mit new Button wird eine Taste in das Fenster eingefügt. Wie die genaue Positionierung erfolgt, wird später erläutert. Wichtig zu wissen ist an dieser Stelle nur, dass keine Positionen angegeben werden, weil ein sogenannter LayoutManager die Platzierung übernimmt. An diesem Widget ist auch erkennbar, das Widgets in SWT immer erzeugt werden, indem deren Vaterelement als Parameter übergeben wird. In diesem Beispiel handelt es sich beim Vaterelement um das Fenster selbst (Parameter shell). Zeile 14: Durch Konstenten wie SWT.PUSH werden sehr häufig Eigenschaften eines Widgets bestimmt. Diese Taste ist eine normale Taste, die nach dem Drücken sofort wieder in ihren Ausgangszustand zurückkehrt. Zeile 15: button.setText trägt die Beschriftung der Taste ein. Zeile 16: button.setBounds legt fest, dass die Taste so groß wie der Inhaltsbereich des Fensters werden soll. Zeile 17: Mit shell.pack wird die Layout-Berechnung gestarten. Die Fenstergröße ermittelt sich aus der Größen und Positionen der Widgets innerhalb des Fensters. Wie das genau funktioniert, wird später erläutert. Zeile 9: shell.setText trägt die Titelzeile des Fensters ein. Zeile 10: shell.setSize setzt manuell eine Fenstergröße. Normalerweise wird diese aus dem Inhalt des Fenster berechnet. Die Zeilen 23 bis 27 schließlich sehen zunächst einmal einfach nur merkwürdig aus. Sie beinhalten die Ereignisschleife. Solange das Fenster nach dem Öffnen nicht geschlossen ist, werden Ereignisse zugestellt. Danach wartet die Anwendung auf weitere Ereignisse (Zeile 25). Nach dem Schließen des Fenster beendet sich die Schleife und alle Ressourcen der Anwendung werden durch display.dispose() freigegeben. Eine genauere Betrachtung der manuellen Freigabe erfolgt in Abschnitt 3.1.4. Eine SWT-Anwendung hat generell diese Schichtenstruktur: Widget Widget Widget Shell-Klasse Display-Klasse Betriebssystemspezifische Klassen Betriebssystem Abbildung 3.3: Struktur einer SWT-Anwendung 17 3.1 Grundlagen 3.1.1.1 Erforderliche Dateien Um das Beispiel übersetzen und ausführen zu können, ist eine jar-Datei erforderlich. Sie finden diese entweder direkt im Verzeichnis plugins Ihrer Eclipse-Installation oder im selben Verzeichnis Ihrer Eclipse-Zielplattform. Mit org.eclipse.swt.Windowsystem.OS.Arch_Version.jar ist vorerst nur eine Datei erforderlich. Dabei stehen die Abkürzungen für: Windowsystem: Name des Fenstersystems OS: Name des Betriebssystems Arch: Name der Prozessorarchitektur Version: Versionsnummer von SWT Die genauen Bezeichnungen für Eclipse 3.5 sehen auf den unterschiedlichen Plattformen so aus: org.eclipse.swt.win32.win32.x86_3.5.2.v3557f.jar für Windows 7, 32 Bit Intel-Architektur, SWT 3.5 org.eclipse.swt.gtk.linux.x86_3.5.2.v3557f.jar für Linux, GTK, 32 Bit Intel-Architektur, SWT 3.5 org.eclipse.swt.cocoa.macosx.x86_64_3.5.2.v3557f.jar für Mac OS X, Cocoa, 64 Bit Intel-Architektur, SWT 3.5 Die wichtigsten darin enthaltenen Pakete, die für die ersten Beispiele benötigt werden, sind org.eclipse.swt und org.eclipse.swt.widget. Später werden noch weitere jar-Dateien und Pakete hinzukommen. 3.1.1.2 Installation der jar-Dateien Die zur Übersetzung und Ausführung verwendeten jar-Dateien lassen sich auf mehreren Wegen in das eigene Projekt einbinden. In Bezug auf die spätere Vorstellung der Eclipse Rich Client Platform ist es aber am sinnvollsten, die Dateien nicht direkt in das eigene Projekt zu kopieren. Es gibt mehrere Möglichkeiten: In der Installation der Eclipse-IDE sind alle jar-Dateien vorhanden. Diese lässt sich als Plattform, gegen die entwickelt wird, eintragen. Das hat aber den großen Nachteil, dass die eigene Anwendung von der Version der IDE abhängig ist. Weiterhin sind in der IDE jar-Dateien vorhanden, die hauptsächlich für die IDE erforderlich sind. Diese Dateien könnten versehentlich auch in eigenen Projekten verwendet werden. Besser ist es, das RCP-SDK von http://download.eclipse.org/eclipse/ downloads/ herunterzuladen und in ein separates Verzeichnis auszupacken. Danach wird das SDK als Zielplattform eingetragen. Statt also die Dateien direkt in das eigene Projekt zu kopieren, ist es sinnvoller, mit einer sogenannten Zielplattform zu arbeiten. In dieser sind alle jar-Dateien vorhanden, die in den eigenen Projekten Verwendung finden sollen. Die Zielplatform kann in den globalen Eigenschaften der IDE eingestellt werden, wie Abbildung 3.4 zeigt. 18 3.1 Grundlagen Die Ziel-Plattform: Abbildung 3.4: Zielplattform einstellen Mit „Add...“ lassen sich neue Zielplattformen wie z.B. das entpackte RCP-SDK hinzufügen (Abbildung 3.5). Abbildung 3.5: Zielplattform einrichten Eine der Plattformen wird schließlich als aktiv markiert, indem der Haken vor deren Namen gesetzt wird. Jetzt können Abhängigkeiten zu einzelnen jar-Dateien der Zielplattform im eigenen Projekt eingestellt werden. Dazu dient die vordefinierte Variable ECLIPSE_HOME, die auf die aktuelle Zielplattform verweist. Abhängigkeiten sollten immer relativ zu dieser Variablen angegeben werden, damit die Plattform austauschbar bleibt (Abbildung 3.6). Eclipse hat manchmal Probleme, nach einem Wechsel der Zielplattform die Paket- und Klassennamen in den Projekten korrekt aufzulösen. Ein Neustart der IDE bewirkt hier Wunder. 19 3.1 Grundlagen über Variable einbinden Abbildung 3.6: Abhängigkeit von Dateien der Zielplattform 3.1.2 Die erste JFace-Anwendung Wie bereits erwähnt, ist JFace eine Klassenbibliothek, die auf SWT aufsetzt und bestimmte Interaktionen vereinfacht und Abstraktion wie z.B. den MVC-Ansatz bietet. Auf die direkte Verwendung von SWT-Klassen kann dennoch nicht verzichtet werden. Der folgende Quelltext zeigt eine Umsetzung des ersten SWT-Beispiels aus Abschnitt 3.1.1 mit den Mitteln von JFace (Quelltextdatei FirstJFaceApplication.java). Die Seitenzahlen dienen auch hier nur der späteren Erklärung des Quelltextes. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class FirstJFaceApplication extends ApplicationWindow { public FirstJFaceApplication() { super(null); // kein Vater-Fenster } // Erzeugen der Oberfläche @Override protected Control createContents(Composite parent) { Button button = new Button(parent, SWT.PUSH); button.setText("Das ist ein Text"); button.setBounds(parent.getClientArea()); parent.pack(); return parent; } public void run() { setBlockOnOpen(true); open(); Display.getDefault().dispose(); } } public static void main(String[] args) { new FirstJFaceApplication().run(); } Schon auf den ersten Blick ist erkennbar, dass das Grundgerüst der Anwendung anders ausfällt und die Ereignisschleife nicht mehr direkt sichtbar ist. Erklärungen: Zeile 10: Die Methode createContents wird intern automatisch von der Basisklasse ApplicationWindow aufgerufen, um die Oberfläche zu erzeugen. Als Er20 3.1 Grundlagen gebnis liefert sie die Vater-Komponente zurück, die alle anderen Komponenten beinhaltet. Es gibt weitere Methoden mit dem Namensaufbau createXYZ, die auch automatisch aufgerufen werden. Sie dienen beispielsweise der Erzeugung von Menüs. Zeile 19: setBlockOnOpen: Mit dem Parameter true kehrt der Aufruf der openMethode erst nach dem Schließen des Fensters zurück. Zeile 20: open: Das Fenster wird angezeigt. Der Aufruf kehrt in diesem Beispiel erst nach dem Schließen des Fensters zurück. Hier verbirgt sich dann auch die Ereignisschleife. Zeile 21: dispose: Alle Ressourcen des Fensters sowie die Ressourcen seiner Kindelemente werden wieder freigeben. Unterbleibt der Aufruf, dann kann Java manche Objekt nicht per Garbage-Collector freigeben. Weiterhin kann es passieren, dass dem Fenstersystem irgendwann die Ressourcen ausgehen. Die Freigabeproblematik wird gleich noch genauer besprochen. Eine JFace-Anwendung hat generell diese Schichtenstruktur: Widget Widget Widget Composite-Klasse ApplicationWindow-Klasse Shell von der ApplicationWindowKlasse erzeugt Display-Klasse Betriebssystemspezifische Klassen Betriebssystem Abbildung 3.7: Struktur einer JFace-Anwendung Im Unterschied zum reinen SWT wird hier durch die Basisklasse ApplicationWindow eine Struktur der Anwendung vorgegeben. 3.1.2.1 Erforderliche Dateien Zur Übersetzung und Ausführung sind einige plattformunabhängige jar-Dateien von JFace erforderlich. Auch diese sollten aus der Zielplattform stammen: org.eclipse.jface_Version.jar: Allgemeine JFace-Klassen org.eclipse.jface.text_Version.jar: Klassen zur Behandlung von Textdokumenten org.eclipse.equinox.common_Version.jar: Klassen für das zugrunde liegende OSGi-Framework. OSGi wird später im Rahmen der Rich Client Platform eingeführt. org.eclipse.core.commands_Version.jar: Klassen, die hauptsächlich von anderen JFace-Klassen verwendet werden 21 3.1 Grundlagen org.eclipse.core.runtime_Version.jar: Klassen für die Eclipse-Laufzeitumgebung. Auch diese wird mit der Rich Client Platform später betrachtet. org.eclipse.osgi_Version.jar: Weitere Klassen für das OSGi-Framework Zusätzlich sind für das Text-Beispiel später diese beidenjar-Dateien notwendig, die nicht Bestandteil des RCP-SDK sind. Sie können aus der Installation der Eclipse-IDE kopiert werden. org.eclipse.text_Version.jar com.ibm.icu.text_Version.jar Diese Dateien sollten auch wirklich nur dann kopiert werden, wenn deren Funktionalität erforderlich ist. Sie werden hier im Skript eingesetzt, um einen „mächtigen“ Texteditor mit Syntaxhervorhebung zu bauen. Die Dateien enthalten Teile des Editors aus der IDE. 3.1.3 Lauffähige Anwendung Obwohl SWT eine plattformabhängige Bibliothek benötigt, lässt sich aus einem solchen Projekt eine eigenständig lauffähige Anwendung erzeugen. Dazu muss das EclipseProjekt für Windows und LinuX lediglich als lauffähige jar-Anwendung mit allen Bibliotheken und Ressourcen exportiert werden. Für MacOS X ist es etwas komplizierter. Unter der URL http://www.eclipse.org/swt/macosx/ ist eine schrittweise Beschreibung vorhanden. 3.1.4 Freigabe von Ressourcen Im Zusammenhang mit SWT tritt ein Problem auf, mit dem in Java so normalerweise nicht gerechnet wird. Werden Ressourcen wie Objekte mit new auf dem Heap angelegt, dann kann der Garbage-Collector diese wieder freigeben, sobald keine Referenz mehr darauf verweist.1 Das funktioniert bei Anwendungen mit SWT so nicht immer. Der Grund ist darin zu sehen, dass SWT Ressourcen des Fenstersystems verwendet, um seine Widgets darzustellen. Zu diesen Ressourcen gehören beispielsweise Fenster, Farben, Zeichensätze und Tasten. Java-Objekte, die solche Ressourcen kapseln, können vom Garbage-Collector nicht automatisch wieder freigeben werden. Dieses muss manuell durch den Aufruf der dispose-Methode des Objektes erfolgen. Nach diesem Aufruf darf weder lesend noch schreibend auf das Objekt zugegriffen werden. Sollte das doch geschehen, dann quittiert SWT das mit dem Auslösen einer Ausnahme. Wie sieht das in der Praxis aus? Es gibt zwei einfache Regeln, die das Verhalten beschreiben. Regel 1: Wer ein Element erzeugt, gibt es auch mit dispose() wieder frei. Beispiel für die Erzeugung eines neuen Farb-Objektes: Color color = new Color(display, 0, 0, 255); ... color.dispose(); Wird dagegen ein existierendes, vordefiniertes Element verwendet, dann darf es nicht freigeben werden. Beispiel für die Verwendung eines von SWT erzeugten FarbObjektes: 1 Ok, das ist etwas vereinfacht ausgedrückt. 22 3.1 Grundlagen Color color = display.getSystemColor(SWT.COLOR_RED); Regel 2: Viele Widgets und andere Ressourcen werden hierarchisch geschachtelt. Bei ihrer Erzeugung wird ein Vaterelement im Konstruktor übergeben. Wird jetzt das Vaterelement gelöscht, dann werden automatisch auch alle seine Kindelemente entfernt und korrekt mit dispose() freigegeben. Beispiel, in dem eine Taste als Kind einem Fenster hinzugefügt wird: Shell shell = new Shell(display); Button button = new Button(shell, SWT.PUSH); Es ist in dem Beispiel ausreichend, das Fenster freizugeben. Dieses löscht automatisch alle seine Kindelemente: shell.dispose(); // Gibt auch button wieder frei. Leider ist es in der Praxis nicht ganz einfach, immer daran zu denken, alle Ressourcen auch wieder freizugeben. Daher sollte sich ein Entwickler an einen gewissen Standardaufbau in seiner Anwendung halten. Die folgenden beiden Abschnitt zeigen diesen sowohl für reine SWT-Lösungen als auch für Anwendungen, die JFace verwenden. Die SWT-Variante ist nur eine Notlösung. Wann immer es möglich ist, sollte der JFace-Weg gewählt werden. 3.1.4.1 Ressourcenverwaltung bei reinen SWT-Anwendungen Die Hauptidee besteht darin, das Anlegen sowie die Freigabe von Ressourcen zentral zu bündeln. Beispiel (Quelltext FirstSWTImageResourceApplication): private void if (image1 image1 = if (image2 image2 = } createImages() { == null) new Image(display, "resources/image1.gif"); == null) new Image(display, "resources/image2.gif"); private void disposeImages() { if (image1 != null) image1.dispose(); if (image2 != null) image2.dispose(); } public void run() { createImages(); createGUI(); // Ereignisschleife disposeImages(); display.dispose(); } Dieser Ansatz funktioniert für sehr kleine Programm ganz gut. Leider hat er aber gerade für „richtige“ Anwendungen große Nachteile. So werden unter Umständen Bilder angelegt, die nicht immer benötigt werden. Es ist ja bei der Erzeugung nicht sicher, dass beispielsweise der Dialog, der die Ressourcen verwendet, überhaupt angezeigt wird. Es ist auch bei vielen Klassen schwierig, Bilder und andere Ressourcen zwischen mehreren Dialogen, Menüs, usw. zu teilen. 23 3.1 Grundlagen 3.1.4.2 Ressourcenverwaltung bei JFace-Anwendungen Der JFace-Ansatz versucht, die Nachteile der SWT-Lösung zu umgehen. Dazu werden entweder ein zentrales oder aber mehrere Registry-Objekte für Bilder, Zeichensätze und Farben der Anwendung angelegt. Eine Registry nimmt normalerweise lediglich Beschreibungen der Ressourcen (sogenannte Deskriptoren) auf. Ein solcher Deskriptor beinhaltet z.B. den Namen der Bilddatei. Beim Programmstart werden die Deskriptoren aller benötigten Ressourcen in der Registry abgelegt. Ist sicher, dass bestimmte Ressourcen immer benötigt werden, dann lassen sich statt der Beschreibungen auch direkt die Ressource-Objekte erzeugen und in der Registry ablegen. Wird später im Betrieb ein Bild oder eine andere Ressource benötigt, dann erzeugt die Registry diese beim ersten Zugriff anhand der Beschreibung. Damit gehören die Ressourcen der Registry, so dass diese beim Programmenende für deren Freigabe verantwortlich ist. Und wann wird die Registry gelöscht? Jedes Registry-Objekt ist ein Kindelement eines Display-Objektes. Sobald das Display-Objekt der Anwendung beseitigt wird, entfernt es auch alle seine Kindelement. Beispiel (Quelltext FirstJFaceImageResourceApplication): public class FirstJFaceImageResourceApplication extends ApplicationWindow { private ImageRegistry imageRegistry; public FirstJFaceImageResourceApplication() { super(null); createImages(); } @Override protected Control createContents(Composite parent) { Button button = new Button(parent, SWT.PUSH); button.setImage(imageRegistry.get("fighting-duke")); // ... return parent; } // Registry mit allen möglicherweise benötigten // Deskriptoren der Ressourcen erzeugen. private void createImages() { imageRegistry = new ImageRegistry(Display.getDefault()); imageRegistry.put("fighting-duke", ImageDescriptor.createFromFile(this.getClass(), "/resources/duke-fight.gif")); } public void run() { setBlockOnOpen(true); open(); Display.getDefault().dispose(); } } public static void main(String[] args) { new FirstJFaceImageResourceApplication().run(); } In dem Beispiel oben sollten die Ressourcen-Namen am besten noch durch StringKonstanten definiert werden. 24 3.2 Layout-Management Der JFace-Ansatz ist recht mächtig und vermeidet zuverlässig Freigabefehler. Wenn es möglich ist, sollte er verwendet werden. 3.2 Layout-Management 3.2.1 Motivation Dieser Abschnitt behandelt generell das Thema, wie einzelne GUI-Elemente („Widgets“) in einem Dialog angeordnet werden. Dazu stellt sich natürlich sofort die Frage, warum man nicht einfach die Koordinaten der Elemente sowie deren Größen vorgibt. Das Ergebnis wäre also eine absolute Positionierung. Daraus resultieren eine ganze Anzahl von Problemen, die die folgenden Abschnitte anhand verschiedener Szenarien beschrieben. 3.2.1.1 Unterschiedliche Plattformen Betrachtet man die Ausgabe auf unterschiedlichen Plattformen, dann ist leicht zu sehen, dass nicht immer alle Zeichensätze auf allen Betriebssystemen vorhanden sind und somit die Texte unterschiedliche Ausmaße besitzen. Weiterhin unterscheiden sich die Designs und damit die Größen der Widgets auf den Plattformen. Das folgende Beispiel wurde mit dem Quelltext NullLayout erstellt. Windows 7: Abbildung 3.8: Windows 7 Windows XP: Abbildung 3.10: Windows XP Abbildung 3.9: LinuX, GNOME 2.28.2 Mac OS X (10.6): Abbildung 3.11: MacOS X Es ist gut zu sehen, dass das Layout, das für Windows 7 erstellt wurde, unter LinuX und MacOS X zu zu kleinen Tasten führt. Die Kernaussage lautet also, dass auf die Zeichensatz- und Widgetgrößen kein Verlass ist. Somit können die Größen einzelner Komponenten nicht manuell berechnet werden. 3.2.1.2 Internationalisierung Programme werden in der Regel nicht nur mit einer Spracheinstellung ausgeliefert. Gerade für den internationalen Markt ist es wichtig, dass ein Programm beliebig viele Sprachen unterstützt. Dummerweise sind aber viele Texte in Sprachen wie deutsch und englisch unterschiedlich lang. Somit schwankt auch deren Platzbedarf auf dem Bildschirm. Die folgenden Darstellungen verdeutlichen das Problem (Quelltexte NullLayoutLanguagesEN.java und NullLayoutLanguagesDE.java, basieren auf dem vorherigem Beispiel). 25 3.2 Layout-Management Abbildung 3.12: Englische Texte Abbildung 3.13: Deutsche Texte Die Idee, einen Dialog in seinem Aussehen manuell der jeweiligen Sprache anzupassen, sollte man schnell wieder begraben, da der Aufwand gewaltig wird. Es muss nicht nur für jede neue Sprache der Dialog angepasst werden, sondern es müssen dann auch verschiedene Varianten desselben Dialogs im Laufe der Jahre gepflegt werden. 3.2.1.3 Interaktive Größenänderungen Dialoge sind für den Anwender dann besonders komfortabel zu verwenden, wenn sie in ihrer Größe so verändert werden können, dass sich der Inhalt sinnvoll anpasst. Damit ist gemeint, dass beispielsweise bei manueller Vergößerung eines Dialogs die Texteingabefelder breiter werden, damit mehr Text sichtbar wird. Beschriftungen dagegen sollten ihre Größe behalten. Bei absoluter Platzierung passiert das, was die folgenden Bilder zeigen: Abbildung 3.14: Vergrößerter Dialog Abbildung 3.15: Verkleinerter Dialog Wie zu sehen ist, werden im linken Bild die Tasten nicht größer, obwohl jetzt genügend Platz vorhanden ist. Auch bei anderen Widgets wie Textfeldern ist es durchaus interessant, den Dialog zu vergrößern, um mehr Text sehen zu können, wenn es die Bildschirmgröße zulässt. 3.2.1.4 Look and Feel SWT erlaubt, das Aussehen durch CSS-Dateien zu verändern. Da davon die Zeichensatzgrößen sowie Bilder betroffen sein können, führt auch hier die absolute Platzierung in eine Sackgasse. 3.2.1.5 Zusammenfassung Eine echte plattformunabhängige Programmierung ist mit absoluten Layouts nicht sinnvoll möglich. Unterschiedliche Zeichensätze oder Zeichengrößen verbieten die manuelle Berechnung einer Größe. Weiterhin erfordert die Portierung einer Anwendung in mehrere Sprachen bei absoluten Layouts manuelles Nacharbeiten, was nicht praktikabel ist. Deshalb sollten in SWT und damit auch JFace Dialoge niemals ohne Layoutmanager erstellt werden. Aber auch andere Toolkits wie QT, GTK, Swing, AWT und WPF arbeiten mit Layouts. Die Manager funktionieren zwar überall etwas unterschiedlich, die Idee und grundsätzliche Arbeitsweise aber sind identisch. Die folgenden Abschnitte stellen die Verwendung sogenannter Layoutmanager vor, die die Platzierung von Widgets anhand von Regeln übernehmen. Eine gute Einführung ist auch unter [Layouts] zu finden. 26 3.2 Layout-Management 3.2.2 Größe einer Komponente Wenn jetzt auf die absolute Positionierung verzichtet wird und statt dessen Layoutmanager Einsatz finden, stellt sich die Frage, wie ein Layoutmanager die Größe aller möglichen Komponenten ermitteln kann. Die Antwort ist einfach: Er kann es nicht selbst. Nur eine Komponente bzw. deren Basisklasse Control weiß, wie groß sie gerne werden möchte. Und diese bevorzugte Größe fragt der Layout-Manager an allen Komponenten ab, die er platzieren muss. Da jedes SWT-Widget von Control erbt, steht die Angabe allen Widgets zur Verfügung. Abbildung 3.16: Hierarchie Button Abbildung 3.17: Hierarchie Label Die Basisklasse Control besitzt die Methode computeSize, die die bevorzugte Größe des Widgets berechnet und zurückgibt. Mit Hilfe der Methoden getSize bzw. setSize kann die aktuelle Größe des Widgets ausgelesen bzw. überschrieben werden. Somit besitzt jedes Widget eine bevorzugte und eine aktuelle Größe. Die aktuelle Größe kann von der bevorzugten abweichen, wenn beispielsweise der Dialog manuell in seiner Größe verändert und die Widgets angepasst werden. Dann kann ein Widget größer oder kleiner werden, als es eigentlich sein möchte. 3.2.3 Fill-Layout Das Fill-Layout platziert alle darin enthaltenen Widgets nacheinander bzw. untereinander in einer einheitlichen Größe. Somit werden alle Widgets auf die Maße des größten im Layout vorhandenen Widgets gedehnt. Weiterhin lassen sich die Abstände zwischen den Widgets, die äußeren Ränder um das Layout sowie die Ausrichtung (horizontales oder verktikales Layout) angeben. Widget 1 Widget 2 ... Widget 1 Widget 2 ... Widget n Widget n Abbildung 3.18: Anordnung im Fill-Layout Die Ausrichtung wird – wie leider Vieles in SWT – durch Konstanten im Konstruktor festgelegt: SWT.HORIZONTAL: Die Widgets werden waagerecht angeordnet. SWT.VERTICAL: Die Widgets werden senkrecht angeordnet. Die folgenden Bilder zeigen verschiedene Anwendungen des Layouts. Es ist gut zu erkennen, dass der Layout-Manager trotz unterschiedlicher Plattformen immer sicherstellt, dass die Texte lesbar sind. Die Widgets, in diesem Fall die Tasten, liefern ja auf jeder Plattform unterschiedliche Werte für ihre bevorzugte Größe zurück. 27 3.2 Layout-Management Abbildung 3.19: Horizontale Anordnung im Fill-Layout Manuelle Verkleinerung der Dialoge: Abbildung 3.20: Manuelle Verkleinerung im Fill-Layout Vertikale Ausrichtung ohne manuellen Eingriff: Abbildung 3.21: Vertikale Anordnung im Fill-Layout Das folgende, kleine Beispiel zeigt den Einsatz des Fill-Layouts mit vertikaler Ausrichtung (Quelltext FillLayoutApplication). ... shell.setLayout(new FillLayout(SWT.VERTICAL)); Button button = new Button(shell, SWT.PUSH); button.setText("1. Taste"); button = new Button(shell, SWT.PUSH); button.setText("2. lange Taste"); button = new Button(shell, SWT.PUSH); button.setText("3. ganz lange Taste"); shell.pack(); ... Die Abstände um die Widgets sowie zum Rand hin werden nicht verändert. SWT vergibt dazu Standardwerte. Sollen diese überschrieben werden, dann kann das mit Hilfe öffentlicher Attribute geschehen. Dieses ist ohnehin ein Prinzip der Layouts in SWT und JFace. Die Eigenschaften werden nicht über Getter- und Setter-Methoden gelesen und beschrieben, sondern durch direkten Zugriff auf die entsprechenden Attribute. Einige Attribute der Klasse FillLayout: 28 3.2 Layout-Management marginWidth: linker und rechter Rand um das Element, das dieses Layout besitzt marginHeight: oberer und unterer Rand um das Element, das dieses Layout besitzt spacing: Abstand zwischen den einzelnen Widgets im Layout Das Vaterelement aller Widgets im Beispiel ist das Fenster. Ihm wird der Layout-Manager zugewiesen. Wichtig ist noch der Aufruf shell.pack() am Ende des Beispiels. Mit ihm werden die Layout-Berechnung gestartet und somit die Widgets durch den Manager platziert. 3.2.4 Row-Layout Der RowLayout ähnelt in gewisser Weise dem FillLayout. Auch hier werden die Widgets vertikal oder horizontal angeordnet. Allerdings berücksichtigt der Layout-Manager die bevorzugten Größen der Widgets und verwendet sie. Somit werden in der Standardeinstellung alle Widgets in ihrer bevorzugten Größe platziert. Dieses Verhalten lässt sich allerdings durch Schreiben eines Attributes ändern. So wird manuell erzwungen, dass alle Komponenten – wie beim FillLayout – so groß wie die größte Komponente werden sollen. Eine genaue Betrachtung erfolgt weiter unten bei der Vorstellung der Attribute des Layouts. Weiterhin kann bei diesem Layout festgelegt werden, dass ein manueller „Zeilenumbruch“ erfolgen soll, wenn die Layout-Zeile voll ist und dadurch Widgets verkleinert werden müssten. Widget 1 Widget 2 Widget 3 Widget 1 Widget 2 ... Widget n ... Widget n Abbildung 3.22: Anordnung im Row-Layout Die Ausrichtung wird auch hier durch die bereits bekannten Konstanten im Konstruktor festgelegt: SWT.HORIZONTAL: Die Widgets werden waagerecht angeordnet. SWT.VERTICAL: Die Widgets werden senkrecht angeordnet. Die folgenden Bilder zeigen verschiedene Anwendungen des Layouts. In der Abbildung 3.23 ist zu erkennen, wie alle Widgets in ihrer bevorzugten Größe platziert wurden. Abbildung 3.23: Horizontale Anordnung im Row-Layout Nach einer manuellen Verkleinerung ergibt sich das folgende Bild. Zu erkennen ist, dass der automatische Zeilenumbruch nicht eingeschaltet wurde. 29 3.2 Layout-Management Abbildung 3.24: Manueller Verkleinerung im Row-Layout (ohne Umbruch) Eine manuelle Verkleinerung mit aktivem Zeilenumbruch dagegen würde die Widgets in ihrer bevorzugten Größe belassen: Abbildung 3.25: Manueller Verkleinerung im Row-Layout (mit Umbruch) Das Beispiel mit vertikaler Ausrichtung ist schließlich nicht mehr sonderlich spannend: Abbildung 3.26: Vertikale Anordnung im Row-Layout Der Programmcode für das Beispiel, mit dessen Hilfe die Screenshots hier angefertigt wurden, ist nicht komplex (RowLayoutApplication): ... RowLayout layout = new RowLayout(SWT.HORIZONTAL); layout.wrap = false; // kein "Zeilenumbruch" shell.setLayout(layout); Button button = new Button(shell, SWT.PUSH); button.setText("1. Taste"); button = new Button(shell, SWT.PUSH); button.setText("2. lange Taste"); button = new Button(shell, SWT.PUSH); button.setText("3. ganz lange Taste"); shell.pack(); ... Bei leistungsfähigeren und damit auch komplexeren Layout-Managern ist relativ Programmcode erforderlich, um alle Attribute einzustellen. Daher wurden mit JFace FabrikKlassen für alle Layout-Manager eingeführt, die die Erzeugung und das Einstellen des Layouts vereinfachen. Für das im Beispiel eingesetzte RowLayout handelt es sich um die Klasse RowLayoutFactory. Damit wird der Programmcode oben umgeformt: ... Button button = new Button(shell, SWT.PUSH); button.setText("1. Taste"); button = new Button(shell, SWT.PUSH); button.setText("2. lange Taste"); button = new Button(shell, SWT.PUSH); button.setText("3. ganz lange Taste"); 30 3.2 Layout-Management RowLayoutFactory.swtDefaults().type(SWT.HORIZONTAL) .wrap(false).applyTo(shell); pack() wird nicht mehr manuell aufgeufen. Das geschieht automatisch nach Einstellen des Layouts. Daher muss die Layout-Erzeugung jetzt unbedingt nach Einfügen aller Widgets erfolgen. Eine genaue Erklärung der Fabrikklasse ist in der API-Dokumentation zu JFace zu finden. Wie beim FillLayout gibt es auch beim RowLayout einige Attribute, die das Verhalten steuern. In JFace erlaubt die Klasse RowLayoutFactory hier das einfachere Setzen dieser Attribute innerhalb der Layout-Angabe. Attribute: center: Ist das Attribut true, dann werden die Widgets bei unterschiedlichen Breiten bzw. Höhen zueinander zentriert Ansonsten werden sie linksbündig bzw. oben ausgerichtet. justify: Ist das Attribut true, dann werden die Widgets wie im Blocksatz angeordnet. Ist es false, dann werden die Widgets am Ursprung platziert. marginTop: oberer Rand über dem Layout marginLeft: linker Rand neben dem Layout marginBottom: unterer Rand unter dem Layout marginRight: rechter Rand neben dem Layout pack: Ist das Attribut true, dann verwendet der Layout-Manager die bevorzugten Größen der Widgets. Ansonsten erzwingt er, dass alle Widgets die Maße der größten Komponente annehmen. spacing: Abstand zwischen den Widgets wrap: Ist das Attribut true, dann wird der automatische Zeilenumbruch aktiviert. Mit diesen Attributen lassen sich bereits viele Eigenschaften des Layouts verändern. Gerade bei komplexeren Layout-Manager soll der Entwickler aber noch weitergehende Einflussmöglichkeiten erhalten. Dazu kann jedem Widget eine Layout-Eigenschaft zugeordnet werden. Diese Eigenschaft ist ein Objekt, das zusätzliche Layout-Bedingungen aufnimmt, die der Layout-Manager berücksichtigen muss. Es existiert für nahezu jeden Layout-Manager eine eigene Klasse mit Layout-Eigenschaften, weil die Eigenschaften eng an den Manager gekoppelt sind. Die Eigenschaften werden durch den Aufruf der Methode setLayoutData (nicht setData!!) am Widget abgelegt. Am Beispiel des RowLayouts lässt sich gut zeigen, was die zusätzlichen Layout-Eigenschaften bewirken können. Möchte der Entwickler ein Widget etwas größer machen, als es die bevorzugte Größe vorsieht, dann ist das bisher praktisch kaum möglich. Wird jetzt aber dem Widget eine Layout-Eigenschaft mit einer fest vorgegeben Größe übergeben, dann verwendet der Layout-Manager diese. Im RowLayout heißt die Eigenschaftsklasse RowData. Dieser Namensaufbau ist übrigens bei allen Layout-Managern einheitlich: Statt Layout erhält die Eigenschaftsklasse die Namensendung Data. Das folgende Beispiel erweitert das bisherige, indem die erste Taste zwangsweise eine feste Breite vom 200 Pixeln erhält, aber ihre bevorzugte Höhe beibehält (Verwendung von SWT.DEFAULT für den Standardwert). 31 3.2 Layout-Management ... Button button = new Button(shell, SWT.PUSH); button.setText("1. Taste"); RowData layoutData = new RowData(200, SWT.DEFAULT); button.setLayoutData(layoutData); ... Abbildung 3.27: Row-Layout mit veränderter Widget-Breite Wie beim Layout werden auch bei den Eigenschaften die Werte direkt und nicht über Setter-Methoden manipuliert. Attribute der Klasse RowData: width: Manuell vergebene Breite des Widgets. Die Angabe von SWT.DEFAULT bewirkt, dass die errechnete bevorzugte Breite des Widgets verwendet wird. height: Manuell vergebene Höhe des Widgets. Die Angabe von SWT.DEFAULT bewirkt, dass die errechnete bevorzugte Höhe des Widgets verwendet wird. exclude: Die Ausgabe des Widgets soll im Layout unterdrückt werden. Es ist somit unsichtbar und belegt auch keinen Platz. Manuelle Größenabgaben sollten möglichst sparsam eingesetzt werden, weil ansonsten spätestens im Zuge der Internationalisierung der Anwendung wieder das Problem auftritt, dass unterschiedlich lange Texte eventuell nicht mehr richtig passen. Für Widgets, die keinen eigenen Text haben, ist eine manuelle Größenvergabe dagegen manchmal sinnvoll. So können mehrzeilige Texteingabefelder einen vordefinierten Platz belegen. Dieser Platz kann nicht vom Widget selbst ermittelt werden, weil es nicht wissen kann, wieviel Text gleichzeitig sichtbar sein soll. 3.2.5 Grid-Layout Nach den recht eingeschränkten FillLayout und RowLayout wird mit GridLayout jetzt ein Layout-Manager vorgestellt, der dem Entwickler deutlich mehr Möglichkeiten in Bezug auf die Platzierung sowie die Größen der Widgets einräumt. Die Grundidee besteht darin, alle Widgets in einem tabellarischen Raster anzuordnen. In jeder einzelnen Zelle des Rasters kann sich immer nur ein Widget befinden. Dabei dürfen sich allerdings einzelne Widgets über mehrere Zeilen und Spalten erstrecken. Weiterhin kann der Layout-Manager durch einstellbare Regeln auch Widgets über ihre bevorzugte Größe hinaus vergrößern und eine einheitliche Spaltenbreite erzwingen. 32 3.2 Layout-Management Widget 1 Widget 2 Widget 7 Widgets dürfen sich über mehrere Zeilen und/oder Spalten erstrecken. Widget 3 Zeilen dürfen unterschiedliche Höhen, Spalte unterschiedliche Breiten besitzen. Widget 8 Widget 6 Widget 5 Widget 4 Abbildung 3.28: Anordnung im Grid-Layout Bevor ein Beispiel betrachtet wird, sollen zunächst die Attribute und somit die Eigenschaften des Layout-Managers vorgestellt werden. horizontalSpacing: Horizontaler Abstand zwischen den Zellen. Damit ist der Abstand zwischen den einzelnen Spalten gemeint. verticalSpacing: Vertikaler Abstand zwischen den Zellen. Dieser beschreibt die Distanz zwischen den Zeilen. makeColumnsEqualWidth: Wenn dieses Attribut true ist, dann erzwingt das Layout eine einheitliche Breite aller Spalten. Die Breite des breitesten Widgets im kompletten Layout bestimmt die Breite aller Spalten. Ist der Wert dagegen false, dann bestimmt in jeder einzelnen Spalte die Breite des darin enthaltenen breitesten Widgets die Gesamtbreite der Spalte. Damit ist noch nicht gesagt, wie sich die schmaleren Widgets verhalten. Normalerweise belegen diese nur einen Teil der ihnen zur Verfügung stehenden Zelle, indem sie an deren jeweiligen Ursprung (z.B. links oben) ausgerichtet werden. Dieses ist anhand der stilisierten Abbildung 3.28 ersichtlich. Es kann aber über die Layout-Eigenschaften einzelner Widgets eingestellt werden, dass sich die Widgets über ihre bevorzugte Größe hinaus vergrößern und die Zelle komplett ausfüllen. Dieses Verhalten wird weiter unten näher betrachtet. marginTop: oberer Rand über dem Layout marginLeft: linker Rand neben dem Layout marginBottom: unterer Rand unter dem Layout marginRight: rechter Rand neben dem Layout marginHeight: Oberer und unterer Rand um das Layout. Diese Eigenschaft kann durch das Setzen von marginTop und marginBottom überschrieben werden. marginWidth: Linker und rechter Rand neben dem Layout. Diese Eigenschaft kann durch das Setzen von marginLeft und marginRight überschrieben werden. numColumns: Anzahl der Spalten im Layout. Diese Angabe ist erforderlich, weil die Widgets ohne die Angabe einer Zelle platziert werden. Statt dessen wird die Reihenfolge, in der die Widgets zu ihrem Vaterelement hinzugefügt werden, verwendet, um daraus die Position im Layout zu ermittlen: Die Widgets werden, beginnend mit dem Ursprung, „von links nach rechts“ und „von oben nach unten“ platziert. Immer wenn die voreingestellte Spaltenzahl erreicht ist, wird eine neue Zeile angefangen. 33 3.2 Layout-Management Wie für das RowLayout existiert ebenso für das GridLayout eine Fabrikklasse zum leichteren Erzeugen des Layouts(GridLayoutFactory). Die Mächtigkeit des LayoutManagers ergibt sich aus den vielen Layout-Eigenschaften, die den Widgets übergeben werden können. Die dafür zuständige Klasse heißt GridData. Dessen wichtigste Attribute sind: exclude: Die Anzeige des Widgets wird im Layout unterdrückt, so dass dessen Zelle leer erscheint. grabExcessHorizontalSpace: Dieses Attribut steuert das Verhalten des Widgets, wenn das Vaterelement (z.B. das Fenster) schmaler oder breiter wird. Ist das Attribut true, dann schrumpft oder wächst das Widget in der Breite, wenn sich die Breite des Vaterelementes ändert. So kann z.B. ein Textfeld dem Anwender mehr Zeichen gleichzeitig darstellen, wenn der Dialog größer wird. Ein Bezeichner („Label“) dagegen würde sich nicht verändern, weil sein kompletter Text ohnehin in der Regel sichtbar ist. grabExcessVerticalSpace: Dieses Attribut steuert das Verhalten des Widgets, wenn das Vaterelement flacher oder höher wird. Ist das Attribut true, so schrumpft oder wächst das Widget in der Höhe, wenn sich die Höhe des Vaterelementes ändert. widthHint: Wird dieses Attribut verwendet, dann überschreibt es die bevorzugte Breite eines Widgets. So lassen sich manuell Größen vorgeben. Wie schon beim Row-Layout beschrieben, sollte das manuelle Setzen einer Größe nur sparsam und gezielt eingesetzt werden, weil es ansonsten spätestens bei der Internationalisierung Probleme geben kann. heightHint: Dieses Attribut überschreibt die bevorzugte Höhe eines Widgets. horizontalAlignment: Mit diesem Attribut wird gesteuert, wie sich ein Widget in einer Zelle verhalten soll: SWT.BEGINNG: Ist das Widget schmaler als die Zelle, dann wird es an den Anfang der Zelle platziert. SWT.CENTER: Ist das Widget schmaler als die Zelle, dann wird es innerhalb der Zelle zentriert. SWT.END: Ist das Widget schmaler als die Zelle, dann wird es an das Ende der Zelle platziert. SWT.FILL: Ist die bevorzugte Breite des Widgets kleiner als die Zellenbreite, dann wird das Widget auf die Breite der Zelle gestreckt. Dieser Wert zusammen mit dem Setzen von grabExcessHorizontalSpace bewirkt, dass ein Widget immer die komplette Zellenbreite belegt, auch wenn das Vaterelement seine Größe verändert. verticalAlignment: Mit diesem Attribut wird gesteuert, wie sich ein Widget in einer Zelle verhalten soll, wenn seine bevorzugte Höhe kleiner als die Zellenhöhe ist. Die Konstanten sind dieselben wie im vorherigen Auszählungspunkt bei der horizontalen Platzierung mit horizontalAlignment. 34 3.2 Layout-Management horizontalIndent: Belegt ein Widget nicht die komplette Breite einer Zelle, dann kann mit diesem Einzug eingestellt werden, um wieviele Pixel es vom Platzierungsursprung entfernt angeordnet werden soll. Bei einer Ausrichtung am Anfang der Zelle würde es um die angegebene Pixelzahl vom Anfang der Zelle entfernt platziert werden. Bei einer Ausrichtung am Ende der Zelle dagegen würde der Einzug den Abstand vom Ende der Zelle definieren. verticalIndent: Wie horizontalIndent, nur auf Höhe des Widgets im Layout bezogen. horizontalSpan: Anzahl Spalten, die das Widget im Layout belegen soll verticalSpan: Anzahl Zeilen, die das Widget im Layout belegen soll minimumWidth: Manche Widgets sollen im Layout bei einer Größenänderung des Dialogs eine gewisse Mindestbreite nicht unterschreiten, damit wichtige Informationen immer sichtbar bleiben. Diese Mindestbreite kann hier festgelegt werden. minimumHeight: Manche Widgets sollen im Layout bei einer Größenänderung des Dialogs eine gewisse Mindesthöhe nicht unterschreiten, damit wichtige Informationen immer sichtbar bleiben. Diese Mindesthöhe kann hier festgelegt werden. Aus der langen Aufstellung ist ersichtlich, dass im schlimmsten Fall ziemlich viele Attribute eingestellt werden müssen. Glücklicherweise können die häufigsten Parameter direkt über die Konstruktoren der Klasse GridData übergeben werden. Näheres dazu ist in der API-Dokumentation zu finden. Zusätzlich existiert auch für die Klasse GridData mit GridDataFactory eine Fabrikklasse zum leichteren Befüllen. Das folgende Beispiel zeigt die Layout-Eigenschaften einer Taste, die sich über die volle Breite und Höhe einer Zelle erstrecken soll, obwohl sie eigentlich kleiner sein möchte. Button button = new Button(shell, SWT.PUSH); button.setText("Ok"); data = new GridData(SWT.FILL, SWT.FILL, true, true); button.setLayoutData(data); Die vier Parameter des Konstruktors der Klasse GridData bedeuten: 1. SWT.FILL: Die Taste belegt die komplette Breite der Zelle. 2. SWT.FILL: Die Taste belegt die komplette Höhe der Zelle. 3. true: Die Taste wächst und schrumpft in der Breite, wenn sich die Zellenbreite ändert. 4. true: Die Taste wächst und schrumpft in der Höhe, wenn sich die Zellenhöhe ändert. Anhand des folgenden, relativ sinnlosen Dialogs soll gezeigt werden, wie mit Hilfe des GridLayouts Widgets platziert werden. Abbildung 3.29: Beispieldialog für das Grid-Layout 35 3.2 Layout-Management Zunächst wird ein Raster über den Dialog gelegt. Immer dort, wo Widgets aneinander stoßen, entstehen neue Spalten bzw. Zeilen: Abbildung 3.30: Rasterhilfslinien auf dem Dialog Jetzt kann das Layout-Objekt für drei Spalten erzeugt werden. Dabei wird keine identische Spaltenbreite erzwungen. Der folgende Quelltext (GridLayoutApplication) verzichtet auf den Einsatz der Fabrikklasse: ... GridLayout layout = new GridLayout(3, false); shell.setLayout(layout); Im nächsten Schritt wird das obere Label als Überschrift in den Dialog eingefügt. Es soll sich über alle drei Spalten erstrecken, weil der Text in anderen Sprachen ja durchaus länger werden kann. Würde das Label lediglich in Spalte 1 stehen, dann würde die Spalte durch einen längeren Labeltext breiter werden. Das ist hier aber nicht gewünscht. Abbildung 3.31: Platzierung des Labels im Grid-Layout Label label = new Label(shell, SWT.BEGINNING); label.setText("Titel"); data = new GridData(SWT.FILL, // Zelle horizontal füllen SWT.DEFAULT, // bevorzugte Höhe true, // horizontal wachsen false, // vertikal nicht wachsen 3, // 3 Spalten 1); // eine Zeile label.setLayoutData(data); Das mehrzeilige, scrollbare Texteingabefeld ist sicherlich das interessanteste Widget im Beispiel. Es soll sowohl horizontal als auch vertikal die komplette Zelle belegen und mitwachsen. Weiterhin soll dem Widget eine bevorzugte Breite übergeben werden, weil das Widget selbst eine viel zu kleine annimmt. 36 3.2 Layout-Management Abbildung 3.32: Platzierung des Textfeldes im Grid-Layout Text text = new Text(shell, SWT.MULTI | SWT.WRAP | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL); data = new GridData(SWT.FILL, // Zelle horizontal füllen SWT.FILL, // Zelle vertikal füllen true, // horizontal wachsen true, // vertikal wachsen 1, // eine Spalte 2); // zwei Zeilen data.widthHint = 200; // bevorzugte Breite 200 Pixel text.setLayoutData(data); Abschließend soll noch eine der beiden Tasten betrachtet werden. Zu beachten ist, dass beide Tasten unabhängig von ihrem Inhalt (ihrer Beschriftung) immer gleich breit sein sollen. Normalerweise würde der Text die Breite bestimmen. Weiterhin ist im Beispiel verlangt, dass die Tasten auch horizontal wachsen. Das ist in der Praxis natürlich recht unsinnig. Abbildung 3.33: Platzierung einer Taste im Grid-Layout Button button = new Button(shell, SWT.PUSH); button.setText("Ok"); data = new GridData(SWT.FILL, // Zelle horizontal füllen SWT.FILL, // Zelle vertikal füllen false, // horizontal nicht wachsen true); // vertikal wachsen button.setLayoutData(data); Soll die Taste dagegen ihre bevorzugte Breite behalten, dann dürfte sie nicht auf die Zellenbreite gestreckt werden: Button button = new Button(shell, SWT.PUSH); button.setText("Ok"); data = new GridData(SWT.DEFAULT, // BEVORZUGTE BREITE! SWT.FILL, // Zelle vertikal füllen false, // horizontal nicht wachsen true); // vertikal wachsen button.setLayoutData(data); 37 3.2 Layout-Management Das Ergebnis zeigt Abbildung 3.34. Abbildung 3.34: Platzierung einer Taste im Grid-Layout ohne Streckung 3.2.6 Geschachtelte Layout Die bisher vorgestelten Layout-Manager waren für sich alleine gesehen zwar schon recht hilfreich, scheitern aber schnell bei komplexen Layouts. Das liegt daran, dass immer jedem Fenster genau ein Layout-Manager zugeordnet war, der das Aussehen des Dialoginhalts selbst festlegen musste. Dieser Ansatz ist allerdings für komplizierte Layouts nicht flexibel genug, bzw. würde extrem mächtige und damit auch umständlich zu verwendende Layout-Manager benötigen. Eine einfachere Lösung besteht darin, Layouts ineinander zu schachteln. Dazu existieren Widgets, die selbst wiederum als Container für andere Widgets dienen. Diesen Containern kann ein eigener Layout-Manager zugewisen werden. Somit ergibt sich eine hierarchische Schachtelung von Layouts. Im folgenden Bild wird ein Composite-Widget als Container eingesetzt. Das Widget hat bei dieser Verwendung normalerweise keine eigene Darstellung. Es dient hauptsächlich dazu, andere Widgets aufzunehmen und diese mit Hilfe eines eigenen Layouts darzustellen. Fenster mit GridLayout Widget 1 Widget 2 Composite-Widget mit FillLayout Abbildung 3.35: Verschachtelung von Containern mit jeweils eigenen Layouts Das folgende Beispiel zeigt einen Dialog mit zwei nebeneinander angeordneten, identisch breiten Tasten (NestedLayoutsApplication). Für solche Dialoge mit Standardtasten werden später spezielle Fensterklassen vorgestellt, so dass es normalerweise unnötig ist, die Tasten wie im Beispiel selbst zu platzieren. NestedLayoutsApplication Composite-Widget mit FillLayout (der Rahmen zeigt die Ausmaße des Composite-Objektes) Abbildung 3.36: Beispiel für ineinander geschachtelte Layouts 38 3.2 Layout-Management Der Rahmen um das Composite-Objekt mit den beiden Tasten soll nur die Ausmaße des Containers verdeutlichen. // Der Container für die Tasten erhält ein FillLayout. Composite buttonPanel = new Composite(shell, SWT.NONE); buttonPanel.setLayout(new FillLayout()); // Der Container streckt sich horizontal über // zwei Spalten. Er wird im übergeordneten GridLayout // rechtsbündig (SWT.END) in seine Zelle gesetzt. // Er soll nicht mit der Zelle mitwachsen. data = new GridData(SWT.END, SWT.DEFAULT, false, false, 2, 1); buttonPanel.setLayoutData(data); // Die Tasten werden zum Composite-Objekt hinzugefügt. new Button(buttonPanel, SWT.PUSH).setText("Ok"); new Button(buttonPanel, SWT.PUSH).setText("Cancel"); Die Screenshots zeigen das Verhalten des Dialogs bei Größenänderungen unter Windows und Mac OS X. Abbildung 3.37: Größenänderungen bei ineinander geschachtelten Layouts Man erkennt, dass die Reihenfolge der Tasten unter Mac OS X nicht den Vorgaben Apples entspricht. Dieses Problem lösen die vordefinierten Dialogklassen, die später betrachtet werden. 3.2.7 Form-Layout Das FormLayout ist das flexibelste und komplexeste Standardlayout im SWT. Hier werden die Widgets relativ zueinander platziert und an ihren Kanten ausgerichtet. 3.2.7.1 Grundidee Die Abbildung 3.38 zeigt, wie die Widget-Ränder entweder an den Rändern des VaterContainers (hier das Fenster) oder an den Rändern anderer Widgets ausgerichtet wer39 3.2 Layout-Management den. Zusätzlich lassen sich auch relative Maße angeben, so dass z.B. ein Widgetrand immer an 50 % der Fensterbreite ausgerichtet sein soll. Ändert sich die Fensterbreite, dann „wandert“ das Widget bzw. dessen Rand mit. Widget 1 Widget 2 Widget 3 Widget 1 wird mit seinem linken und rechten Rand am Fenster ausgerichtet. Widget 3 wird mit seinem rechten Rand immer am Rand des Fenster ausgerichtet. Widget 3 wird mit seinem unteren Rand am oberen Rand von Widget 5 ausgerichtet. Widget 4 Widget 5 Widget 5 wird mit seinem unteren Rand immer am Fenster ausgerichtet. Abbildung 3.38: Anordnung im Form-Layout Das FormLayout ist sehr mächtig, dummerweise aber auch etwas unhandlich in seiner Anwendung. Zunächst werden die wichtigsten Eigenschaften des Layouts vorgestellt. spacing: Standardabstand zwischen den einzelnen Widgets in Pixeln marginTop: Größe des Randes oberhalb des Layouts in Pixeln marginLeft: Größe des Randes links des Layouts in Pixeln marginBottom: Größe des Randes unterhalb des Layouts in Pixeln marginRight: Größe des Randes rechts des Layouts in Pixeln marginHeight: Größe des Randes oberhalb und unterhalb des Layouts in Pixeln marginWidth: Größe des Randes links und rechts des Layouts in Pixeln Auch beim FormLayout werden die eigentlichen Platzierungsangaben für einzelne Widgets über Layout-Eigenschaften der Widgets eingestellt. In diesem Fall ist es die Klasse FormData. Sie nimmt für die vier Seiten des Widgets die Platzierungsbeziehungen auf. Jede Beziehung wird wiederum durch ein Objekt der Klasse FormAttachment definiert. Attribute der Klasse FormData: top: Ausrichtung der oberen Kante des Widgets left: Ausrichtung der linken Kante des Widgets bottom: Ausrichtung der unteren Kante des Widgets right: Ausrichtung der rechten Kante des Widgets width: Wenn die bevorzugte Breite des Widgets nicht zum Layout passt, dann kann hier manuell eine Breite vergeben werden. Das sollte wie bei den anderen Layouts in der Regel nur dann verwendet werden, wenn das Widget nicht in der Lage ist, eine sinnvolle Breite selbst zu berechnen (z.B. mehrzeilige Texteingabefelder). height: Wenn die bevorzugte Höhe des Widgets nicht zum Layout passt, dann kann hier eine Höhe vorgegeben werden. 40 3.2 Layout-Management Schließlich fehlt noch die Klasse FormAttachment, die eine Platzierungsregel für eine Kante enthält. Wichtig ist hierbei, dass es prinzpiell zwei Möglichkeiten der Anordnung einer Kante gibt: Sie wird an einer Kante eines Zielwidgets im selben Layout oder an einer Kante des Vater-Containers (z.B. des Fensters) ausgerichtet. alignment: Kante des Widgets, an dem sich dieses Widget ausrichten soll. Die möglichen Werte für die eigene obere oder untere Kante sind: SWT.TOP: Die Platzierungsangabe bezieht sich auf den oberen Rand des Zielwidgets. SWT.CENTER: Die Platzierungsangabe bezieht sich auf die Mitte des Zielwidgets. SWT.BOTTOM: Die Platzierungsangabe bezieht sich auf den unteren Rand des Zielwidgets. Die möglichen Werte für die eigene linke oder rechte Kante sind: SWT.LEFT: Die Platzierungsangabe bezieht sich auf den linken Rand des Zielwidgets. SWT.CENTER: Die Platzierungsangabe bezieht sich auf die Mitte des Zielwidgets. SWT.RIGHT: Die Platzierungsangabe bezieht sich auf den rechten Rand des Zielwidgets. control: Dieses Attribut nimmt die Referenz des Widgets auf, zu dem das eigene Widget relativ positioniert werden soll. Hat control den Wert null, dann soll die eigene Widgetseite relativ zum Vater-Container platziert werden. offset: Absoluter Abstand in Pixeln von der Kante des Zielwidgets numerator, denominator: Ein Beispiel verdeutlicht die Verwendung dieser beiden zunächst merkwürdig erscheinenden Attribute: Ein Widget soll mit seinem linken Rand immer in der Mitte des Dialogs erscheinen. Somit muss sein linker Rand am linken Rand des Dialogs ausgerichtet werden. Die Angabe „in der Mitte“ wird durch den Prozentwert der Breite des Dialogs angegeben, hier also 50 % der Breite. Die beiden Attribute stellen jetzt Zähler (numerator) und Nenner (denominator, Standardwert ist 100) des Bruchs, mit dem der Prozentwert abgebildet wird, dar. Somit ergibt sich die aktuelle Position durch folgende Berechnung: numerator pos = denominator ∗ size + of f set. size ist hierbei in dem Beispiel die automatisch ermittelte Breite des Dialogs. Damit das Ganze nicht zu abstrakt bleibt, zeigen einige Beispiele in den folgenden Abschnitten die Verwendung der Klassen auf. 3.2.7.2 Beispiel für die relative Anordnung zum Vaterobjekt Hier soll sich Widget W1 links und rechts am Fenster ausrichten. Sein linker Rand bleibt also immer am linken Rand des Fenster, sein rechter am rechten Rand des Fenster. Damit belegt W1 die komplette Fensterbreite. 41 3.2 Layout-Management FormData data = new FormData(); // Der rechte Rand sitzt bei 100% Breite // des Fensters (100/100). Der Offset // ist 0. data.right = new FormAttachment(100); W1 // Der linker Rand sitzt bei 0% Breite // des Fensters (0/100). Der Offset // ist 0. data.left = new FormAttachment(0); w1.setLayoutData(data); Abbildung 3.39: Beispiel 1 für die Anordnung im Form-Layout 3.2.7.3 Beispiel für die relative Platzierung zu einem anderen Widget Widget W1 wird soll seinen linken Rand immer relativ zum rechten Rand von W2 platzieren. Der Abstand zwischen beiden beträgt 0 Pixel, was nicht sehr praxisnah ist. FormData data = new FormData(); // Der rechte Rand schließt direkt an // den linken Rand von W1 an. Der Abstand // wird durch das Layout vorgegeben. data.right = new FormAttachment(w2); W1 W2 // Der untere Rand sitzt bei 100% Höhe // des Fensters (100/100). Der Offset // ist 0. data.bottom = new FormAttachment(100); w1.setLayoutData(data); Abbildung 3.40: Beispiel 2 für die Anordnung im Form-Layout 3.2.7.4 Beispiel für die relative Anordnung zum Vaterobjekt und zu einem anderen Widget W1 und W2 belegen jeweils 50 % der Breite des Fensters und haben in der Mitte einen zusätzlichen Abstand von 4 Pixeln. 42 3.2 Layout-Management FormData w1Data = new FormData(); FormData w2Data = new FormData(); W1 W2 // Der rechte Rand von W2 liegt am Fenster. w2Data.right = new FormAttachment(100); // Der linke Rand von W2 liegt mit 4 Pixeln // Abstand rechts von W1. w2Data.left = new FormAttachment(w1, 4); // Der rechte Rand von W1 liegt bei 50% der // Breite (minus 2 Pixel). w1Data.right = new FormAttachment(50, -2); // Der linke Rand von W1 liegt am Fenster. w1Data.left = new FormAttachment(0); w1.setLayoutData(w1Data); Abbildung 3.41: Beispiel 3 für die Anordnung im Form-Layout 3.2.7.5 Auszurichtende Zielseiten Es fällt auf, dass nirgendwo die Kanten, auf die sich die Positionierungen beziehen, angegeben sind. alignment wird also in der Klasse FormAttachment niemals beschrieben. Das ist in diesen Beispielen auch nicht notwendig, weil das Form-Layout die Angabe selbst ermitteln kann. Beispiel: W1 W2 Der linke Rand von W2 wird an dem rechten Rand von W1 ausgerichtet. Abbildung 3.42: Automatische Randerkennung im Form-Layout Der rechte Rand von W1 hat den linken von W2 als Nachbarn. Das wird automatisch erkannt und muss daher nicht angegeben werden. Lediglich in Szenarien, in denen die Kante des Zielobjektes nicht eindeutig erkennbar ist, muss sie angegeben werden. Das ist beispielsweise dann der Fall, wenn ein Widget über einem anderen steht und seine linke oder rechte Kante auf die linke oder rechte Kante des Ziels ausgerichtet werden muss. Dann kann das Form-Layout nicht entscheiden, welche Kante des Ziels gemeint ist. Beispiel: W1 W2 Soll der linke Rand von W2 an dem rechten oder linken Rand von W1 ausgerichtet werden? Abbildung 3.43: Keine automatische Randerkennung im Form-Layout 43 3.2 Layout-Management 3.2.7.6 Durchgängiges Beispiel Ein vollständiges Beispiel (FormLayoutApplication) schließt die Betrachtung des Form-Layouts ab. Viele der vorher vorgestellten Attribute lassen sich vergleichsweise bequem über die Kontruktoren setzen. Als Basis dient dieser Screenshot: Abbildung 3.44: Screenshot des Beispiels Die Rahmen um das Widget für die Titelzeile sowie das Bild dienen nur dazu, deren Ausmaße zu erkennen. Auch für dieses Beispiel gilt, dass es bereits vordefinierte Dialoge mit den Standardtasten gibt und dass diese der manuellen Programmierung vorzuziehen sind. Im Layout erhalten alle Widgets einen Abstand untereinander von 4 Pixeln. Das ist auch der Abstand zum Fensterrand. FormLayout layout = new FormLayout(); layout.marginHeight = 4; // oberer und unterer Rand layout.marginWidth = 4; // linker und rechter Rand layout.spacing = 4; // Abstände zwischen den Komponenten shell.setLayout(layout); Im nächsten Schritt wird die Titelzeile eingefügt. Sie ist ein Label, das sich immer über die komplette Fensterbreite erstrecken soll und oben am Fenster platziert ist. Der untere Rand wird nicht festgelegt. Seine Position ergibt sich aus der des oberen zuzüglich der bevorzugten Höhe. Abbildung 3.45: Einfügen des Titels Label label = new Label(shell, SWT.BEGINNING | SWT.BORDER); label.setText("Titel"); FormData data = new FormData(); // linker Rand bei 0% Fensterbreite data.left = new FormAttachment(0); // rechter Rand bei 100% Fensterbreite data.right = new FormAttachment(100); label.setLayoutData(data); 44 3.2 Layout-Management Jetzt wird die Taste für den Abbruch („Cancel“) in den Dialog eingefügt. Sie sitzt immer am unteren rechten Rand des Dialogs. Die Positonen ihres oberen und linken Randes ergeben sich dann aus der bevorzugten Größe. Abbildung 3.46: Einfügen der Abbruch-Taste Button cancelButton = new Button(shell, SWT.PUSH); cancelButton.setText("Cancel"); data = new FormData(); // rechter Rand bei 100% Fensterbreite data.right = new FormAttachment(100); // unterer Rand bei 100% Fensterhöhe data.bottom = new FormAttachment(100); cancelButton.setLayoutData(data); Jetzt kann die Ok-Taste links neben die Abbruch-Taste gesetzt werden. Ihr rechter Rand positioniert sich relativ zum linken Rand der Abbruch-Taste. Die Abstände zwischen den Tasten ergeben sich aus dem spacing-Wert des Layouts. Sollen beide Tasten eine identische Breite aufweisen, dann müsste in das Form-Layout ein Fill-Layout eingefügt werden, das beide Tasten aufnimmt. Abbildung 3.47: Einfügen der Ok-Taste Button okButton = new Button(shell, SWT.PUSH); okButton.setText("Ok"); data = new FormData(); // rechten Rand an der Cancel-Taste ausrichten data.right = new FormAttachment(cancelButton); // unterer Rand bei 100% Fensterhöhe data.bottom = new FormAttachment(100); okButton.setLayoutData(data); Das Bild am linken Rand soll in der Höhe, aber nicht in der Breite wachsen. Es wird daher mit seinem linken Rand am Fenster, mit seinem oberen am Titel und mit seinem unteren an der Abbruch-Taste befestigt. Nur durch die Anbindung an die Taste wächst das Bild in 45 3.2 Layout-Management der Höhe mit, weil die Taste ja bei einer Dialogvergrößerung „nach unten wandert“ und ihre Größe beibehält. Abbildung 3.48: Einfügen des Bildes Label dukeLabel = new Label(shell, SWT.CENTER | SWT.BORDER); dukeLabel.setImage(imageRegistry.get("duke")); data = new FormData(); // linker Rand bei 0% Fensterbreite data.left = new FormAttachment(0); // oberen Rand am Titel-Label ausrichten data.top = new FormAttachment(label); // unteren Rand an der Cancel-Taste ausrichten data.bottom = new FormAttachment(cancelButton); dukeLabel.setLayoutData(data); Abschließend fehlt noch das mehrzeilige Texteingabefeld mit seinen Scrollbalken. Es soll sowohl in der Höhe als auch in der Breite mitwachsen. Dazu werden sein linker Rand am Bild, sein oberer Rand am Titel, sein rechter Rand am Fensterrahmen und sein unterer Rand an der Abbruch-Taste ausgerichtet. Durch die beiden zuletzt genannten Bedingungen kann der Text bei Größenänderung des Dialogs mitwachsen oder auch schrumpfen. Abbildung 3.49: Einfügen des Textfeldes Text text = new Text(shell, SWT.MULTI | SWT.WRAP | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL); data = new FormData(); // linken Rand am Duke-Label (Bild) ausrichten data.left = new FormAttachment(dukeLabel); // oberen Rand am Titel-Label ausrichten data.top = new FormAttachment(label); // rechter Rand ist bei 100% Fensterbreite data.right = new FormAttachment(100); // unteren Rand an der Cancel-Taste ausrichten data.bottom = new FormAttachment(cancelButton); text.setLayoutData(data); 46 3.2 Layout-Management 3.2.8 Form-Layout (JGoodies) Das Form-Layout erlaubt zwar eine sehr flexible Gestaltung von Dialogen, hat aber die Mängel, dass einerseits eine große Menge Code geschrieben werden muss und andererseits das Vorgehen häufig als wenig intuitiv empfunden wird. Der zweite Punkt rührt daher, dass viele Entwickler ein Raster auf einen Dialogentwurf legen und so in Zeilen und Spalten denken. Genau das wird beim Form-Layout aber nicht direkt sichtbar. Das FormLayout von JGoodies schafft hier auf eine recht komfortable Art und Weise Abhilfe. An dieser Stelle soll keine komplette Einführung in das FormLayout erfolgen. Unter der URL http://ffxml.net/swtforms.html lässt sich die SWT-Portierung der eigentlich für Swing geschriebenen Klassenbibliothek mit einer sehr guten Dokumentation kostenlos herunterladen. 3.2.8.1 Grundidee Im Form-Layout werden die einzelnen Komponenten tabellarisch angeordnet. Im Gegensatz zum Form-Layout aus SWT lassen sich die Struktur der Tabelle und das Verhalten der einzelnen Zeilen und Spalten vorab durch einen String festlegen. Dadurch fallen die einzelnen Ausrichtungen beim Einfügen von Komponenten weg. Weiterhin erlaubt das Form-Layout auch Größenangaben in Form so genannter „Dialog Units“. Werden jetzt Abstände nicht pixel-genau sondern durch Dialog Units spezifiziert, dann passen sich die Abstände automatisch an, wenn beispielsweise die Zeichensatzgröße im Dialog oder die Pixelgröße des Bildschirm verändert werden. Durch eine Pixelangabe dagegen wären die Abstände bei vergrößerten Zeichensätzen dann zu klein. Die Abbildung 3.50 zeigt einen Beispieldialog mit dem zugrunde liegenden tabellarischen Raster. Wichtig ist hier, dass auch die Abstände zwischen den Komponenten als einzelne Zeilen und Spalten beschrieben werden. FormLayout layout = new FormLayout( "4dlu, left:pref, 4dlu, fill:max(80dlu;pref):grow, 4dlu, pref, 4dlu", "4dlu, pref, 4dlu, fill:pref:grow, 4dlu, fill:pref:grow", 4dlu); Zeilen fill:max(80dlu;pref):grow Abbildung 3.50: Dialog mit Form-Layout Die Definition der Zeilen und Spalten wird in Form zweier Strings an den Layout-Manager übergeben. Der erste String beinhaltet die Spaltenmaße: 1. Die erste Spalte ist lediglich ein Abstandshalter zum linken Rand mit einer Breite von vier Dialog-Einheiten („Dialog Units“). 2. Die Spalte mit dem „Duke“ soll in der bevorzugten Breite (pref) linksbündig (left) gesetzt werden. Die linksbündige Angabe ist eigentlich überflüssig, da sich in dieser Spalte nur eine Komponente befindet. 3. Jetzt folgt wieder ein Platzhalter von vier Dialog-Einheiten. 47 3.2 Layout-Management 4. Die vierte Spalte ist etwas komplizierter definiert. Sie soll beim Start ihre bevorzugte Breite (pref) bzw. mindestens 80 Dialog-Einheiten einnehmen. Die Angabe (80dlu,pref) beinhaltet ein Intervall mit einer Untergrenze von 80 Dialogeinheiten und einer Obergrenze, der bevorzugten Breite. Die Spalte kann zusätzlichen Platz belegen (fill) und sie darf, wenn der Dialog größer wird, auch in die Höhe wachsen (grow). 5. Dann folgt der Platzhalter, der einen festen Abstand zu den beiden Tasten erzwingt. 6. Die Tasten sollen in ihren bevorzugten Breiten angeordnet werden. 7. Abschließend kommt ein Platzhalter, der den Abstand zum rechten Rand definiert. Nach den Spalten fehlt noch die Festlegung der Zeilen: 1. Die erste Zeile ist ein Abstandshalter zum oberen Rand mit einer Höhe von vier Dialog-Einheiten. 2. Die Spalte mit der Überschrift soll in der bevorzugten Höhe der Komponenten gesetzt werden. Hier bestimmt nur die bevorzugte Höhe des Labels „Titel“ den Platz. 3. Jetzt folgt wieder ein Platzhalter von vier Dialog-Einheiten. 4. Die vierte Zeile besitzt drei Angaben. Sie soll beim Start ihre bevorzugte Höhe einnehmen (pref), sie kann zusätzlichen Platz belegen (fill) und sie darf, wenn der Dialog größer wird, auch in die Höhe wachsen (grow). 5. Dann folgt der Platzhalter, der einen festen Abstand zwischen den beiden Tasten erzwingt. 6. Diese Zeile verhält sich genau wie die vierte. 7. Abgeschlossen wird die Beschreibung durch einen Platzhalter, der den Abstand zum unteren Rand definiert. 3.2.8.2 Einfügen von Komponenten im Beispiel Nachdem das Grundgerüst des Dialogs steht, lassen sich jetzt sehr einfach die Komponenten einfügen. In der Regel ist es ausreichend, ihre Position im Raster sowie die Anzahl Zeilen und Spalten, über die sich sich erstrecken, anzugeben. Dabei ist zu beachten, dass die Zählung der Zeilen und Spalten bei 1 beginnt! Der PanelBuilder hilft beim Einfügen von Widgets in den Vater-Container. Dazu werden ihm Bedingungen in Form der Klasse CellConstraints übergeben. Diese bestehen in der Regel aus der X- und Y-Position der Zelle, in der das Widget platziert sein soll. Zusätzlich lassen sich auch die Anzahl Spalten und Zeilen angeben, die ein Widget belegen soll. Fehlt diese Angabe, dann belegt das Widget genau eine Zelle. // Der Panel-Builder hilft beim Einfügen von Komponenten // in das Layout. Er bekommt als zweiten Parameter // den Container übergeben, in den die Widgets // eingefügt werden. PanelBuilder builder = new PanelBuilder(layout, shell); // Die Bedingungen werden später durch Aufruf von // Methoden übergeben. Das Objekt kann // wiederverwendet werden. CellConstraints cc = new CellConstraints(); 48 3.2 Layout-Management Die Titelzeile erstreckt sich über eine Zeile und fünf Spalten, wobei die Platzhalterspalten mitgezählt werden. Abbildung 3.51: Einfügen des Titels // Der Text über allen Komponenten befindet sich in // Zelle (2,2) und erstreckt sich über 5 Spalten // und eine Zeile. Label label = new Label(shell, SWT.BEGINNING); label.setText("Titel"); builder.add(label, cc.xywh(2, 2, 5, 1)); Das mehrzeilige Texteingabefeld erstreckt sich über eine Spalte und drei Zeilen. Abbildung 3.52: Einfügen des Textfeldes Text text = new Text(shell, SWT.MULTI | SWT.WRAP | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL); builder.add(text, cc.xywh(4, 4, 1, 3)); Als letztes Widget in diesem Beispiel soll noch die Ok-Taste eingefügt werden. Sie belegt nur eine einzige Zelle. Abbildung 3.53: Einfügen der Ok-Taste Button button = new Button(shell, SWT.PUSH); button.setText("Ok"); builder.add(button, cc.xy(6, 4)); Das Form-Layout von JGoodies unterstützt auch identische Spaltenbreiten und Zeilenhöhen (siehe API-Dokumentation). 49 3.3 SWT-Widgets 3.2.9 GUI-Designer Es gibt Entwickler, denen die manuelle Programmierung von Layouts wenig Spaß bereitet. Daher bietet es sich in der Praxis an, die Oberfläche mit einen GUI-Editor interaktiv zu erzeugen. Unterstützung bieten unter Anderem: WindowBuilder Pro für Eclipse (kostenlos unter http://code.google.com/webtoolkit/tools/wbpro/index.html) Visual Editor Plugin für Eclipse (noch etwas „unfertig“, unterstützt nicht immer die aktuellsten Eclipse-Versionen, http://www.eclipse.org/vep/) Jigloo-Plugin für Eclipse (www.cloudgarden.com/jigloo/, kostenlos für den nicht-kommerziellen Einsatz) Hier können die Komponenten visuell den Zellen zugeordnet werden. Ein eigener Editor erlaubt die Änderung von Attributen einer Komponente oder eines Constraints. Der Nachteil des Ansatzes besteht darin, dass die mit einer IDE interaktiv erstellten Oberflächen sich nicht immer in einer anderen IDE interaktiv verändern lassen, da GUI-Editoren nicht zwangsweise dieselben Techniken verwenden, um die erzeugten Oberflächen so abzuspeichern, dass sie bearbeitbar bleiben. Hier hilft nach einem IDE-Wechsel gelegentlich nur noch der Weg der manuellen Programmierung. Ein GUI-Designer entbindet den Entwickler aber nicht davon, Layout-Manager und deren Verhalten zu kennen. Auch ein GUI-Designer verwendet für die Platzierung Layouts. 3.3 SWT-Widgets In diesem Kapitel werden einige wichtige Widgets aus SWT vorgestellt. Das Kapitel 3.7 stellt anhand eines Widgets aus JFace vor, wie es das entsprechende Gegenstück aus SWT kapselt und ihm einen sogenannten Model-View-Controller-Ansatz (MVC) hinzufügt. Sie finden an dieser Stelle keine Erklärung der kompletten SWT-API. Diese würde sicherlich mehrere Bücher füllen. Statt dessen erhalten Sie eine Übersicht über die wichtigsten Komponenten, die zum Bau einer Oberfläche benötigt werden. Diese Komponenten sind aber nicht unabhängig voneinander. Sie stehen vielmehr über eine Vererbungshierarchie mit gemeinsamen Basisklassen in Verbindung (siehe Abschnitt 3.5). Die folgende Aufstellung ist nicht vollständig. Sie soll Ihnen lediglich eine erste Orientierung zur Auswahl und zur Suche in der SWT-API geben. Sehen Sie sich einfach einmal die Übersicht über wichtige Widgets unter http://www.eclipse.org/swt/ widgets/ an. Neben den Widgets aus dem Paket org.eclipse.swt existieren auch weitere im Paket org.eclipse.swt.custom. Diese haben ähnliche Namen wie die aus org.eclipse.swt, besitzen aber teilweise ein erweitertes Verhalten oder wurden in ihrer Darstellung so angepasst, dass sie optimal in Tabellen eingefügt werden können. 50 3.3 SWT-Widgets Tabelle 3.1: Wichtige Widgets Name Verhalten und Verwendung Browser Der Browser ist ein Widget, das einen im Betriebssystem installierten Browser kapselt. Button (SWT.PUSH) Ein Button, der mit der Konstanten SWT.PUSH erzeugt wurde, ist eine „normale“ Taste. Sie kann gedrückt werden, wobei der Selektionszustand nicht erhalten bleibt. Sie dient dazu, Aktionen anzustoßen, indem Ereignisse ausgelöst werden. Button (SWT.FLAT) Die Taste verhält sich wie die mit der Konstanten SWT.PUSH erzeugte. Sie weist aber eine andere (flachere) Rahmendarstellung auf. Button (SWT.ARROW | SWT.LEFT) Die Taste verhält sich wie die mit der Konstanten SWT.PUSH erzeugte. Anhand der zusätzlich übergebenen Konstante erzeugt sie eine Pfeildarstellung. Erlaubt sind SWT.LEFT, SWT.RIGHT, SWT.UP und SWT.DOWN. Button (SWT.CHECK) Es handelt sich um eine Taste, die selektiert und deselektiert werden kann. Der Selektionszustand bleibt bis zur Änderung erhalten. Die Komponente dient dazu, eine Möglichkeit an- oder abzuwählen, ohne dass dadurch die Belegung einer anderen Komponente direkt beeinflusst wird. Beispiel: Fettund Kursivschrift können unabhängig von einander bestimmt werden. Button (SWT.RADIO) Ein Button, der mit der Konstanten SWT.RADIO erzeugt wurde, ist eine Taste, die sich an- oder abwählen lässt. Befinden sich andere solcher Tasten im Layout in ihrer Nähe, dann bilden diese eine Gruppe, von der immer nur eine Taste zu einem Zeitpunkt selektiert sein kann. Dadurch ist sichergestellt, dass bei Auswahl einer Belegung keine andere in der Gruppe möglich ist. Beispiel: Die Auswahl einer Textausrichtung für links- und rechtsbündig ist nicht gleichzeitig sinnvoll. Button (SWT.TOGGLE) Diese ist eine Taste, die wie eine Taste mit der Konstanten SWT.CHECK eingesetzt werden kann. Lediglich die Darstellung unterscheidet sich. Diese Taste wird sehr häufig mit einem eigenen Icon verwendet. Beispiel: Auswahl einer Textausrichtung Combo (SWT.DROP_DOWN), CCombo Eine Combobox zeigt einen Eintrag aus einer Liste möglicher Einträge an. Der Eintrag kann frei gewählt werden. Je nach Einstellung der Combobox ist sogar die manuelle Eingabe eines Wertes möglich. Die Liste ist normalerweise verborgen und wird erst durch Interaktion mit dem Benutzer sichtbar. Die Klasse CCombo ist eine Combobox, die für die Darstellung in einer Tabellenzelle angepasst wurde. Combo (SWT.SIMPLE) Wie eine Combobox mit der Konstanten SWT.DROP_DOWN erlaubt sie die Selektion aus bestimmten Vorgaben. Hinzu kommt aber noch, dass eine Mehrfachauswahl erlaubt werden kann. Weiterhin ist die maximale Anzahl gleichzeitig sichtbarer Vorgaben einstellbar. Die manuelle Eingabe neuer Werte ist allerdings nicht möglich. Date (SWT.DATE) Das Datumswidget erlaubt nur die Eingabe korrekter Datumswerte. Dieses kann entweder durch eine manuelle Eingabe oder durch Drücken der Tasten neben dem Feld erfolgen. Dieses Widget ist daher für die Datumseingabe einem einfachen Textfeld vorzuziehen. 51 3.3 SWT-Widgets Name Verhalten und Verwendung Date (SWT.TIME) Das Datumswidget erlaubt durch Setzen der Konstanten SWT.TIME statt einer Datums- eine korrekten Zeiteingabe. Dieses kann entweder durch eine manuelle Eingabe oder durch Drücken der Tasten neben dem Feld erfolgen. Dieses Widget ist daher für die Zeiteingabe einem einfachen Textfeld vorzuziehen. Date (SWT.CALENDAR) Das Datumswidget erlaubt durch Setzen der Konstanten SWT.CALENDAR die komfortable Auswahl eines Datums aus einem Kalender. Group Eine Gruppe ist ein Container-Objekt, das für Radio-Buttons verwendet wird. Es gruppiert diese logisch und optisch. Es kann immer nur eine Taste dieser Gruppe zu einem Zeitpunkt selektiert sein. Label, CLabel Das Label dient als Erklärung mit einer textuellen Bezeichnung bzw. einem Bild für eine andere Komponente. Es erlaubt keine direkte Eingabe von Werten. Die Klasse CLabel erweitert das normale Label um eine gleichzeitige Darstellung von Bild und Text. Es bietet einen Farbverlauf im Hintergrund, ein Hintergrundbild sowie automatische Textkürzungen, falls der Text zu lang sein sollte. Link Der Link zeigt einen Text an, innerhalb dessen der auswählbare Teil mit <A>Text</A> markiert ist. So kann z.B. innerhalb eines Textes in einem Dialog ein Verweis auf die zugehörige Hilfe eingebaut werden. List Eine List funktioniert ähnlich wie ein Combo-Widget mit der Konstanten SWT.SIMPLE. ProgressBar SWT.SMOOTH Der Fortschrittsbalken wird mit dieser Konstanten verwendet, um den aktuellen Stand einer eine länger laufenden Operation für eine bekannte Anzahl von Schritten bis zum Ende anzuzeigen. ProgressBar SWT.INDETERMINATE Der Fortschrittsbalken wird der Konstanten SWT.INDETERMINATE eingesetzt, um den aktuellen Stand einer eine länger laufenden Operation für eine unbekannte Anzahl an Schritten bis zum Ende anzuzeigen. Scale Der Schieberegler erlaubt die Auswahl eines ganzzahligen Wertes aus einem vorgegebenen Bereich. Der Regler kann sowohl horizontal als auch vertikal angeordnet werden und an vorgegebenen Markierungen einrasten. ScrollBar Scrollbalken lassen sich vertikal und horizontal ausrichten. Normalerweise werden sie nicht direkt erzeugt, sondern durch Angabe einer entsprechenden Konstante beim Erzeugen eines Widgets, dass das Scrollen unterstützt. Bisher wurde in diesem Skript immer ein mehrzeiliges Texteingabefeld verwendet, dessen Inhalt gescrollt werden soll, wenn er zu groß für das Widget ist. Die Scrollbalken können mit Hilfe der Methoden getVerticalBar() bzw. getHorizontalBar() ausgelesen werden. Slider Der Schieberegler erlaubt die Auswahl eines ganzzahligen Wertes aus einem vorgegebenen Bereich. Der Regler kann sowohl horizontal als auch vertikal angeordnet werden und an vorgegebenen Markierungen einrasten. 52 3.3 SWT-Widgets Name Verhalten und Verwendung Spinner Mit Hilfe dieses Widgets lassen sich ganzzahlige Werte bequem einstellen. Es können eine Obergrenze sowie die Schrittweite der Erhöhungen vergeben werden. StyledText Dieses Textfeld ist Text sehr ähnlich. Allerdings erlaubt es variable Textattribute. Ein Beispiel dazu ist in Abschnitt 3.7.3 zu finden. Table Die Table erlaubt die tabellarische Darstellung und Eingabe von Daten. Die Art der Darstellung kann vom Entwickler beeinflusst werden. Einzelne Zellen, Spalten, Zeilen oder Bereiche lassen sich auswählen. Die Abschnitte 3.7.1 und 3.7.2 behandelen die Konzepte (ohne und mit MVC-Ansatz) genauer. Text Es handelt sich hierbei um ein einzeiliges Freitexteingabefeld mit Cursorsteuerung und Selektion. Variable Textattribute werden nicht unterstützt. Somit haben alle Zeichen dieselbe Größe, Farbe und einen identischen Zeichensatz. Das Textfeld wird auch für Passworteingaben verwendet. Dazu muss die Konstante SWT.PASSWORD übergeben werden. Tree Ein Tree erlaubt die baumartige Darstellung mit ein- und ausklappbaren Knoten. Die Art der Darstellung kann ähnlich wie bei der Tabelle vom Entwickler beeinflusst werden. Daneben besitzt SWT eine ganze Anzahl weiterer Widgets. So wurde das Composite als Container für andere Widgets bereits vorgestellt. Weitere Container werden später im Abschnitt 3.8 im Skript betrachtet. Interessant ist auch das Canvas-Widget. Innerhalb seiner Fläche kann der Entwickler durch Zeichenoperationen den Inhalt komplett frei gestalten. Weitere Widgets, die bisher nicht Bestandteil von SWT sind, befinden sich im Eclipse-Projekt Nebula (http://www.eclipse.org/nebula/). Abbildung 3.54: Einige Widgets aus dem Nebula-Projekt Aber auch außerhalb der Eclipse-Seiten gibt es viele Widgets, die zum Teil kostenlos verfügbar sind. Ein Beispiel dafür ist die „RCPToolbox“ (http://www.richclientgui. com/detail.php?product_id=1). Sie besitzt unter Anderem ein Widget zur Anzeige von Karten aus „Google Maps“. 53 3.4 Menüs und Toolbar-Leisten Abbildung 3.55: Einige Widgets aus der RCPToolbox 3.4 Menüs und Toolbar-Leisten Diese Kapitel zeigt die Verwendung von Menüs und Toolbar-Leisten mit reinem SWT. Später in Abschnitt 3.6.3 werden Sie sehen, dass unter Verwendung von JFace einige Schritte deutlich einfacher umgesetzt werden können. 3.4.1 Normale Menüs Menüs setzen sich aus einer Anzahl unterschiedlicher Komponenten zusammen, die im Folgenden dargestellt werden: Mnemonic MenuItem (SWT.CASCADE) Menu (SWT.BAR) Accelerator MenuItem (SWT.NONE) Menu MenuItem (SWT.CHECK) Abbildung 3.56: Komponenten in einem Menü 54 3.4 Menüs und Toolbar-Leisten In Menüs dürfen auch Radio-Tasten dargestellt werden: T.CHECK) MenuItem (SWT.RADIO) MenuItem (SWT.SEPARATOR) Abbildung 3.57: Radio-Tasten in einem Menü Bei Menüs werden ebenso wie bei den bisher vorgestellten Widgets Konstanten verwendet, um eigentlich unterschiedliche Klassen anzusprechen. Menu (SWT.BAR): Das ist die Menüleiste selbst, die innerhalb eines Fensters platziert wird. Die Menüleiste nimmt die einzelnen Menüs auf. Menu (ohne Konstante): Ein solches Menü ist ein einzelnes Menü in der Menüleiste oder ein Untermenü in einem anderen Menü. MenuItem (SWT.CASCADE): Hierbei handelt es sich um einen Eintrag in einem Menü oder in der Menüleiste, der ein Untermenü enthält. MenuItem (SWT.NONE): Dieser Eintrag verhält sich wie eine Taste. Wenn er ausgewählt wird, löst er genau dieselben Ereignisse wie eine Taste aus. Es handelt sich also um einen „normalen“ Menüeintrag. MenuItem (SWT.CHECK): Hierbei handelt es sich um eine Checkbox, die dem Aussehen und Verhalten der Menüeinträge angepasst wurde. Ein solcher Eintrag kann also gewählt und auch wieder abgewählt werden. MenuItem (SWT.RADIO): Dieses Element entspricht einem Button mit der Konstanten SWT.RADIO, dessen Aussehen dem der Menüeinträge angepasst wurde. So können einzelne Auswahlen (z.B. von Optionen) direkt im Menü angezeigt werden. Um eine Selektion aus einer Anzahl solcher Tasten zu ermöglichen, werden Radio-Tasten zu Gruppen zusammengefasst. Dieses geschieht automatisch anhand der Einträge vor und nach den Radio-Tasten. Alle zusammengehörig platzierten Radio-Tasten bilden eine Gruppe, in der immer nur ein Eintrag ausgewählt sein kann. MenuItem (SWT.SEPARATOR): Dieser Eintrag ist ein optisches Trennelement zwischen zwei Menüeinträgen. So lassen sich logisch unterschiedliche Gruppen von Einträgen optisch trennen. Accelerator: Menüs sollen nicht nur mit der Maus, sondern auch über die Tastatur bedient werden können. Durch einen Accelerator kann ein Menüpunkt aufgerufen werden, ohne dass das zugehörige Menü überhaupt sichtbar ist (z.B. F2, Ctrl-C). Der Accelerator wird normalerweise rechts innerhalb des Menüeintrags dargestellt. Mnemonic: Mittels Mnemonics kann durch die Tastatur innerhalb des Menüs navigiert werden. Ein Mnemonic ist ein einzelnes Zeichen, das in der Regel durch einen unterstrichenen Buchstaben im zugehörigen Text dargestellt wird. Die Navigation erfolgt plattformabhängig (z.B. mit ALT-C unter Windows). Weil Apple die Verwendung von Mnemonics in seinen eigenen Richtlinien nicht empfiehlt, werden diese unter Mac OS X gar nicht erst dargestellt. 55 3.4 Menüs und Toolbar-Leisten 3.4.1.1 Bau von Menüstrukturen Der folgenden Schritte zeigen die Konstruktion eines Menüs, das aus mehreren Einträge besteht (Quelltext MenuApplication.java). Die Tastatursteuerung wird erst später im Abschnitt 3.4.1.6 ergänzt. 1. Zuerst wird eine Menüleiste erzeugt und am Fenster registriert. Dazu wird der Leiste das Fenster als Vater-Element übergeben. Menu menubar = new Menu(shell, SWT.BAR); shell.setMenuBar(menubar); 2. Jetzt wird der erste Eintrag in der Menüleiste erzeugt. An diesen Eintrag soll im Anschluss daran ein Menü angehängt werden. MenuItem fileItem = new MenuItem(menubar, SWT.CASCADE); fileItem.setText("File"); 3. Das Menü in Form eines Untermenüs kann an den erzeugten Menüeintrag angehängt werden. Das Untermenü hat den Eintrag, über den es aufgeklappt wird, als Vaterelement. Menu fileMenu = new Menu(menubar); // Drop-Down-Menue fileItem.setMenu(fileMenu); 4. Das neue Untermenü wird mit zwei Einträgen („New“ und „Open“) gefüllt. MenuItem item = new MenuItem(fileMenu, SWT.NONE); item.setText("New"); item = new MenuItem(fileMenu, SWT.NONE); item.setText("Open"); Das ist das Ergebnis der Konstruktion, wobei die Schritte oben nicht ganz vollständig sind. Es werden nicht alls Menü-Einträge erzeugt. Außerdem fehlt noch die Tastatursteuerung. Abbildung 3.58: Ausgabe des ersten Menü-Beispiels Dürfen gewisse Einträge zu bestimmten Zeitpunkte nicht verwendet werden, dann bietet es sich an, diese mit setEnabled(false) einfach zu sperren. 3.4.1.2 Checkbox als Menü-Eintrag Menüs dürfen Tasten enthalten, die ihren Selektionszustand speichern. Diese entsprechen den Button-Widgets mit der Konstanten SWT.CHECK. MenuItem checkItem = new MenuItem(fileMenu, SWT.CHECK); checkItem.setText("Check"); checkItem.setSelection(true); // vorausgewählt 56 3.4 Menüs und Toolbar-Leisten Das ist das Ergebnis, nachdem den Schritten aus Abschnitt 3.4.1.1 der Checkbox-Eintrag hinzugefügt wurde: Abbildung 3.59: Hinzufügen der Checkbox zum Menü 3.4.1.3 Radio-Taste als Menü-Eintrag Darf immer nur eine Selektion aus einer Gruppe von Einträgen aktiv sein, dann sollten Radio-Tasten verwendet werden. Diese entsprechen den Button-Widgets mit der Konstanten SWT.RADIO. MenuItem redItem = new MenuItem(editMenu, SWT.RADIO); redItem.setText("Red"); redItem.setSelection(true); // vorausgewählt Hier sehen Sie die Ausgabe. Der Trennstrich wird im folgenden Beispiel hinzugefügt. Abbildung 3.60: Hinzufügen der Radio-Taste zum Menü 3.4.1.4 Trennstrich zwischen Menü-Einträgen Der letzte Code-Ausschnitt zeigt die Erzeugung eines Trennstriches. Das Ergebnis war bereits in Abbildung 3.60 zu sehen. new MenuItem(editMenu, SWT.SEPARATOR); 3.4.1.5 Weitere Menü-Eigenschaften Menüs dürfen neben dem Text auch Bilder besitzen. Dazu besitzen Einträge die Methode setImage. 3.4.1.6 Tastatursteuerung Um eine bequeme Bedienung der Menüfunktionen ohne Maus erzielen zu können, müssen die sogenannten Accelerator an den Menüeinträgen registriert werden. Dazu zeigt die folgende Abbildung noch einmal die in wichtigsten Begriffe für die Tastatursteuerung. 57 3.4 Menüs und Toolbar-Leisten keine Mnemonics bei Mac OS X (von Apple nicht empfohlen) Accelerator Mnemonic Abbildung 3.61: Begriffe zur Tastatursteuerung in Menüs Erklärung der beiden Begriffe: Mnemonic: Die direkte Bedienung des Menüs erfolgt unter Zuhilfenahme von Mnemonics. Ein Mnemonic steht für einen Buchstaben aus dem Text des Menüeintrags. Dieses Zeichen wird einfach dadurch festgelegt, dass vor dem entsprechenden Buchstaben im Text ein &-Zeichen gesetzt wird. Dieses wird von SWT nicht angezeigt. Beispiel: &New deklariert das Zeichen N als Mnemonic für diesen Eintrag. Accelerator: Um diese Schnellbedienung zu aktivieren, sind zwei Schritte erforderlich. Zum Einen muss die Tastenkombination mit einem Tabulatorzeichen dem Ausgabetext angehängt werden, zum Anderen muss die Tastenkombination explizit als Accelerator festgelegt werden. Dazu dient der Methoden-Aufruf setAccelerator eines Menü-Eintrags. Beispiel: MenuItem newItem = new MenuItem(fileMenu, SWT.NONE); // Text + Accelerator-Text setzen newItem.setText("&New\tCtrl+N"); // Accelerator einschalten newItem.setAccelerator(SWT.CTRL | ’N’); Da mit SWT plattformunabhängige Programme erstellt werden, ist das Problem zubeachten, dass sich die Tastenkombinationen auf den Plattformen unterscheiden. Wo unter Windows die CTRL-Taste verwendet wird, setzt Mac OS auf die Apfel-Taste. Daher ist es in der Regel besser, plattformneutrale „Modifizierer“ (Konstanten aus der Klasse SWT) zu verwenden: SWT.MOD1 statt SWT.CTRL: Unter Windows wird die CTRL-, unter MacOS X die Apfel-Taste genommen. SWT.MOD2 statt SWT.SHIFT: SHIFT-Taste sowohl unter Windows als auch unter Mac OS X SWT.MOD3 statt SWT.ALT: ALT-Taste unter Windows und Mac OS X Es gibt noch ein weiteres plattformspezifisches Verhalten. Wo ist es unter Windows erforderlich, sowohl den Text für den Accelerator mit \t getrennt anzugeben als auch den Code mit setAccelerator zu setzen, wird unter Mac OS X dagegen der Text automatisch anhand des Tastatur-Codes gesetzt. Es ist dort aber zulässig, wie unter Windows zu verfahren. Damit sind aus technischer Sicht die wichtigsten plattformspezifischen Merkmale unterschieden. Leider gibt es aber noch ein nicht-technisches Problem zu beachten. So geben Hersteller von Fenstersystemen häufig Entwurfsrichtlinien für die Reihenfolge und Positionen der Menüs und ihrer Einträge vor. Weiterhin unterscheiden sich auf den Plattformen in der Regel auch die Tastatur-Kürzel zur schnelleren Bedienung. So führt normalerweise kein Weg daran vorbei, Menüstrukturen plattformabhängig zu bauen. 58 3.4 Menüs und Toolbar-Leisten 3.4.2 Popup-Menüs Popup-Menüs werden in der Regel bei einem „Rechtsklick“ auf eine Komponente angezeigt. Damit kann der Anwender Operationen ausführen, die diese Komponente direkt manipulieren. Die Erstellung eines solchen Menüs unterscheidet sich nur unwesentlich von der eines „normalen“ Menüs. 1. Zuerst wird das Popup-Menü erzeugt und an dem Widget registrieren. Das Menü erhält das Widget auch als Vaterelement. Menu popupMenu = new Menu(component, SWT.CASCADE); component.setMenu(popupMenu); 2. Jetzt lassen sich Einträge in das Menü einfügen: MenuItem copyItem = new MenuItem(popupMenu, SWT.NONE); copyItem.setText("Copy"); 3. Wie bei normalen Menüs dürfen auch Untermenüs eingehängt werden. Die Abbildung 3.62 zeigt ein Popup-Menü, das direkt an einem Fenster registriert wurde. Abbildung 3.62: Popup-Menü auf verschiedenen Plattformen 3.4.3 System-Tray Das System-Tray ist ein spezielles Menü, das auf dem Desktop verankert wird. Mit seiner Hilfe kann der Anwender wichtige Funktionen einer Anwendung steuern, ohne deren Fenster manuell öffnen zu müssen. Auch diese Menü wird fast wie ein „normales“ erzeugt. 1. Zuerst wird die Referenz auf das System-Tray vom Fenstersystem geholt. Es besteht die Möglichkeit, dass einzelne Fenstersysteme kein System-Tray unterstützen. Dieser Fall muss berücksichtigt werden. Anschließend wird für die eigene Anwendung ein einzelnes TrayItem-Element als Menü-Eintrag für das System-Tray erzeugt. Das TrayItem-Element sollte ein eigenes Bild erhalten, damit der zugehörige Eintrag in der häufig recht kompakten Darstellung im System-Tray gut erkennbar ist. Tray tray = display.getSystemTray(); if (tray != null) { TrayItem trayItem = new TrayItem(tray, SWT.NONE); trayItem.setImage(imageRegistry.get("duke")); 2. Jetzt kann dem TrayItem ein Menü zugeordnet werden, dass bei Aktivierung angezeigt wird. Es muss sich um ein Popup-Menü handeln, weil es ein Untermenü des System-Tray ist. final Menu menu = new Menu(shell, SWT.POP_UP); 59 3.4 Menüs und Toolbar-Leisten 3. Danach lassen sich Einträge und weitere Untermenüs in das Menü einfügen: MenuItem item = new MenuItem(tray, SWT.NONE); item.setText("Higher"); 4. Das eigene Menü muss manuell bei einem Mausklick eingeblendet werden. Dazu ist etwas Ereignisbehandlung erforderlich. Diese wird in Abschnitt 3.6 eingehend betrachtet. trayItem.addListener(SWT.MenuDetect, new Listener () { public void handleEvent (Event event) { menu.setVisible (true); } }); Die Positionen und das Aussehen unterscheiden sich etwas auf den unterschiedlichen Plattformen, was die Abbildung 3.63 zeigt. Abbildung 3.63: Menü in einem System-Tray auf verschiedenen Plattformen 3.4.4 Toolbars Toolbars werden eingesetzt, um dem Benutzer einen schnellen Zugriff auf häufig benötigte Funktionen zu bieten. Sie enthält in der Regel Tasten und Trennabstände, aber auch andere Komponenten sind möglich, wenn auch ungewöhnlich. Normalerweise werden Radio-Tasten, „normale“ Tasten, Checkboxen und Drop-Down-Listen eingesetzt. Gerade wenn Tasten mit sehr kleinen Bildern verwendet werden, sollten unbedingt auch TooltipTexte angegeben werden, da nicht jeder Benutzer sofort die Bedeutung eines kleines Bildchens erkennen kann. Die folgenden Schritte sind in der Regel zu durchlaufen, um eine Toolbar zu erzeugen: 1. Zuerst wird die Toolbar als Container angelegt. Neben einer horizontalen wäre auch eine vertikale Ausrichtung denkbar. ToolBar bar = new ToolBar(shell, SWT.HORIZONTAL); 2. Jetzt lassen sich Einträge zur Leiste hinzufügen. Hier wird eine „normale“ Taste eingesetzt, die statt eines Textes ein Bild besitzt. Da Bilder nicht immer sehr aussagekräftig sind, erhält die Taste auch einen Tooltip-Text. ToolItem item = new ToolItem(bar, SWT.PUSH); item.setImage(image); item.setToolTipText("Neu..."); Mögliche Tasten-Typen sind: SWT.PUSH: „normale“ Taste 60 3.4 Menüs und Toolbar-Leisten SWT.CHECK: Taste, die ihren Zustand speichern kann (Checkbox) SWT.RADIO: exklusive Auswahl aus einer Anzahl Möglichkeiten (Radio-Taste) SWT.SEPERATOR: Trennsymbol (z.B. Strich, größerer Abstand, . . . ) SWT.DROP_DOWN: Taste für eine Auswahlliste. Die Liste muss manuell angelegt und angezeigt werden. Das ist recht umständlich und soll daher hier nicht gezeigt werden. Zu beachten ist noch, dass sich die Toolbar im Layout des Fensters befindet. Daher muss ein passender Layoutmanager verwendet werden, der so ein schmales Element an seinem Rand unterstützt. Das Beispiel ToolBarApplication verwendet ein einspaltiges GridLayout, in dem die Toolbar in der obersten Zeile sitzt. Und so sieht das Resultat aus: ToolBarApplication Abbildung 3.64: Toolbar auf verschiedenen Plattformen 3.4.5 Coolbars Das Widget mit dem merkwürdigen Namen CoolBar ist eine flexiblere Toolbar. Da es nicht nur auf Tasten und Auswahllisten beschränkt ist, darf es beliebige Widgets beinhalten. Weiterhin kann der Anwender interaktiv die Einträge in der Coolbar verschieben. Die Schritte zur Erstellung sind auch hier relativ einfach: 1. Zuerst wird die Coolbar als Container angelegt. Neben einer horizontalen wäre auch eine vertikale Ausrichtung denkbar. CoolBar bar = new CoolBar(shell, SWT.HORIZONTAL); 2. Anschließend lassen sich einzelne Einträge in Form von CoolItems zur Leiste hinzufügen. Diese nehmen dann die eigentlichen Widgets auf. Dabei handelt es sich häufig um Toolbar-Elemente, aber auch andere Widgets sind denkbar. Leider muss die Größe eines Eintrags manuell berechnet werden, weil die CoolItems dazu nicht in der Lage sind. CoolItem item = new CoolItem(bar, SWT.NONE); item.setControl(contentsOfItem); // manuell Größe des CoolItems berechnen Ein ausführliches Beispiel folgt nach dieser Aufzählung. Wie bei der Toolbar gilt auch hier, dass sich die Coolbar im Layout des Fensters befindet. Daher muss der Layout-Manager „passen“. 61 3.4 Menüs und Toolbar-Leisten Coolbar Coolitem (veränderbar) Abbildung 3.65: Elemente einer Coolbar Das folgende Beispiel (CoolBarApplication) zeigt, wie eine Toolbar gefüllt und als Widget in die Coolbar eingefügt wird. Es handelt sich dabei um die erste Toolbar aus der Abbildung 3.65. private void initGUI(Shell shell) { // GridLayout mit einer Spalte: Zeile 0 für die Leiste shell.setLayout(new GridLayout(1, false)); // Horizontal angeordnete Leiste erzeugen. CoolBar bar = new CoolBar(shell, SWT.HORIZONTAL); // Größenänderung: Die Coolbar soll horizontal wachsen. GridData data = new GridData(SWT.FILL, SWT.DEFAULT, true, false); bar.setLayoutData(data); } // Einen Eintrag mit drei Tasten hinzufügen. CoolItem item = new CoolItem(bar, SWT.NONE); Composite element = createFileToolBar(bar); computeCoolItemSize(item, element); Jetzt wird die Toolbar erzeugt: // Drei Tasten zum Eintrag hinzufügen: // Dazu wird eine Toolbar mit den Tasten // angelegt und die Leiste als Control des CoolItems angegeben. // Prinzipiell könnte auch ein Composite mit eigenem // Layout verwendet werden. private Composite createFileToolBar(Composite parent) { ToolBar bar = new ToolBar(parent, SWT.HORIZONTAL); ToolItem item = new ToolItem(bar, SWT.PUSH); item.setImage(imageRegistry.get("new")); item.setToolTipText("Neu..."); } // Weitere Tasten ... return bar; Abschließend fehlt noch die Methode zur Größenberechnung des CoolItems anhand seines Inhalts. 62 3.5 Basisklassen // Größe eines CoolItems anhand seines Inhalts eintragen. // Element ist in diesem Beispiel eine Toolbar, // deren Größe die Maße des CoolItems bestimmt. private void computeCoolItemSize(CoolItem item, Composite element) { // ToolBar in das CoolItem als Element einfügen. item.setControl(element); // Größe des ToolBars berechnen. Point size = element.computeSize(SWT.DEFAULT, SWT.DEFAULT); // Größe des CoolItems so berechnen, dass ein // Element der Größe size aufgenommen werden kann. size = item.computeSize(size.x, size.y); } // Berechnete Größe in das CoolItem eintragen. item.setSize(size); Und so sieht dann das fertige Ergebnis aus, wenn alle Einträge hinzugefügt wurden: Abbildung 3.66: Beispiel einer Coolbar 3.5 Basisklassen Dieser Abschnitt beschäftigt sich mit einem Teil der Klassenhiererachie des Widgets. Das ist erforderlich, um zu verstehen, welche Klassen bestimmte Aufgaben erfüllen. Die Abbildung 3.67 zeigt einen Teil der Klassenhierarchie. Widget Caret Menu ScrollBar Control Label Button Scrollable Slider Text List Composite Abbildung 3.67: Vereinfachte Klassenhierachie 63 3.5 Basisklassen 3.5.1 Widget Das Widget ist die Basisklasse aller Klassen, die Benutzerinteraktion zulassen. Das heißt, dass alle Elemente, mit denen der Benutzer in irgendeiner Form interagieren kann, von Widget abstammen. Das Widget verwaltet darüber hinaus den Freigabezustand. Dazu ist es wichtig, sich daran zu erinnern, dass SWT Ressourcen und somit Widgets des darunterliegenden Fenstersystems verwendet. Diese Ressourcen können nicht vom Garbage Collector freigegeben werden, sondern müssen vom Entwickler verwaltet werden (siehe Abschnitt 3.1.4). In der Klasse Widget ist die Information abgelegt, ob das Widget bereits wieder manuell freigegen wurde. Dann würden viele Methodenaufrufe auf solch einem Widget mit einer Ausnahme abgebrochen werden. Weiterhin erlaubt ein Widget die Speicherung anwendungsspezifischer Informationen. Es handelt sich hierbei um eine Hashtabelle. Diese hilft dem Entwickler dabei, Daten direkt am Widget abzulegen, die er später z.B. zur Auswertung der Eingaben des Anwenders benötigt. So muss der Entwickler dazu keine eigene Verwaltung aufbauen. Die Verwendung ist sehr einfach: void setData(String key, Object value): Lege den Wert value unter dem Schlüssel key ab. Object getData(String key): Liest den Wert aus, der zum Schlüssel key gehört. Das Widget führt auch eine erste Ereignisbehandlung mit zwei verschiedenen Typen von Ereignissen ein: DisposeEvent: Dieses wird ausgelöst, wenn das Widget manuell freigegeben wurde. So lässt sich überwachen, ab wann auf dieses Widget nicht mehr zugegriffen werden kann. Untypisierte Ereignisse: Ein Ereignistyp wird durch eine Konstante beschrieben. Es gibt keine unterschiedlichen Klassen für die verschiedenen möglichen Ereignisse. Das Control, das von Widget erbt, besitzt dagegen für alle unterstützten Ereignisse spezielle Klassen und Methoden. Man spricht in diesem Fall von typisierten Ereignissen. Die Ereignisse und ihre Behandlung werden in Kapitel 3.6 eingeführt. 3.5.2 Control Ein Control ist ein Widget, das ein Gegenstück auf Betriebssystemebene besitzt. Es verwaltet eine ganze Anzahl von Eigenschaften, die in den folgenden Unterabschnitten vorgestellt werden. 3.5.2.1 Größen und Position Im Control werden die bevorzugte sowie die aktuelle Größe der Komponente abgelegt. Weiterhin beinhaltet es die aktuelle Position bezogen auf sein Vaterelement. In Abbildung 3.68 hat die Taste „Ok“ ein (zur Hervorhebung mit einem Rahmen versehenes) Composite-Objekt als Vater. Die Taste wird relativ zum Composite-Objekt, dieses wiederum relativ zu seinem Vater, dem Fenster, platziert. 64 3.5 Basisklassen Abbildung 3.68: Relative Positionierung von Controls 3.5.2.2 Farben Alle Control-Elemente unterstützen separate Vorder- und Hintergrundfarben. Diese lassen sich mit setForeground bzw. setBackground setzen. Zusätzlich kann mit Hilfe der Methode setBackgroundImage ein Bild als Hintergrund platziert werden. Abbildung 3.69: Hintergrundfarbe eines Controls 3.5.2.3 Tooltips Gerade wenn einem Anwender nicht sofort auf den ersten Blick ersichtlich ist, was er mit einem Control machen kann, dann sollte dem Control ein erklärender Tooltip-Text mit setToolTipText eingetragen werden. Dieses betrifft besonders solche Controls, die lediglich ein Bild und keinen Text besitzen. setToolTipText Abbildung 3.70: Tooltip an einem Control 3.5.2.4 Aktivierungszustand Controls erlauben die Ein- und Ausgabe von Werten. Ist zu einem Zeitpunkt eine Eingabe nicht erwünscht oder sinnvoll, dann sollte sie auch verhindert werden. Dazu kann ein Control gesperrt und später auch wieder freigegeben werden (Methode setEnabled). Abbildung 3.71: Gesperrtes Control 65 3.5 Basisklassen 3.5.2.5 Weitere Eigenschaften Es gibt noch einige weitere interessante Eigenschaften: Die Sichtbarkeit kann mit setVisible ein- oder ausgeschaltet werden. Soll das Control ein eigenes Popup-Menü erhalten, dann kann es mit setMenu eingetragen werden. Jedes Control kann seinen Text in einem separaten Zeichensatz darstellen. Dieser wird mit Hilfe der Methode setFont am Control registriert. Das Kapitel 3.2 hat verschiedene Layout-Manager eingeführt. Bei einigen von ihnen ist es erforderlich, zusätzliche Layout-Eigenschaften am Control abzulegen. Die Methode setLayoutData übernimmt diese Aufgabe. Jedem Control kann ein individueller Mauszeigertyp zugewiesen werden, um die Art der Eingabe anzuzeigen. Es ist möglich, die Reihenfolge festzulegen, in der Controls den Tastaturfokus erhalten. Weiterhin kann einem Control auch direkt der Fokus zugewiesen werden. Controls erlauben das „Ziehen und Ablegen“ (Drag- und Drop) von Daten. Ein kompletter Dialog besteht aus einem Baum von Controls und Widgets. Es lässt sich innerhalb dieses Baums nicht nur vom Vater zu seinen Kindern, sondern auch von einem Kind zu seinem Vaterlement navigieren. Dazu enthält jedes Control eine Referenz auf sein Vater-Element. 3.5.2.6 Ereignisse Die Basisklasse Widget verwaltet lediglich untypisierte Ereignisse. In der abgeleiteten Klasse Control dagegen werden typisierte Ereignisse unterstützt. Damit ist gemeint, dass es für jede Art von Ereignis eine eigene Ereignisklasse und separate Methoden gibt. Dieses vereinfacht die Ereignisbehandlung gerade in den Fällen, in denen der Entwickler lediglich an bestimmten Ereignistypen interessiert ist. Weiterhin ist der Zugriff auch typsicherer. Auswahl an Ereignissen: ControlEvent: Dieses Ereignis wird gemeldet, wenn das Control verschoben oder in seiner Größe verändert wurde. DragDetectEvent: Der Inhalt des Controls soll per „Drag and Drop“ verschoben oder kopiert werden. FocusEvent: Das Control hat den Tastaturfokus erhalten oder verloren. HelpEvent: Es wurde eine Hilfe für das Control angefordert. KeyEvent: Es wurde eine Taste der Tastatur gedrückt oder losgelassen. MenuDetectEvent: Es wurde ein Popup-Menü für die Komponente angefordert. MouseEvent: Es wurde eine Maustaste über der Komponente gedrückt. MouseMoveEvent: Der Mauszeiger wurde über der Komponente bewegt. PaintEvent: Die Komponente muss neu gezeichnet werden. Das Ereignis kann dazu verwendet werden, eigene Controls zu erstellen, die sich selbst zeichnen. TraverseEvent: Es wurde eine Taste gedrückt, um den Fokus auf eine andere Komponente zu setzen. Von Control abgeleitete Klassen bieten häufig noch weitere typisierte Ereignisse. 66 3.6 Ereignisbehandlung 3.5.3 Scrollable Einige Controls verwalten Inhalte, die für einen Dialog zu groß werden können. Dabei kann es sich beispielsweise um ein mehrzeiliges Texteingabefeld oder eine Zeichenfläche in einem Malprogramm handeln. Um den Inhalt trotzdem bearbeiten zu können, muss dieser gescrollt werden. Alle Controls, die ihren Inhalt verschieben („scrollen“) können, erben von der Basisklasse Scrollable. Diese verwaltet den zur Verfügung stehenden Platz und stellt bei Bedarf die horizontalen und vertikalen Scrollbalken zu Verfügung, auf die mit den folgenden Methoden zugegriffen werden kann: ScrollBar getHorizontalBar(): horizontalen Scrollbalken auslesen ScrollBar getVertikalBar(): vertikalen Scrollbalken auslesen 3.5.4 Composite Das Composite ist schon aus Beispielen zu geschachtelten Layouts bekannt. Es ist die Basisklasse für alle Widgets, die weitere Widgets als Kindelemente besitzen können, die also als Container für andere Controls oder Widgets dienen. Ein Composite verwaltet die Kindelemente und sowie Layout-Manager zu deren Platzierung. Weiterhin kennt es die Reihenfolge, in der die Kindelemente beim Druck auf die Tabulatortaste besucht werden. 3.6 Ereignisbehandlung Nachdem in den vorherigen Abschnitten immer wieder einmal auf die Ereignisbehandlung hingewiesen wurde, soll sie jetzt endlich richtig eingeführt werden. Mit dem Begriff der „Ereignisbehandlung“ ist hier gemeint, wie die Anwendung Ereignisse mitgeteilt bekommt. Da in Anwendungen ein aktives Abfragen von Benutzerinteraktionen (Polling) völlig indiskutabel ist, wird die Anwendung durch Ereignisse informiert, wenn der Anwender Aktionen einleiten möchte. Der Auslöser einer solchen Aktion kann beispielsweise ein Tastendruck, eine Mausbewegung usw. sein. Denkbar sind aber auch alternative Eingabegeräte wie Braille-Tastaturen für blinde Anwender. 3.6.1 Beobachter-Muster Die Ereignisbehandlung basiert auf dem Beobachter-Entwurfsmuster. Statt „Beobachter“ wird in deutschsprachiger Literatur auch manchmal der englischsprachige Begriff „Listener“ eingesetzt. Das folgende Diagramm zeigt zur Erinnerung noch einmal die Funktionsweise des Beobachtermusters ohne einen Bezug zu SWT und JFace. 67 3.6 Ereignisbehandlung EventObject -source: Object <<interface>> Listener +eventHappened AnEvent <<interface>> Subject 0..* +addAnEventListener +removeAnEventListener +fireAnEventHappened ConcreteListener -state: Type ConcreteSubject +eventHappened +addAnEventListener +removeAnEventListener +fireAnEventHappened AnEvent event = new AnEvent(…); for each listener (listeners) listener.eventHappened(event); Abbildung 3.72: Allgemeines Bobachter-Muster Die Interaktion läuft immer wie folgt ab, wobei die Namen der Klassen und Schnittstellen in Diagramm 3.72 nicht den realen Gegebenheiten entsprechen: 1. Je nach Aufgabe erzeugt ein Control sehr unterschiedliche Ereignisse. Beispiele sind das Verschieben des Mauszeigers oder das Drücken einer Taste. Diese Ereignisse werden durch unterschiedliche Klassen implementiert (AnEvent). Alle Klassen haben eine gemeinsame Basisklasse (EventObject) und eventuelle weitere Attribute und Methoden. 2. Möchte man einen Beobachter für einen bestimmten Ereignistyp schreiben, dann muss dieser eine zum Ereignistyp passende Schnittstelle implementieren. Der Name der Schnittstelle ist aus dem Ereignistyp ableitbar: EreignisnameListener 3. Danach registrieren sich ein oder mehrere Beobachter (ConcreteListener) an den Objekten (ConcreteSubject), über deren Änderungen sie informiert werden wollen. Die Methode zur Registrierung hat in SWT und JFace immer den festen Namensaufbau addEreignisnameListener. 4. Sollte der Beobachter nicht länger an der Benachrichtigung interessiert sein, dann kann er sich auch wieder abmelden. 5. Tritt im Laufe der Zeit ein Ereignis auf, dann wird intern im Subjekt eine Methode (hier: fireAnEventHappened) aufgerufen. Diese durchläuft die Liste aller registrierten Beobachter und informiert sie, indem (in diesem Beispiel) die Methode eventHappened in ConcreteListener aufgerufen wird. 68 3.6 Ereignisbehandlung Der Ablauf lässt sich wie folgt zusammenfassen: listener ConcreteListener subject ConcreteSubject 1: addAnEventListener(this) registrieren event AnEvent 2: create benachrichtigen 3: eventHappened() Abbildung 3.73: Ablauf im Bobachter-Muster Teilweise ist in der Literatur auch die Bezeichnung „publisher-subscriber model“ für das Beobachter-Muster zu finden. 3.6.2 Ablauf unter SWT Bevor die programmatische Verwendung der SWT-Ereignisse behandelt wird, soll im kommenden Abschnitt noch ein kurzer Blick das Zusammenspiel zwischen Fenstersystem und SWT bei der Ereigniszustellung geworfen werden. 3.6.2.1 Anbindung an das Fenstersystem SWT und JFace setzen auf dem zugrundeliegende Fenstersystem auf. Da der Ursprung der Ereignisse auch das Fenstersystem ist, lohnt es sich, einen Blick auf den „Weg“ eines Ereignisses von seiner Entstehung bis zu einem Widget bzw. Control als Ziel zu werfen. Betriebssystem Ereignis-Queue Nachricht Display Top-Level- Ereignis Shell Widget Ereignis ListenerInterface Aufruf Event-HandlerMethode Abbildung 3.74: Ereigniszustellung unter SWT 69 3.6 Ereignisbehandlung Der interne Ablauf sieht so aus: 1. Nachdem das Betriebs- bzw. Fenstersystem das Ereignis erkannt hat, wird es intern in einer Warteschlange („Ereignis-Queue“) gespeichert. Diese Pufferung kann erforderlich sein, weil eventuell schneller Ereignisse anfallen, als sie von einer Anwendung abgearbeitet werden können. 2. Danach wird das Display-Objekt ermittelt, das zur Anwendung gehört. Normalerweise besitzt jede SWT-Anwendung nur ein solches Objekt. 3. Anhand des Display-Objektes kann das Fenster ermittelt werden, in dem das Ereignis ausgelöst wurde. 4. In diesem Schritt wird innerhalb des Fensters das Widget gesucht, dem das Ereignis galt. Handelt es sich beispielsweise um ein Mausereignis, dann ist das Ziel der Interaktion das oberste Widget an der Position des Mauszeigers. 5. Das Widget wird informiert, dass ein Ereignis eingetreten ist. Es informiert daraufhin alle Beobachter, die an ihm registriert sind. Das geschieht, indem die entsprechende Methode, die die Beobachter-Schnittstelle vorgegeben hat, aufgerufen wird. 6. Der „Event-Handler“ ist der Beobachter. Er wird vom Widget informiert und kann seinerseits Aktionen durchführen. Hier wird begrifflich zwischen „Nachricht“ und „Ereignis“ unterschieden. Das ist relativ willkürlich und soll lediglich ausdrücken, dass der normale Entwickler die Nachrichten des Fenstersystems gar nicht zu Gesicht bekommt. 3.6.2.2 Programmierung In SWT werden sehr viele unterschiedliche Arten von Ereignissen unterstützt. Das wohl am häufigsten verwendete Ereignis ist das SelectionEvent. Es soll daher hier exemplarisch für alle anderen Ereignisse näher betrachtet werden. So lösen unter Anderem Tasten („Buttons“) dieses Ereignis aus, nachdem sie gedrückt und wieder losgelassen wurden. Um diese Ereignisse abzufangen, müssen immer die folgenden Schritte durchlaufen werden: 1. Definition der Beobachter-Klassen 2. Erzeugen einer Instanz der Beobachter-Klasse 3. Registrierung der Beobachter-Instanz an einem Widget Das ist nicht neu und wurde bereits im vorherigen Abschnitt über das allgemeine Beobachter-Muster so vorgestellt. Da Ereignisse in SWT einem festen Namensschema folgen, kann sehr leicht aus dem Namen des Ereignistyps auf die Namen der anderen beteiligten Klassen geschlossen werden. Im Falle des SelectionEvents sind das: Die Ereignisklasse, die zusätzliche Informationen zum Ereignis beinhaltet, heißt so wie der Ereignistyp: SelectionEvent Die Schnittstelle des Beobachters bekommt an den Typ des Ereignisses die Endung Listener angehängt. Somit heißt sie SelectionListener. 70 3.6 Ereignisbehandlung Die Beobachter-Schnittstellen besitzen bei vielen Ereignistypen eine ganze Anzahl von Methoden, die aber nicht immer in jedem Szenario benötigt werden. Damit der Entwickler nicht alle unnötigen Methoden selbst (leer) implementieren muss, gibt es Adapter-Klassen. Diese implementieren die Beobachter-Schnittstelle mit leeren Methodenrümpfen. Der Name des Adapaters bekommt als Endung Adapter. Beim SelectionEvent heißt der demnach SelectionAdapter. Der Name der Methode des Controls, über die sich ein Beobachter anmeldet, beginnt mit add, gefolgt vom Ereignistyp und endet mit Listener. Für das Selektionsereignis lautet der Name also addSelectionListener. Das nächste Beispiel zeigt, wie sich ein Beobachter in Form einer anonymen inneren Klasse an einer Taste registriert. Button okButton = new Button(shell, SWT.PUSH); okButton.setText("Weiter"); // Als Beobachter an der Taste anmelden. okButton.addSelectionListener(new SelectionListener() { // die wichtige Methode @Override public void widgetSelected(SelectionEvent event) { System.out.println("widgetSelected: " + event.widget); } // Wird auf einigen Plattformen nie erzeugt (z.B. // Doppelklick auf einen Listeneintrag). @Override public void widgetDefaultSelected(SelectionEvent event) { } }); Einfach ist es, wenn statt einer Schnittstelle der passende Adapter verwendet wird. In diesem Fall ist es nicht mehr erforderlich, die zweite, nicht benötigte Methode widgetDefaultSelected zu überschreiben. Button okButton = new Button(shell, SWT.PUSH); okButton.setText("Weiter"); // Wieder als Beobachter anmelden. okButton.addSelectionListener(new SelectionAdapter() { // die wichtige Methode @Override public void widgetSelected(SelectionEvent event) { System.out.println("widgetSelected: " + event.widget); } }); 3.6.2.3 Unterscheidung der Ereignisquellen Nun kommt es in realen Anwendungen nicht selten vor, dass mehr als ein Widget ein Selection-Ereignis auslösen kann. Wie können die Quellen der Ereignisse unterschieden werden? Es ist ja durchaus interessant zu wissen, ob die Weiter- oder Abbruchtaste in einem Dialog gedrückt wurde. Für jede Quelle einen eigenen Beobachter zu nehmen, ist nicht nur sehr aufwändig sondern auch unflexibel. Die Lösung besteht darin, den Beobachter die Ereignisquelle herausfinden zu lassen. Generell für alle Ereignistypen ist 71 3.6 Ereignisbehandlung das durch Auslesen der Quelle aus dem Ereignisobjekt mit Hilfe des öffentlichen Attributs widget der Basisklasse TypedEvent möglich. Alle Ereignisklassen, die typisierte Ereignisse repräsenatieren, erben direkt oder indirekt von TypedEvent. Das Attribut widget liefert eine Widget-Referenz auf den Auslöser des Ereignisses zurück. Diese Referenz kann mit den bekannten Referenzen verglichen werden. Nun kann es aber vorkommen, dass die Referenzen auf die Oberflächen-Komponenten in der Dialogklasse privat sind und ein Zugriff somit nicht erlaubt ist. Da der Fall nicht selten vorkommt, gibt es eine weitere Lösung. Abschnitt 3.5.1 hat die Eigenschaften der Klasse Widget aufgeführt. Dazu gehört auch die Möglichkeit, benutzerspezifische Informationen an einem Widget abzulegen. An jeder Komponente, die ein Ereignis auslösen kann, wird eine Information abgelegt, die später während der Ereignisbehandlung ausgelesen wird. Das Beispiel zeigt den Einsatz (Datei ButtonEventApplication.java): final Button okButton = new Button(shell, SWT.PUSH); okButton.setText("Weiter"); // Benutzerspezifische Information ablegen. // - "source" ist der Schlüssel. // - "ok" ist der zugehörige Wert. okButton.setData("source", "ok"); // Beobachter registrieren okButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { // Variante 1: Auslesen der benutzerspezifischen // Information boolean wasOk = event.widget.getData("source").equals("ok"); } }); // Variante 2: Vergleich der Referenzen auf das Widget boolean wasOk = event.widget == okButton; In der Methoden widgetSelected können nun die Ereignisquellen sehr einfach unterschieden werden. Es ergibt sich in der Praxis häufig der folgende Aufbau, wobei der Name des Schlüssels natürlich nicht unbedingt "source" sein muss: public void widgetSelected(SelectionEvent event) { if (event.widget.getData("source").equals("command1")) { // Ausführung für Komponente 1 } else if (event.widget.getData("source").equals("command2")) { // Ausführung für Komponente 2 } // ... else if (event.widget.getData("source").equals("commandN")) { // Ausführung für Komponente N } } 3.6.2.4 Entwurfsmöglichkeiten Wer aber behandelt die Ereignisse? Soll es die Klasse machen, die den Dialog erzeugt hat, oder aber ist es sinnvoller, eine separate Klasse zu verwenden? 72 3.6 Ereignisbehandlung Eine zentrale Beobachterklasse empfängt die Ereignisse Zunächst wird die Dialogklasse so erweitert, dass sie von der Klasse SelectionAdapter erbt oder die Schnittstelle SelectionListener implementiert. Interessant ist auch hier wieder nur die Methode widgetSelected. Danach wird ein Objekt der Beobachter-Klasse erzeugt und an den Komponenten (hier Tasten) registriert: SelectionListener listener = new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { ... } }); okButton.addSelectionListener(listener); cancelButton.addSelectionListener(listener); Jedes Widget erhält seinen eigenen Beobachter Im Unterschied zur Variante 1 wird für jedes Widgets, das ein Ereignis auslösen kann, eine eigene Beobachter-Klasse geschrieben. Ansonsten unterscheidet sich diese Variante nicht von der ersten. Vergleich der Ansätze In der Praxis gibt es weitere Möglichkeiten, die später auch noch vorgstellt werden. Wichtig in diesem Zusammenhang ist auch die Trennung von Anwendungslogik bzw. Geschäftslogik und Oberfläche. Weil gerade dieses Thema in der Praxis eine hohe Bedeutung erfährt, wird im Skript in Kapitel 4 eine separate Dialogsteuerung behandelt. Daher werden hier nur die beiden bereits bekannten Varianten diskutiert. 1. Variante: Eine separate Klasse empfängt die Ereignisse: + Die Lösung ist schnell erstellt. + Es werden weniger Klassen benötigt. – Der Ansatz wird schnell sehr unübersichtlich, da eine Methode fast die komplette Ereignisbehandlung übernimmt. 2. Variante: Jedes Widget erhält seinen eigenen Beobachter: + + – – 3.6.2.5 Die Auftrennung ist bei vielen verschiedenen Ereignistypen häufig flexibler. Der Ereigniscode kann für verschiedene Klasse wiederverwendet werden. Die Lösung ist teilweise aufwändiger zu erstellen. Es werden mehr Klassen benötigt. Ereignisse bei Fenstern Dieser kleine Abschnitt zeigt exemplarisch die Verwendung der Ereignisses, die ein Fenster auslösen kann. Der Ereignistyp ist in diesem Fall das ShellEvent.. Somit heißen die Beobachter ShellListener für die Schnittstelle und ShellAdapter. Es gibt in diesem Beobachter mehrere Methoden: 73 3.6 Ereignisbehandlung public void shellIconified(ShellEvent event): Das bisher geöffnete Fenster wurde zu einem Icon verkleinert. public void shellDeiconified(ShellEvent event): Das Fenster wurde aus einem Icon zu seiner vorherigen Größe wiederhergestellt. public void shellDeactivated(ShellEvent event): Ein offenes Fenster ist nicht länger aktiv. Es war bisher das oberste Fenster. Jetzt wurde aber ein anderes nach oben gebracht. public void shellActivated(ShellEvent event): Das Fenster ist aktiv. Es wird das oberste Fenster, das Eingaben erwartet. public void shellClosed(ShellEvent event): Das Fenster soll vom Anwender geschlossen werden. Diese Aktion lässt sich programmgesteuert verhindern, indem das Attribut doIt im Ereignisobjekt auf false gesetzt wird. Dieses auf den ersten Blick merkwürdige Verhalten lässt sich gut an einem Beispiel begründen: Das Programm soll einen Anwender beim Schließen eines Fensters fragen, ob alle bisher ungesicherten Daten gespeichert oder verworfen werden sollen, oder ob das Programm das Fensters lieber doch nicht schließen soll. Für den letzten Fall kann das Attribut doIt verwendet werden. shell.addShellListener(new ShellAdapter() { @Override public void shellClosed(ShellEvent event) { if (!fensterschliessen()) { event.doit = false; } } }); Das Attribut wird auch für andere Ereignistypen verwendet, die aber bisher noch nicht vorgestellt wurden. Die API-Dokumentation ist hier wie immer hilfreich. 3.6.3 Ablauf unter JFace JFace basiert auf den SWT-Ereignissen, ergänzt aber eine Abstraktionsschicht. Die Idee dahinter stellt der kommende Abschnitt vor. 3.6.3.1 Abstraktion von SWT mittels Aktionen Ein kleines Beispiel einer Anwendung soll die Abstraktion vorstellen: Das Programm besitzt ein Menü sowie eine Toolbar-Zeile. In beiden befindet sich jeweils ein Eintrag, über den der Anwender seine Daten speichern kann. Er kann also auf zwei Arten dieselbe Aktion auslösen. Unter SWT würde ein Entwickler den Menüeintrag sowie die Taste in der Toolbar-Leiste separat erzeugen und einfügen. Dann müsste er noch einen Beobachter schreiben und an beiden Elementen registrieren. In JFace dagegen wird eine Operation sowie dessen Beschreibungen in einer Einheit, der Aktion, gekapselt. Eine Aktion (Klasse Action) in dem Beispiel würde also aus dem Text wie z.B. „Sichern“, einem Icon sowie einer Methode bestehen, die den Beobachter darstellt. Zusätzlich kann einer Aktion auch eine Tastenkombination zugeordnet werden, über die der zugehörige Beobachter aufgerufen wird. Damit ist die Aktion unabhängig davon, wie sie dargestellt wird. Aus ihrer Beschreibung werden nun Menüeinträge oder Tasten für Toolbar-Leisten 74 3.6 Ereignisbehandlung erzeugt. Weiterhin wird damit automatisch der Beobachter registriert. Zusammengefasst lässt sich sagen, dass eine Aktion aus diesen Komponenten besteht: ein beschreibender Text, der z.B. als Beschriftung für Tasten verwendet werden kann ein Bild für Tasten, Toolbar-Tasten usw. Tastenkombinationen als Auslöser Beobachter-Code, der bei Aktivierung der Aktion ausgeführt wird. Anhand der Abbildung 3.75 ist ersichtlich, dass aus einer Aktion Widgets erzeugt werden. Umgekehrt rufen diese die Aktion als Beobachter auf, wenn ein Ereignis eingetreten ist. 2. ruft auf 1. beschreibt Darstellung 1. beschreibt Darstellung Aktion Abbildung 3.75: Aktion für Menüeintrag und Toolbar-Taste Die Darstellung ist teilweise plattformabhängig. So kann es vorkommen, dass die MenüIcons unter Gnome in dessen Standard-Einstellung ausgeblendet sind. In Abbildung 3.76 ist das anhand der zweiten Zeile von Screenshots gut zu erkennen. Abbildung 3.76: Darstellungen aus einer Aktion 3.6.3.2 Anbindung an das Fenstersystem JFace verbirgt einen Teil der Details, die unter SWT noch direkt erkannbar waren. So fällt dann die Zustellung eines Ereignisses durch das Fenstersystem auch etwas anders aus. Betriebssystem: Ereignis-Queue verwaltet jeweils ein Menü, eine Toolbar, ... Nachricht Display Application Window Action Contribution Aufruf Hauptfenster einer Anwendung run-Methode einer Aktion Abbildung 3.77: Ereigniszustellung unter JFace 75 3.6 Ereignisbehandlung ApplicationWindow ist eine Klasse, die das Shell-Objekt kapselt (siehe Abschnitt 3.1.2). Dabei werden bestimmte Methoden vorgegeben, die ein Entwickler überschreiben kann, um seine Oberfläche zu erzeugen. Eine „Contribution“ ist für die Verteilung von Ereignisses zuständig, die aus eine Aktion erzeugt wurden. Je Menü, Toolbar-Leiste usw. gibt es ein solches Objekt. Trifft bei ihm ein Ereignis ein, dann ruft es in der zugehörigen Aktion die run-Methode auf. Diese Methode ersetzt die widgetSelected-Methode der SWT-Ereignisbehandlung und wird vom Entwickler überschrieben. 3.6.3.3 Programmierung Aktionen abstrahieren nur von SelectionEvents. Andere Ereignistypen sind nicht betroffen. Anhand des folgenden Beispiels wird gezeigt, wie prinzipiell mit Aktionen gearbeitet wird. Die Aktion soll ein neues Dokument erzeugen. Abbildung 3.78: Aktion zur Dokumenterzeugung Aktionsklassen erzeugen Zunächst muss eine Klasse erstellt werden, die die Aktion darstellt. Im einfachsten Fall erbt sie von der Klasse Action. Im Konstrutor werden Methoden der Basisklasse aufgerufen, um Texte, das Bild und sowie die Tastenkombination zur Aktivierung der Aktion zu übergeben (Beispiel ActionApplication). public class NewDocumentAction extends Action { public NewDocumentAction() { // Text, der die Aktion darstellt super("&Neu"); // Text, "N" ist Mnemonic // Tooltip-Text setToolTipText("Neues Dokument"); // Tastenkombination zur Aktivierung setAccelerator(SWT.MOD1 | ’N’); } // Bild, ActionApplication.NEW_ICON ist ein // String der Beispielanwendung mit einem // Dateinamen. setImageDescriptor(ImageDescriptor.createFromFile( this.getClass(), ActionApplication.NEW_ICON)); // Aktion, die bei Selektion des Eintrags ausgeführt wird. @Override public void run() { // Im Beispiel wird eine Meldung in der Statuszeile 76 3.6 Ereignisbehandlung } } // des Fensters ausgegeben. ActionApplication.getApplication().setStatus( "Neues Dokument erzeugt!"); Aktionsobjekte erzeugen Nach der Erstellung der Aktionsklassen können diese in der eigentlichen Anwendung eingesetzt werden. Dazu werden Objekte der Klassen erzeugt. public class ActionApplication extends ApplicationWindow { // Verwendete Icons public static final String NEW_ICON = "/res/new_wiz.gif"; public static final String SAVE_ICON = "/res/save_edit.gif"; // Alle private private private benötigten Aktionen Action newDocumentAction; Action saveDocumentAction; Action exitAction; // Alle Aktionsobjekte erzeugen. public ActionApplication() { super(null); newDocumentAction = new NewDocumentAction(); saveDocumentAction = new SaveDocumentAction(); exitAction = new ExitAction(); } // Die Methode wird gleich erstellt. createContents(); //... Menüeintrag aus einer Aktion erzeugen Wie in Abschnitt 3.6.3.2 beschrieben, verwaltet eine sogenannte „Contribution“ die Aktionen eines Menüs oder einer Toolbar. Für Menüs wird hierzu die Klasse MenuManager verwendet. Der Manager wird in der überschriebenen Methode createMenuManager der Fensterklasse ApplicationWindow erzeugt, wobei für jedes Untermenü eine eigene „Contribution“ erzeugt werden muss. // ... Fortsetzung der Klasse ActionApplication @Override protected MenuManager createMenuManager() { // Menüzeile (Contribution) erzeugen. MenuManager mainManager = new MenuManager(); // Das Dateimenü bekommt auch eine Contribution. MenuManager fileMenuManager = new MenuManager("&Datei"); // Aktionen zum Dateimenü hinzufügen, // erzeugt den jeweiligen Menüeintrag. fileMenuManager.add(newDocumentAction); fileMenuManager.add(saveDocumentAction); // Trennstrich fileMenuManager.add(new Separator()); 77 3.6 Ereignisbehandlung fileMenuManager.add(exitAction); } // Dateimenü in Menüzeile einhängen. mainManager.add(fileMenuManager); // Menüzeile (Contribution) zurückgeben. return mainManager; // ... sprechenden Abbildung 3.79: Menü aus Aktionen erzeugen Coolbar-Taste aus einer Aktion erzeugen Für Toolbar- und Coolbar-Tasten funktioniert der Mechanismus genauso wie für Menüeinträge. Hier liegen die „Contributions“ in Form der beiden Klasse CoolBarManager bzw. ToolBarManager vor. // ... Fortsetzung der Klasse ActionApplication @Override protected CoolBarManager createCoolBarManager(int style) { // CoolBar-Leiste erzeugen. CoolBarManager coolManager = new CoolBarManager(style); // ToolBar-Leiste erzeugen. ToolBarManager fileManager = new ToolBarManager(style); // Aktionen zur ToolBar-Leiste hinzufügen. fileManager.add(newDocumentAction); fileManager.add(saveDocumentAction); } // ToolBar-Leiste zur CoolBar-Leiste hinzufügen. // Das funktioniert wie bei Menüs mit vorhandenen. // Untermenüs. coolManager.add(fileManager); return coolManager; // ... Abbildung 3.80: Coolbar aus Aktionen erzeugen 78 3.7 Model-View-Controller mit JFace Anwendung In Anwendung müssen jetzt noch die Methoden der Basisklasse ApplicationWindow aufgerufen werden, die sicherstellen dass das Menü sowie die Coolbar auch wirklich erstellt werden. // ... Fortsetzung der Klasse ActionApplication private createContents() { // Bewirkt, dass createMenuManager aufgerufen wird. addMenuBar(); // Bewirkt, dass createCoolBarManager aufgerufen wird. addCoolBar(SWT.HORIZONTAL); // Bewirkt, dass die Statuszeile erzeugt wird. addStatusLine(); } // ... Abbildung 3.81: Ergebnis auf einigen Fenstersystemen Wie Ihnen vielleicht aufgefallen ist, müssen die Tastenkombinationen für die „Accelerator“ nicht mehr dem Text, mit einem Tabulator getrennt, angehängt werden. Es reicht aus, die Tastencodes mit setAccelerator einzutragen. 3.7 Model-View-Controller mit JFace In den bisherigen Kapiteln hat die eigentliche Anwendung immer direkt auf die Widgets zugegriffen. Allerdings hat sich in den vergangenen Jahren sehr schnell gezeigt, dass diese fehlende Trennung zwischen Darstellung und den dargestellten Daten problematisch ist und zu einem schwer wartbaren Programm führt. Deshalb wurde als einer der ersten Lösungsansätze das MVC-Entwurfsmuster eingeführt. Den Begriff „Entwurfsmuster“ gab es damals allerdings in der Informatik noch nicht. MVC wurde zuerst in Smalltalk Ende der 80er des vorigen Jahrhunderts eingesetzt. Es handelt sich dabei um eine Anwendung des Beobachter-Musters, das aus drei Teilen besteht: 1. Das Model enthält die Zustandsinformation eines Widgets. Dazu könnte bei einem Texteingabefeld beispielsweise der eingegebene Text gehören. 2. Der View ist Beobachter des Zustands (des Modells), um diesen bei Änderungen sofort darzustellen. 3. Der Controller legt das Verhalten des Widgets auf Benutzereingaben fest. 79 3.7 Model-View-Controller mit JFace View Model Controller Abbildung 3.82: Struktur im MVC-Ansatz So sieht der Ablauf bei einer Eingabe des Benutzers aus: Der Controller verändert das Modell. Weiterhin informiert er die Sicht bei solchen Benutzereingaben, die nicht das Modell verändern. Dazu gehört beispielsweise das Verschieben eines Textcursors. Das Modell ändert seinen Zustand und informiert seinerseits die Sicht. Die Sicht holt sich die Daten aus dem Modell und stellt sie dar. Wichtig beim MVC-Ansatz ist, dass das Modell weder Controller noch Sicht „kennt“. Der große Vorteil dieses Ansatzes besteht darin, dass die Implementierungen der Komponenten leicht ausgetauscht werden können, oder dass mehrere Sichten das Modell auf unterschiedliche Arten darstellen können. Diagramm 3.83 zeigt den Ablauf mit Pseudomethoden zur MVC-Kommunikation mit einer Komponente, wenn der Benutzer ein Ereignis ausgelöst hat. view Component model Datamodel controller Controller 1: registrieren 2: Ereignis senden event AnEvent 2.1.1 erzeugen 2.1: Modellereignis erzeugen 2.1.2: Zustandswechsel senden Abbildung 3.83: Ablauf im MVC Die nächsten Abschnitte zeigen die Verwendung von Tabellen mit und ohne Trennung von Ansicht und Modell. SWT selbst bietet keine Trennung. Erst mit JFace ist eine solche Trennung durch die Programmierschnittstelle vorgegeben. Neben Tabellen unterstützen eine ganze Anzahl weitere Widgets die Trennung. Dazu gehören mehrzeilige Texteingabefelder, Listen und Bäume. 80 3.7 Model-View-Controller mit JFace 3.7.1 Tabelle ohne MVC Abbildung 3.84 zeigt, wie sich eine SWT-Tabelle zusammensetzt. Table TableItem TableColumn Abbildung 3.84: Wichtige Klassen für eine Tabelle Eine Tabelle der Klasse Table nur mit SWT zu erzeugen bedeutet, dass die komplette Tabellenstruktur manuell gebaut werden muss und dass ein direkter Lese- und Schreibzugriff auf die einzelnen Zellen erforderlich ist. Wie funktioniert so eine SWT-Tabelle? Die Klasse Table stellt eine Tabelle dar, die einzelne Spalten (TableColumn) und Zeilen in Form der Klasse TableItem visualisiert. Damit der Anwender später nicht nur Text eingeben kann, besteht die Möglichkeit, die Eingaben in einzelne Zellen durch verschiedene Editoren vorzunehmen. Dazu wird die Klasse TableEditor eingesetzt. Die Vorlesung behandelt nur einen kleinen Aspekt der Klasse Table. Sie finden aber sowohl in der API-Dokumentation einen guten Einstieg als auch in [JavaBsp] Beispiele für eigene Experimente. 3.7.1.1 Bau einer Tabelle Um eine Tabelle zu erzeugen, werden normalerweise die folgenden Schritte durchlaufen. Eine Eingabe über Editoren ist hier nicht vorgesehen. 1. Erzeugen der Tabelle. table = new Table(shell, SWT.MULTI | SWT.CHECK | SWT.FULL_SELECTION); SWT_CHECK bewirkt, dass die erste Spalte zusätzlich eine Checkbox erhält. Mit SWT.MULTI sind Mehrfachauswahlen, mit SWT.SINGLE nur Einfachauswahlen von Einträgen erlaubt. Und schließlich weist SWT.FULL_SELECTION die Tabelle an, immer die komplette Zeile bei einer Auswahl zu markieren. 2. Das Neuzeichnen der Tabelle sollte während ihrer Erzeugung verhindern. Das ist wichtig, weil ansonsten die Darstellung der Tabelle flackern kann. Außerdem würde ein Neuzeichnen nach jedem Schritt den Aufbau deutlich verlangsamen. table.setRedraw(false); 81 3.7 Model-View-Controller mit JFace 3. Jede Spalte muss einzeln angelegt werden. Die Tabelle ist immer das Vaterelement. In der folgenden Abbildung ist die Spalte rot markiert, die durch den anschließenden Code-Schnippsel erzeugt wird. TableColumn tcol = new TableColumn(table, SWT.LEFT); tcol.setText("Auswahl"); Mit SWT.LEFT wird der Inhalt linksbündig ausgerichtet. Erlaubt sind auch die Angaben SWT.CENTER und SWT.RIGHT. Die Methode setText trägt den Spaltentitel ein. 4. Jede Zeile muss mit ihren Daten angelegt werden. Auch hier ist das Vaterelement die Tabelle. Die nächste Abbildung zeigt rot markiert die Zeile, die durch den CodeAusschnitt darunter erzeugt wird. TableItem item = new TableItem(table, SWT.NONE); item.setText(new String[]{"Zeilenwert 0", "Zeilenwert 1", "Zeilenwert 2"}); Die einzige erlaubte Konstante ist SWT.NONE, weil eine Tabellenzeile keine eigenen Attribute besitzt. Die Methode setText erhält ein eindimensionales Array mit den Werten einer kompletten Zeile. 5. SWT kann die Spaltenbreiten automatisch anhand des Inhaltes berechnen. Dazu muss auf jedem Spaltenobjekt die pack-Methode aufgerufen werden. Später bei der Einführung virtueller Tabellen werden Sie sehen, wie die Spaltenbreiten manuell vergeben werden können. table.getColumn(colIndex).pack(); 6. Zum Schluss muss das Neuzeichnen der Tabelle wieder zugelassen werden. table.setRedraw(true); 82 3.7 Model-View-Controller mit JFace 3.7.1.2 Beispiel Die Beispielanwendung (Klasse TableSWTApplication) enthält den fast vollständigen Code, um die Tabelle aus Abbildung 3.84 zu erzeugen. public class TableSWTApplication { // ... // Tabellendaten private String[][] data; // Spaltenüberschriften private static final String[] COL_HEADERS = { "Auswahl", "Spalte 1", "Spalte 2" }; // Tabelle bei der Konstruktion der Oberfläche anlegen private void createGUI() { // ... // Tabelle erzeugen: Mehrfachselektion, Checkbox in der // ersten Spalte, Selektion erstreckt sich immer über // alle Spalten table = new Table(shell, SWT.MULTI | SWT.CHECK | SWT.FULL_SELECTION); // Spaltenüberschriften sollen sichtbar sein. table.setHeaderVisible(true); // Trennlinien zwischen den Zellen werden angezeigt. table.setLinesVisible(true); // Neuzeichnen während der Konstruktion der Tabelle // verhindern, damit es schneller geht. table.setRedraw(false); // Tabellenspalten erzeugen und Überschriften eintragen. for (int col = 0; col < COL_HEADERS.length; col++) { TableColoumn col = new TableColumn(table, SWT.LEFT); col.setText(COL_HEADERS[ col ]); } // Zeilen in die Tabelle einfügen und mit Daten füllen. for (int rowIndex = 0; rowIndex < data.length; rowIndex++) { TableItem item = new TableItem(table, SWT.NONE); item.setText(data[ rowIndex ]); } // Spaltenbreiten berechnen lassen. Ansonsten sind nicht // alle Inhalte lesbar. for (int col = 0; col < table.getColumnCount(); col++) { table.getColumn(col).pack(); } } } // Neuzeichnen der Tabelle wieder zulassen. table.setRedraw(true); // ... // ... 83 3.7 Model-View-Controller mit JFace 3.7.1.3 Tabellen mit sehr vielen Zeilen Was passiert, wenn die Tabelle sehr viele Einträge besitzt (z.B. mehr als 1000)? Die Einträge sind aber niemals alle gleichzeitig sichtbar. Trotzdem werden zunächst einmal alle Zeilenobjekte (TableItem) erzeugt. Das bedeutet, dass sowohl die Konstruktion sehr lange Zeit benötigt, als auch, dass Speicherplatz verschwendet wird. SWT bietet als Lösung des Problems virtuelle Tabellen, die ihre Daten dann dynamisch durch Ereignisse anfordert, wenn diese Daten sichtbar werden sollen. Die Konstruktion einer solchen Tabelle unterscheidet sich doch deutlich von der einer statisch erzeugten. 1. Zuerst muss die Tabelle angelegt werden. Zum Kennzeichnen, dass die Daten dynamisch zur Laufzeit bereitgestellt werden, muss die Konstante SWT.VIRTUAL übergeben werden. table = new Table(shell, SWT.MULTI | SWT.CHECK | SWT.FULL_SELECTION | SWT.VIRTUAL); 2. Anschließend werden die Spalten mit ihren Überschriften angelegt. Weil die Tabelle immer nur die sichtbaren Daten dynamisch anfordert, kann sie nicht selbst ermitteln, wieviel Platz jede einzelne Spalte benötigt. Somit kann die Tabelle auch nicht automatisch die Spaltenbreiten festlegen. Das muss der Entwickler manuell durch Aufruf der Methode setWidth auf jeder einzelnen Spalte durchführen. TableColumn tcol = new TableColumn(table, SWT.LEFT); tcol.setText("Auswahl"); tcol.setWidth(200); 3. Wichtig ist noch, dass ein SetData-Listener an der Tabelle registriert wird. Über ihn meldet die Tabelle immer dann ein Ereignis, wenn sie neue Daten anzeigen muss. Im Beobachter werden die neuen Daten in die Tabelle bzw. das TableItemObjekt eingetragen. Das Ereignisobjekt hält in seinem öffentlichen Attribut index die Nummer der Zeile, für die neue Daten benötigt werden. table.addListener(SWT.SetData, new Listener() { public void handleEvent(Event event) { // "data" ist in diesem Beispiel ein Array mit den // Tabellendaten. Das Array wird intern von der // Anwendung verwaltet. data[event.index] liefert // ein eindimensionales Array mit den Zellwerten // für alle Spalten einer Zeile. ((TableItem) event.item).setText(data[event.index]); } }); Das Beispiel VirtualTableSWTApplication enthält den vollständigen Quelltext, um eine virtuelle Tabelle für Abbildung 3.84 zu erzeugen. 3.7.1.4 Weitere Eigenschaften SWT-Tabellen sind mächtiger, als die vorhergehenden Kapitel vermuten lassen. So können Spalten durch Klick in den Tabellenkopf sortiert werden. Weiterhin können die Ausgabe der Werte in den Zellen sowie die Eingabemöglichkeit durch den Anwender vielfältig verändert werden. Das Beispiel VirtualEditableTableSWTApplication zeigt die prinzipielle Funktionsweise. Da dieses aber relativ umständlich ist und JFace hier einfachere Möglichkeiten bietet, soll an dieser Stelle lediglich kurz auf das Sortieren der Einträge verwiesen werden: 84 3.7 Model-View-Controller mit JFace 1. Zunächst muss ein Beobachter an der Tabelle registriert werden, der auf Klicks in die Spaltenköpfe reagiert. 2. Wird der Beobachter aufgerufen, dann muss er die Tabellendaten selbst sortieren. 3. Anschließend löscht er den kompletten Tabelleninhalt und fügt die sortieren Daten wieder in die Tabelle ein. Alternativ ist es häufig sinnvoller, statt alle TableItemObjekte zu löschen und erneut anzulegen, diese nur mit den sortierten Daten neu zu befüllen. Damit sind aber leider nicht alle Probleme gelöst. So findet hier keine echte Trennung von Daten und ihrer Darstellung statt. Genau diese bietet aber JFace, so dass in den kommenden Abschnitten auf den dort vorhandenen MVC-Ansatz eingegangen wird. 3.7.2 Tabelle mit MVC JFace bietet bei einigen Komponenten, die komplexere Datenstrukturen visualisieren, einen sauberen MVC-Ansatz: Für die SWT-Klasse Tree steht ein TreeViewer zur Verfügung. Die SWT-Tabelle Table wird von einem TableViewer gekapselt. Für die SWT-Liste List stellt ListViewer den MVC-Ansatz zur Verfügung. Die Auswahlliste Combo aus SWT wird vom ComboViewer gekapselt. Die erforderlichen JFace-Klassen sind alle im Paket org.eclipse.jface.viewers zu finden. Da die Arbeit mit allen Viewer-Klassen relativ ähnlich ist, soll hier nur das Prinzip anhand der Tabelle gezeigt werden. Alle Beispiele funktionieren mit Eclipse ab Version 3.3. Für ältere Versionen gibt es auch entsprechende Lösungen, die aber nicht unbedingt mehr zu empfehlen sind. Das folgende vereinfachte Klassendiagramm zeigt die wichtigsten Klassen für den TableViewer. Die nicht mehr relevanten älteren Klassen fehlen. Mit blauer Schrift sind die Klassen gekennzeichnet, die vom Benutzer in seiner eigenen Anwendung erstellt werden. Die Namen sind nur beispielhaft zu sehen. Table TableColumn stellt dar 1 stellt dar 1 TableViewerColumn holt Daten über 1 SWT <<abstract>> CellLabelProvider TableViewer 1 gehört zu eingeben <<interface>> IContentProvider <<abstract>> EditingSupport stellt dar 1 1 <<interface>> IStructuredContent Provider MyEditingSupport verwendet 1 ColumnLabelProvider <<abstract>> CellEditor MyContentProvider MyLabelProvider TextCellEditor ModellDaten Eingabe Modell Darstellung Abbildung 3.85: Wichtige Klassen für eine JFace-Tabelle 85 3.7 Model-View-Controller mit JFace Gut zu erkennen sind die unterschiedlichen Klassen für das Modell, die Ansicht und die Benutzereingabe. Die grau hinterlegte Bereich „SWT“ beinhaltet einen Teil der Klassen, die JFace von SWT nutzt. 3.7.2.1 Modell Dieser Abschnitt beschäftigt sich mit dem Modell sowie dessen Anbindung an die Tabelle. Table TableColumn stellt dar 1 stellt dar 1 TableViewerColumn holt Daten über 1 SWT <<abstract>> CellLabelProvider TableViewer 1 gehört zu eingeben <<interface>> IContentProvider <<abstract>> EditingSupport stellt dar 1 1 <<interface>> IStructuredContent Provider MyEditingSupport verwendet 1 ColumnLabelProvider <<abstract>> CellEditor MyContentProvider MyLabelProvider TextCellEditor ModelData Eingabe Modell Darstellung Abbildung 3.86: Modellklassen für die JFace-Tabelle Um ein Modell für eine Tabelle zu erstellen, ist keine direkte Abhängigkeit von einer JFace-Klasse erforderlich. Im einfachsten Fall besteht das Modell aus einer ArrayList mit Objekten. Jedes Objekt repräsentiert eine Zeile der Tabelle. Durch Filter kann die Anzeige bestimmer Zeilen (Objekte) unterdrückt werden. Eine separate Sortierung erlaubt es, die Reihenfolge in der Ansicht anhand von Kriterien vertauschen zu lassen. Denkbar ist beispielsweise, den Tabelleninhalt anhand des Nachnamens einer Person zu sortieren. Dabei wird nicht der Modellinhalt verändert. Es handelt sich lediglich um eine Darstellung der Zeilen, die nicht der Reihenfolge im Modell entspricht. Die folgende Beispiel zeigt eine einfache Klasse Person mit dem Namen und dem Alter einer Person. public class Person { private String name; private int age; // ... } Das Modell enthält eine ArrayList mit Person-Objekten, die wie in Abbildung 3.87 dargestellt werden soll. 86 3.7 Model-View-Controller mit JFace public class Person { private String name; private int age; // ... } ArrayList Abbildung 3.87: JFace-Tabelle für eine Liste von Personen Mit diesem Modell kann die Tabelle noch gar nichts anfangen. Um die Daten aus dem Modell in die Tabelle zu kopieren, wird die Schnittstelle IStructuredContentProvider benötigt. Der Entwickler stellt eine modellspezifische Klasse bereit, die diese Schnittstelle implementiert und die Modelldaten so aufbereitet, dass die Tabelle sie darstellen kann. Dazu müssen die Modelldaten in ein Array mit Objekten konvertiert werden. Das ist im Falle einer ArrayList recht einfach, weil diese mit der Methode toArray schon die erforderliche Konvertierung besitzt. Darüber hinaus informiert die Tabelle den IStructuredContentProvider über eine komplette Änderungen der Daten in der Tabelle. Abbildung 3.88 zeigt den Ablauf der Interaktion. application viewer TableViewer 1: setInput(dataModell) provider IStructuredContentProvider 1.1: inputChanged(dataModell) 2: getElements(dataModell) liefert eine ArrayDarstellung des Datenmodells dataModell. Abbildung 3.88: Interaktion zwischen JFace-Tabelle und Modell Die Anwendung trägt die eigentlichen Modelldaten mit setInput in die Tabelle ein. Diese speichert die Daten und informiert den IStructuredContentProvider darüber, dass sich die Daten geändert haben. Er kann jetzt auf die Datenänderung reagieren, falls das erforderlich sein sollte. Wenn die Tabelle die Daten zu einem späteren Zeitpunkt anzeigen möchte, dann kann sie mit den bei setInput gespeicherten Daten aber nicht direkt etwas anfangen. So fordert sie eine Arrayrepräsentation der gepsicherten Daten durch Aufruf der Methode getElements an. Der IStructuredContentProvider agiert also im einfachsten Fall als Konverter für die Modelldaten. Ein kleines Beispiel soll die Anwendung des Prinzips zeigen. Sie finden den Quelltext in den Paketen tableViewer.person.viewer und tableViewer.person.common des Eclipse-Projektes zur Vorlesung. Das Beispiel verwaltet die Personenobjekte aus der Einführung in diesem Kapitel. Dazu implementiert PersonContentProvider die Schnittstelle IStructuredContentProvider mit ihren drei Methoden: getElements konvertiert die Modelldaten in ein Array mit Objekten. Jeder Eintrag des Arrays entspricht einer Tabellenzeile. In dispose kann der Konverter eventuell angelegte Ressourcen wieder freigeben, bevor er selbst freigegeben wird. 87 3.7 Model-View-Controller mit JFace inputChanged wird von dem TableViewer aufgerufen, wenn neue Daten in die Tabelle eingetragen werden. Im Beispiel ist lediglich die Konvertierungsmethode getElements von Interesse, da hier keine Ressourcen verwaltet und auch keine Änderungen gespeichert werden. public class PersonContentProvider implements IStructuredContentProvider { @Override public Object[] getElements(Object inputElement) { return ((List<Person>) inputElement).toArray(); } @Override public void dispose() { // keine Ressourcen angelegt } } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { // interessiert uns hier nicht } Jetzt kann der Konverter verwendet werden. Dazu wird eine ArrayList mit einigen Personenobjekten als unser Modell gefüllt. Danach wird der TableViewer im Layout angelegt. Er wiederum erzeugt intern die benötigte Table aus dem SWT. Abschließend erhält der TableViewer den Konverter übergeben. public class TableJFacePersonApplication extends ApplicationWindow { private TableViewer tableViewer; private ArrayList<Person> personModel = new ArrayList<Person>(); protected Control createContents(Composite parent) { personModel.add(new Person("Name 1", 42)); personModel.add(new Person("Name 2", 66)); tableViewer = new TableViewer(parent, SWT.FULL_SELECTION); tableViewer.setContentProvider(new PersonContentProvider()); } } // usw. Abbildung 3.89 zeigt, dass diese Schritte noch nicht ausreichen. 88 3.7 Model-View-Controller mit JFace Fertig? Nein: Abbildung 3.89: Ausgabe der JFace-Tabelle (noch unvollständig) Die Tabelle ist nicht zu sehen, weil JFace nicht automatisch die Spalten erzeugen kann. Dass muss dann doch wie bei SWT manuell geschehen. In JFace dient hierzu die Klasse TableViewerColumn, deren Objekte jeweils eine Spalte darstellen. Die Spalten erhalten als ersten Parameter wie immer das Vaterobjekt, was eigentlich immer der TableViewer ist. Der zweite Parameter bestimmt die Ausrichtung des Spalteninhaltes (rechts, links, zentriert). Table table = tableViewer.getTable(); // Fortsetzung der Methode createContents // aus dem vorherigen Quellcode. TableViewerColumn column; column = new TableViewerColumn(tableViewer, SWT.LEFT); column.getColumn().setText("Name"); column.getColumn().setWidth(200); column = new TableViewerColumn(tableViewer, SWT.RIGHT); column.getColumn().setText("Age"); column.getColumn().setWidth(80); table.setHeaderVisible(true); table.setLinesVisible(true); // sichtbare Spaltentitel // sichtbare Linien Auch das ist noch nicht ausreichend. Die Tabelle erhält durch den Konverter zwar ein Array aller Modellobjekte, kann aber nicht selbständig entscheiden, wie die Attribute der einzelnen Objekte in den Zeilen dargestellt werden sollen. 3.7.2.2 View Nachdem der vorherige Abschnitt beschrieben hat, wie die Tabelle erzeugt wird und die wie das Modell die Daten bereitstellt, soll jetzt die Darstellung der Modelldaten untersucht werden. 89 3.7 Model-View-Controller mit JFace Table TableColumn stellt dar 1 stellt dar 1 TableViewerColumn holt Daten über 1 SWT <<abstract>> CellLabelProvider TableViewer 1 gehört zu eingeben <<abstract>> EditingSupport stellt dar 1 1 <<interface>> IContentProvider <<interface>> IStructuredContent Provider MyEditingSupport verwendet 1 ColumnLabelProvider <<abstract>> CellEditor MyContentProvider MyLabelProvider TextCellEditor ModelData Eingabe Modell Darstellung Abbildung 3.90: Ansichtsklassen für die JFace-Tabelle Die Idee hier ist auch nicht sonderlich kompliziert. Für jede Spalte muss ein Formatierer bereitgestellt werden, der aus einem (oder mehreren) Attributen der Modellklasse einen String erzeugt, der dann in genau dieser Zelle angezeigt wird. Man kann sich leicht vorstellen, dass beispielsweise Datumswerte, die im Modell als Date-Objekte abgelegt sind, ganz unterschiedlich angezeigt werden können: Soll es in einem kurzen oder langen Format dargestellt werden bzw. welche länderspezifische Einstellungen ist vorhanden? Auch hier wird wieder nur ein Ansatz betrachtet, der ab Eclipse 3.3 funktioniert. Für ältere Versionen existieren ebenso Lösungen, die aber durch neuere überflüssig geworden sind. Jeder Formatierer ist eine Klasse, die von CellLabelProvider erbt. Die Klasse besitzt einige Methoden, die überschrieben werden können. getFont liefert den Zeichensatz zurück, mit dem die Werte in dieser Spalte dargestellt werden sollen. getBackground gibt die Hintergrundfarbe für die Spaltenwerte zurück. getForeground bestimmt die Vordergrundfarbe für die Spaltenwerte. getImage liefert als Ergebnis ein Bildobjekt zur Darstellung der Werte. getText ist sicherlich die wichtigste Methode. Sie gibt den fertig formatierten String zurück, der den Wert eines oder mehrerer Attribute des Modells darstellt. Alle Methoden bekommen das Modellobjekt als einzigen Parameter übergeben. Es müssen nur die Methoden überschrieben werden, die für die Anwendung relevant sind. Soll beispielsweise kein Bild verwendet werden, dann muss getImage nicht überschrieben. Das gilt auch für die Farben und den Zeichensatz. Diese Methoden sind nur dann erforderlich, wenn die Vorgaben abgewandelt werden sollen. Der Entwickler erstellt für jede Spalte einen eigenen Formatierer und registriert ihn an dieser. Das Beispiel mit den Personen lässt sich so fortsetzen, wobei statt separater Klassen auch anonyme innere Klassen möglich sind. Der Name der Person wird aus dem übergebenen Objekt entnommen. Dazu muss das Datenobjekt in ein Person-Objekt „gecastet“ werden, so dass das Namensattribut ausgelesen werden kann. 90 3.7 Model-View-Controller mit JFace // für den Namen einer Person public class PersonNameLabelProvider extends ColumnLabelProvider { public String getText(Object element) { return ((Person) element).getName(); } } Das Alter der Person ist ein ganzzahliger Wert. Er muss in einen String umgewandelt werden. // für das Alter einer Person public class PersonAgeLabelProvider extends ColumnLabelProvider { public String getText(Object element) { return String.valueOf(((Person) element).getAge()); } } Jetzt müssen die beiden Formatierer noch an den Spalten registriert werden. column ist eine Spalte der Klasse TableViewerColumn aus dem Codefragment direkt nach Abbildung 3.89. Wichtig ist, nach dem Registrieren noch die Modelldaten dem TableViewer zu übergeben. // ... // An den Spalten registrieren, hier exemplarisch // für die Namensspalte: column.setLabelProvider(new PersonNameLabelProvider()); // Danach müssen die Modelldaten dem TableViewer // übergeben werden. tableViewer.setInput(personModel); Jetzt sieht die Darstellung schon ganz gut aus (Abbildung 3.91). Abbildung 3.91: Ausgabe der JFace-Tabelle mit Formatierer Dynamische Spaltenbreiten Was passiert, wenn der Anwender die Größe des Dialogs verändert? Die Spaltenbreiten der Tabelle passen sich nicht an, so dass neben der Tabelle im Falle der Vergrößerung ungenutzter Platz entsteht. Abbildung 3.92 zeigt das Problem. 91 3.7 Model-View-Controller mit JFace Dynamische Spaltenbreiten? Abbildung 3.92: Fehlende dynamische Spaltenanpassung bei einer Tabelle Zur Lösung des Problems besitzt JFace den speziell für diese Tabellen entworfenen Layout-Manager TableColumnLayout. Es ist in der Lage, die Spalten einer Tabelle automatisch an den zur Verfügung stehenden Platz anzupassen. Dazu muss allerdings die Tabelle das alleinige Widget in einem Composite mit TableColumnLayout sein. Dann werden die Spaltenbreiten entweder absolut in Pixeln durch Objekte der Klasse ColumnPixelData oder prozentual bezogen auf den zur Verfügung stehenden Platz durch Objekte der Klasse ColumnWeightData festgelegt. Es können auch minimale Spaltenbreiten erzwungen werden, damit bestimmte Inhalte immer sichtbar sind. Die erforderlichen Attribute zur Steuerung des Verhaltens lassen sich entweder direkt setzen oder aber schon im Konstruktor der jeweiligen Klasse übergeben. Die APIDokumentation zeigt die Details. Die Implementierung TableJFacePersonResizableColumnsApplication erweitert den aktuellen Stand der Personenanwendung TableJFacePersonApplication so, dass Spalten prozentuale Breiten zugewiesen bekommen und dass sich die Spalten dynamisch anpassen. Zunächst muss die Tabelle jetzt in einem separaten Composite-Objekt erzeugt werden, das die Tabelle mittels TableColumnLayout anordnet. Composite tableComp = new Composite(parent, SWT.NONE); TableColumnLayout tcl = new TableColumnLayout(); tableComp.setLayout(tcl); tableViewer = new TableViewer(tableComp, SWT.FULL_SELECTION); Die Spaltenbreiten werden direkt dem Layout und nicht den Spalten selbst übergeben. In diesem Beispiel sollen relative Spaltenbreiten verwendet werden, die sich bei dynamischer Breitenänderung automatisch anpassen. Eine Minimalbreite wird nicht erzwungen. // SWT-Tabelle auslesen Table table = tableViewer.getTable(); // erste Spalte erzeugen (linksbündig) TableViewerColumn column = new TableViewerColumn(tableViewer, SWT.LEFT); // Spaltentitel eintragen column.getColumn().setText("Name"); // Die Spalte belegt immer 70% der Platzes. // Eine Größenanpassung ist erlaubt. tcl.setColumnData(column.getColumn(), new ColumnWeightData(70, true)); Jetzt simmt das Verhalten (Abbildung 3.93). 92 3.7 Model-View-Controller mit JFace Abbildung 3.93: Korrekte dynamische Spaltenanpassung bei einer Tabelle Sortierung In vielen Anwendungen wie beispielsweise dem Explorer unter Windows oder dem Dateimanager und LinuX kann der Anwender die Tabellendaten anhand der Werte einer Spalte sortiert anzeigen, indem er auf den entsprechenden Spaltentitel klickt (siehe Abbildung 3.94). Klick auf Namen-Header Klick auf Alter-Header Abbildung 3.94: Sortierung einer Tabelle Solch ein Verhalten lässt sich auch für JFace-Tabellen implementieren. Dazu muss der Entwickler der Tabelle im Wesentlichen vorgeben, wie einzelne Werte in den Spalten verglichen werden sollen. Da Spalten ja ganz unterschiedliche Werte darstellen, kann JFace das nicht automatisch für alle möglichen Datentypen selbst durchführen. Der Ablauf sieht so aus: Ein „Sortierer“ (eigentlich handelt es sich eher um einen Vergleicher) erbt von der Basisklasse ViewerSorter und überschreibt deren compare-Methode. Diese Methode wird von der Tabelle immer aufgerufen, um paarweise zwei Spaltenwerte miteinander zu vergleichen. Der „Sortierer“ muss darüber hinaus in der Lage sein, sowohl aufsteigende als auch absteigende Sortierungen zu unterstützen. Das erwartete Verhalten besteht nämlich darin, bei jedem Klick auf denselben Spaltentitel die Sortierreihenfolge umzukehren. Der „Sortierer“ wird am TableViewer registriert. Schließlich fehlt noch die Auslösung der Sortierung bzw. die Umkehr der Sortierreihenfolge. Dazu wird ein Beobachter (ein SelectionListener) an jedem Tabellentitel registriert. Dieser teilt dem „Sortierer“ mit, anhand welcher Spalte sortiert werden soll und aktualisiert anschließend die Tabellendarstellung. Der Sortierer muss 93 3.7 Model-View-Controller mit JFace intern unterscheiden, ob direkt hintereinander anhand derselben Spalte sortiert wird. Dann muss er jedes Mal die Reihenfolge umdrehen. Prinzipiell kann für jede Spalte ein eigener „Sortierer“ erstellt werden. Im folgenden Beispiel dagegen kann der „Sortierer“ für alle Spalten verwendet werden, weil das Modell nicht sehr komplex ist. Die Klasse PersonSorter stellt in diesem Beispiel den „Sortierer“ dar. public class PersonSorter extends ViewerSorter { // mögliche Sortiereihenfolgen: aufsteigend // und absteigend public enum Direction { ASCENDING, DESCENDING }; // Aktuelle Spalte, anhand derer sortiert wurde. // Bleibt bei zwei Aufrufen die Spalte gleich, // dann ändert sich die Sortierreihenfolge. private int sortColumn; // Aktuelle Sortierreihenfolge private Direction sortDirection = Direction.DESCENDING; // Diese Methode wird bei jedem Klick in einen Spaltentitel // vom Beobachter aufgerufen. Wenn sich die Spalte ändert, // wird immer aufsteigend sortiert. Ansonsten wird die // Sortierreihenfolge umgedreht. public void doSort(int column) { if (column == sortColumn) { sortDirection = (sortDirection == Direction.ASCENDING) ? Direction.DESCENDING : Direction.ASCENDING; } else { sortColumn = column; sortDirection = Direction.ASCENDING; } } // Diese Methode wird von der Tabelle aufgerufen, um // paarweise zwei Werte einer Spalte miteinander zu // vergleichen. Die Methode verhält sich bezüglich der // Rückgabewerte exakt so wie die entsprechende Methode // der Java-Klasse java.util.Comparator. public int compare(Viewer viewer, Object object1, Object object2) { int result = 0; Person person1 = (Person) object1; Person person2 = (Person) object2; switch (sortColumn) { case PersonIndices.NAME_INDEX: result = getComparator().compare(person1.getName(), person2.getName()); break; case PersonIndices.AGE_INDEX: result = person1.getAge() - person2.getAge(); break; } result = sortDirection == Direction.DESCENDING ? result : -result; 94 3.7 Model-View-Controller mit JFace } } return result; Der „Sortierer“ kann in seiner Standardimplementierung auch Objekte zu Gruppen (Kategorien) zusammenfassen und innerhalb der Gruppen sortieren. Nähere Informationen liefert die API-Dokumentation. Jetzt muss der „Sortierer“ am TableViewer registriert werden. final PersonSorter sorter = new PersonSorter(); tableViewer.setSorter(sorter) Abschließend darf der Beobachter nicht fehlen, damit die Klicks auf die Spaltentitel auch erfasst werden. Das Beispiel zeigt nur die Anmeldung an der Namens-Spalte. column.getColumn().addSelectionListener( new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // Zu sortierende Spalte in den "Sortierer" eintragen. sorter.doSort(PersonIndices.NAME_INDEX); // Tabelle zwingen, sich neu zu zeichnen. tableViewer.refresh(); } }); Filterung Enthalten Tabellen sehr viele Zeilen, dann möchte der Anwender eventuell irrelevante Informationen ausblenden. Genau dazu dient der Filter. Im Windows-Explorer wird diese Eigenschaft unter Anderem dazu benutzt, um versteckte Dateien zu verbergen. Der prinzipielle Ablauf zur Filterung sieht so aus: Der Entwickler schreibt eine eigene Klasse, die von der abstrakten Basisklasse ViewerFilter erbt. In dieser Klasse überschreiben er die Methode select, die bestimmt, ob ein Objekt (z.B. eine Person) angezeigt werden soll oder nicht. Diese Methode wird von der Tabelle für jede Zeile aufgerufen. JFace-Tabellen unterstützen die gleichzeitige Verwendung beliebig vieler Filter. Der Entwickler kann also alle Filter am TableViewer mit setFilters registrieren. Die Tabelle befragt für jede Zeile alle Filter, ob das entsprechende Objekt angezeigt werden soll oder nicht. Virtuelle Tabellen Ebenso wie SWT unterstützt auch JFace virtuelle Tabellen, um große Datenmengen effizient handhaben zu können. Der bisher in Abschnitt 3.7.2.1 vorgestellte Ansatz mit dem IStructuredContentProvider funktioniert dafür aber nicht mehr. Statt dessen kann der ILazyContentProvider eingesetzt werden, der nicht mehr alle Modelldaten, sondern einzelne Zeilen zurückliefert. Um hier auch ein eventuell zeitaufwändiges Sortieren und Filtern zu ermöglichen, kann der DeferredContentProvider bei solchen Tabellen das Sortieren und Filtern in einem Hintergrundthread vornehmen. 95 3.7 Model-View-Controller mit JFace Gestaltung der Zellenwerte Möchte der Entwickler das Aussehen der Zellinhalte noch viel freier gestalten, als es der CellLabelProvider zulässt, dann muss er den StyledCellLabelProvider einsetzen und das Zeichnen des Zelleninhaltes selbst übernehmen. Eine weitere Möglichkeit besteht darin, andere Widgets zur Darstellung der Zellinhalte zu verwenden. Das ist etwas komplizierter und soll hier nicht besprochen werden. 3.7.2.3 Eingabe JFace verwendet intern einen Controller, der Modell und Ansicht steuert. Daher finden Sie an dieser Stelle auch keinen Abschnitt zum Controller. Statt dessen wird diskutiert, wie JFace-Tabellen auch Benutzereingaben unterstützen. Table TableColumn stellt dar 1 stellt dar 1 TableViewerColumn holt Daten über 1 SWT <<abstract>> CellLabelProvider TableViewer 1 gehört zu eingeben <<interface>> IContentProvider <<abstract>> EditingSupport stellt dar 1 1 <<interface>> IStructuredContent Provider MyEditingSupport verwendet 1 ColumnLabelProvider <<abstract>> CellEditor MyContentProvider MyLabelProvider TextCellEditor ModelData Eingabe Modell Darstellung Abbildung 3.95: Eingabe in eine JFace-Tabelle Die einfachste Art und Weise, einen Editor anzugeben, besteht darin, einen vorhandenen Editor aus JFace zu verwenden. Für die wichtigsten Eingabearten sind bereits Editoren vordefiniert: TextCellEditor: Er erlaubt die einzeilige Eingabe in ein Textfeld. Mit einer geeigneten Konvertierung kann hier von Zahlen über normale Bezeichner und Datumswerte nahezu jeder Datentyp im Modell abgebildet werden. CheckboxCellEditor: Damit kann der Anwender einen Boole’schen Wert auswählen. Der Datentyp im Modell ist dabei in der Regel ein boolean. ComboBoxCellEditor: Mit der Combobox lässt sich ein Wert aus einer Menge von Vorgaben durch ein CCombo-Widget auswählen. Im Gegensatz zu einem Textfeld kann also die Wahlfreiheit eingeschränkt werden. ColorCellEditor: Der Anwender wählt eine Farbe durch einen Dialog aus. Im Modell kann so ein Color-Objekt manipuliert werden. DialogCellEditor: Sind komplexere Eingaben erforderlich, dann kann ein eigener Dialog dem Anwender diese ermöglichen. So lassen sich beispielsweise zusammengesetzte Modell-Attribute durch mehrere Felder im Dialog erfassen. Die Anzeige 96 3.7 Model-View-Controller mit JFace des Wertes in der Tabelle übernimmt normalerweise ein Label. Durch Druck auf eine zusätzliche Taste innerhalb der Zelle wird der Auswahldialog gestartet. Weitere Editoren können durch Überschreiben von CellEditor selbst implementiert werden. Die Arbeit mit einem Editor skizziert die Abbildung 3.96. viewer TableViewer editor ComboBoxCellEditor editingSupport EditingSupport Darf das Attribut, für das der Editor registriert ist, in dem Objekt object verändert werden? 1.1: canEdit(object) 1.2: getCellEditor() 1.3: getValue(object) 1.4: doSetValue(value) Eingabe beendet 2.1: doGetValue() Wert in den Editor eintragen Wert aus dem Editor auslesen CellEditor für den Wert auslesen. Attributwert aus dem Objekt object lesen In das Attribut des Objektes object, für das der Editor registriert, den neuen Wert value eintragen 2.2: setValue(object, value) Abbildung 3.96: Interaktion mit einem Editor in einer JFace-Tabelle Die Klasse EditingSupport ist hier die zentrale Klasse. Sie hat vier wesentliche Aufgaben: 1. Sie teilt der Tabelle mit, ob ein Attribut überhaupt bearbeitet werden kann. Vor der eigentlichen Interaktion ruft die Tabelle daher die Methode canEdit auf. Nur wenn eine Bearbeitung zugelassen ist, dann sind die folgenden Punkte relevant. 2. EditingSupport stellt der Tabelle den eigentlichen Editor zur Verfügung. Dazu ruft die Tabelle die Methode getCellEditor auf und übergibt das zu bearbeitende Modellobjekt. 3. Damit die Tabelle den Editor mit dem richtigen Modellwert starten kann, benötigt sie diesen Wert. Er wird vom EditingSupport bereitgestellt, indem seine Methode getValue aufgerufen wird. 4. Nach Beendigung der Eingabe durch den Anwender muss der neue Wert wiederum in das Modellobjekt eingetragen werden. Hierzu ruft die Tabelle die Methode setValue der Klasse EditingSupport auf. Somit sieht der typische Interaktionsablauf mit dem EditingSupport anhand des Diagramms 3.96 so aus: 1.1 Die Tabelle fragt im EditingSupport nach, ob der Anwender den Wert überhaupt verändern darf. 1.2 Ist die Eingabe zulässig, dann fordert die Tabelle den eigentlichen Editor an, mit dessen Hilfe der Anwender seine Daten verändern kann. 97 3.7 Model-View-Controller mit JFace 1.3 Anschließend fragt die Tabelle den EditingSupport nach dem zu bearbeitenden Attribut und übergibt dazu das komplette Modellobjekt (die Zeile). 1.4 Den erhaltenen Wert trägt die Tabelle in den Editor ein und startet die Bearbeitung durch den Anwender. 2.1 Ist die Bearbeitung abgeschlossen, dann liest die Tabelle den neuen Wert aus dem Editor aus. 2.2 Jetzt muss nur noch der neue Wert wieder in das Modellobjekt kommen. Dazu verwendet die Tabelle wiederum den EditingSupport, der den neuen Wert sowie das Modellobjekt übergeben bekommt und den Wert in das Objekt einträgt. Mein Lesen der Erklärungen fällt vielleicht auf, dass nirgendwo eine Möglichkeit beschrieben wurde, wie der EditingSupport herausfinden kann, welche Spalte und damit welches Attribut bearbeitet werden soll. Das ist ziemlich einfach zu beantworten: Die API sieht die Unterscheidung nicht vor. Im einfachsten Fall wird daher für jede Spalte ein eigener EditingSupport geschrieben. Damit ist die Zuordnung wieder eindeutig. Dieser Abschnitt soll noch mit einem kleinen Beispiel abgeschlossen werden. Zunächst bekommt die Person aus den vorherigen Beispielen ein zusätzliches Attribut, damit später auch eine Combobox zur Eingabe verwendet werden kann. Das Attribut nimmt den Beschäftigungszustand der Person in Form eines Aufzähltyps auf. public class Person { public enum Status { STUDYING, WORKING, RETIRED, DEAD }; } private private private // usw. String name; int age; Status status; (Getter, Setter) Abbildung 3.97 zeigt das angestrebte Ergebnis. Abbildung 3.97: Anzeige und Eingabe in eine JFace-Tabelle Der erste EditingSupport handhabt die Eingabe des Namens durch ein Textfeld. Somit wird als Editor TextCellEditor eingesetzt. public class PersonNameEditingSupport extends EditingSupport { // Zur Bearbeitung des Namens wird ein Text-Widget verwendet. private TextCellEditor cellEditor; public PersonNameEditingSupport(TableViewer viewer) { super(viewer); // Das Vaterelement des Editors ist die Tabelle. 98 3.7 Model-View-Controller mit JFace } cellEditor = new TextCellEditor(viewer.getTable()); // Der Name kann immer bearbeitet werden. protected boolean canEdit(Object element) { return true; } // Rückgabe des eigentlichen Editors. protected CellEditor getCellEditor(Object element) { return cellEditor; } // Das Namensattribut des Objektes auslesen. protected Object getValue(Object element) { return ((Person) element).getName(); } } // Den neuen Wert in das Namensattribut eintragen und // den TableViewer veranlassen, die Zeile mit dieser // Person neu zu zeichnen. protected void setValue(Object element, Object value) { ((Person) element).setName((String) value); getViewer().update(element, null); } Jetzt muss der Editing-Support noch an der Spalte registrieren, für die er die Steuerung der Eingaben übernimmt. tableColumn.setEditingSupport( new PersonNameEditingSupport(tableViewer)); Der Beschäftigungsstand wird durch ein Label angezeigt. Möchte der Anwenden diesen ändern, dann erscheint als Editor eine Combobox (Klasse ComboBoxCellEditor), die alle Auswahlmöglichkeiten des Aufzähltyps anbietet. Wichtig ist hier zu wissen, dass die Tabelle als eingegebenen Wert immer den ausgewählten Index der Combobox übergibt. public class PersonStatusEditingSupport extends EditingSupport { // Zur Bearbeitung des Status wird intern // ein CCombo-Widget verwendet. private ComboBoxCellEditor cellEditor; public PersonStatusEditingSupport(TableViewer viewer) { super(viewer); // Der Benutzer darf keinen Wert manuell in // die Combobox eingeben. Es ist lediglich eine // Auswahl aus den vorhandenen Werten möglich. cellEditor = new ComboBoxCellEditor(viewer.getTable(), person.getStatusValuesAsStringArray(), SWT.READ_ONLY); } // Auch der Status kann immer bearbeitet werden. protected boolean canEdit(Object element) { return true; } // Rückgabe des eigentlichen Editors. protected CellEditor getCellEditor(Object element) { 99 3.8 Weitere Container } return cellEditor; // Den Wert des Statusattributes auslesen und // als Index innerhalb der Combobox zurueck geben. protected Object getValue(Object element) { return ((Person) element).getStatus().ordinal(); } } // Den neuen Wert in das Statusattribut eintragen und // den TableViewer veranlassen, die Zeile mit dieser // Person neu zu zeichnen. Die Methode setStatus // der Personenklasse nimmt die Konvertierung // des Index aus der Combobox in den Aufzählwert vor. protected void setValue(Object element, Object value) { ((Person) element).setStatus((Integer) value); getViewer().update(element, null); } Der EditingSupport muss noch an der Spalte registriert werden. Das vollständige Beispiel, das auch die Eingabe des Altersattributes unterstützt, ist in den beiden Paketen tableViewer.person.editing und tableViewer.person.common des Quelltextes zur Vorlesung zu finden. 3.7.3 Attributierter Text ohne MVC Dieses Kapitel folgt, wenn alle anderen, die für die Vorlesung relevant sind, fertiggestellt sind. 3.7.4 Attributierter Text mit MVC Dieses Kapitel folgt, wenn alle anderen, die für die Vorlesung relevant sind, fertiggestellt sind. 3.8 Weitere Container 100 4 Anbindung an die Geschäftslogik 5 Deklarative Beschreibungen 6 Eclipse Rich Client Platform 7 Multithreading in Java Java besitzt eine sehr mächtige und umfangreiche API zur Erstellung multithreadingfähiger Programme. In Java 5 wurde diese API nochmals erweitert. In den folgenden Abschnitten werden nur die wichtigsten Grundfunktionalitäten vorgestellt. Die Pakete java.util.concurrent und deren Unterpakete werden hier aus Platzgründen ebenso wenig betrachtet wie viele andere Eigenschaften von Threads. 7.1 Threads erzeugen und starten Ein Thread ist ein Objekt der Klasse Thread. Im Wesentlichen wird ein Thread auf zwei verschiedene Arten erzeugt: 1. Eine Klasse erbt von Thread und überschreibt die Methode run. Diese implementiert die (Endlos-)Schleife des Threads und wird beim Start aufgerufen. 2. Eine Klasse implementiert die Schnittstelle Runnable und überschreibt ebenso die Methode run. Ein Objekt der Klasse wird dem Konstruktor von Thread übergeben. Beispiel: public class MyThread extends Thread { public void run() { while (true) { // Implementierung } } } Der Thread wird schließlich mit start gestartet. Beispiel: MyThread mThr = new MyThread(); mThr.start(); Die Thread-Priorität liegt zwischen MIN_PRIORITY (0) und MAX_PRIORITY (10). Im Standardfall beträgt sie NORM_PRIORITY (5). 7.2 Threads beenden und löschen 7.2 Threads beenden und löschen Die einzige in aktuellen Java-Versionen unterstützte Möglichkeit, einen Thread zu beenden, besteht darin, dass der Thread seine run-Methode selbst verlässt. Ein Problem tritt auf, wenn der Thread durch einen anderen terminiert werden soll. Als Ausweg kann einem Thread ein Unterbrechungssignal gesendet werden. Der ausführende Thread testet zyklisch auf das Vorhandensein des Signals und beendet sich selbst. Beispiel: public class MyThread extends Thread { public void run() { while (true) { if (interrrupted()) { return; // beenden } // ... } } } Dem Thread wird mit interrupt das Signal zur Unterbrechung geschickt. Beispiel: MyThread mThr = new MyThread(); mThr.start(); // ... mThr.interrupt(); Ein Thread läuft häufig nicht permanent sondern legt sich für eine gewisse Zeit „schlafen“. Aus diesem Schlafzustand wird er ebenso mit interrupt geweckt. Um zwischen einem „normalen“ Aufwecken aus dem Schlafzustand und einem Signal zur Beendigung der Arbeit unterscheiden zu können, lässt sich der Thread sehr einfach durch Hinzufügen einer Boole’schen Variablen erweitern. Beispiel (Thread): public class MyThread extends Thread { private boolean terminated = false; public void run() { while (!terminated) { // Aufwecken aus dem Schlafzustand try { sleep(2000); } catch (InterruptedException ex) { if (!terminated) { // Arbeit nach dem Wecken } } // Arbeiten // Test auf Aufwecken während der Arbeit if (interrupted() && terminated) { return; } // Arbeiten } } 105 7.3 Zugriff auf gemeinsam genutzte Ressourcen } public void terminate() { terminated = true; interrupt(); } Der Aufruf der terminate-Methode teilt dem Thread mit, dass er sich beenden soll. Dabei ist zu beachten, dass ein aktiver Thread unter Umständen erst nach einer gewissen Zeit das Stopsignal sieht. Es ist daher sinnvoll, die Variable terminated in regelmäßigen Abständen zu überprüfen. 7.3 Zugriff auf gemeinsam genutzte Ressourcen In Java dürfen verschiedene Thread nur unter sehr genau definierten Bedingungen gemeinsam auf Variablen zugreifen. Sicher ist immer der synchronisierte Fall, wie er in Abschnitt 7.4 vorgestellt wird. Allerdings kostet die Synchronisation sehr viel Zeit. Glücklicherweise gibt es bestimmte Szenarien, in denen sie nicht erforderlich ist. Beispielsweise ist der Zugriff auf Refenrenzen sowie alle primitiven Datentypen außer long und double atomar. Das heißt, dass während eines solchen Zugriffs kein Kontextwechsel erfolgt. Damit könnte die Diskussion eigentlich beendet sein. Problem treten aber besonders auf Prozessoren mit mehreren Kernen auf. Es zwar garantiert, dass die Zugriffe unteilbar sind. Es ist aber nicht garantiert, dass ein Thread, der nach dem Schreiben lesend auf die Variable zugreift, die Änderung überhaupt sehen wird. Der Quelltext unterstellt die sogenannte „Sequential Consistency“. Damit ist gemeint, dass ein Thread die Änderungen eines anderen Threads, die dieser vorher vorgenommen hat, sehen kann. Solch ein Modell wird von der virtuellen Maschine aber gar nicht unterstützt. Das hängt damit zusammen, dass jeder Thread seinen eigenen Speicher besitzt. Dieser kann sich im Falle von Mehrkern-Prozessoren durchaus auch auf verschiedenen Prozessorkernen befinden. Aus Geschwindigkeitsgründen werden diese separaten Speicher nicht nach jedem Zugriff untereinander abgeglichen. Der lokale Speicher wird nur dann automatisch mit dem Hauptspeicher synchronisert, wenn sich der Thread beendet. Das ist in der Regel aber viel zu spät. Für ein manuelles Zurückschreiben der Änderungen reicht es aus, eine gemeinsam genutzte Variable als volatile zu kennzeichnen. volatile int sharedMemory = 42; Beschreibt ein Thread solch eine Variable, dann wird sein gesamter Zustand inklusive der als volatile deklarierten Variablen in den Hauptspeicher zurück geschrieben. Liest ein Thread dagegen solch eine Variable, wird zunächst der Inhalt der Variablen aus dem Hautpspeicher erneut gelesen, weil er sich inzwischen ja geändert haben kann. Dabei wird aber nicht nur der Inhalt dieser einen Variable gelesen, sondern es wird der gesamte Arbeitsspeicher des Threads aufgefrischt. Der Aufwand beim Zugriff auf volatileVariablen ist also nicht zuvernachlässigen. Es ist aber deutlich geringer als bei manueller Synchronisation (siehe Abschnitt 7.4). Statt Variablen, die als volatile deklariert werden, können seit Java 5 auch Klassen aus dem Paket java.util.concurrent.atomic verwendet werden. Diese kapseln primitive Daten mit thread-sicherem Zugriff und bieten darüber hinaus auch weitere Operationen auf den Daten an (z.B. getAndIncrement). 106 7.4 Synchronisation von Threads Bei Konstanten ist der Fall einfacher: Bei ihrer Initialisierung werden die Daten im Hauptspeicher aktualisiert, und alle Thread können dann problemlos darauf zugreifen, weil der erste Zugriff ein Auffrischen des thread-eigenen Speichers bewirkt. 7.4 Synchronisation von Threads Zur einfachen Synchronisation von Threads existieren in der Basisklasse aller Klassen Object mehrere Methoden. Das Objekt, auf dem die Methoden aufgerufen werden, dient dabei als Monitor. Alle folgenden Methoden sollten nur innerhalb synchronisierter Blöcke oder Methoden, die den Monitor als Synchronisationsobjekt verwenden, aufgerufen werden. Synchronisierte Methoden und Blöcke werden in den Folgeabschnitten beschrieben. Wird die Synchronisation verwendet, dann ist sichergestellt, dass die beteiligten Threads auch auf Mehrkern-Prozessoren immer mit den richtigen Speicherwerten arbeiten, weil jede manuelle Synchronisation automatisch ein Rückschreiben und eine Auffrischung der Thread-Daten erzwingt. wait: Beim Aufruf der Methode wird der gerade aktive Thread an diesem Synchronisationsobjekt blockiert. Optional ist die Angabe einer maximalen Wartezeit. Da diese Methode nur innerhalb eines synchronisierten Abschnitts (Block oder Methode) aufgerufen werden soll, wird beim Blockieren der Abschnitt wieder freigegeben. Sobald der Thread deblockiert wird, wird er wieder Eigentümer des Abschnitts und blockiert ihn somit. wait löst eine InterruptedException aus, wenn der wartende Thread ein Unterbrechungssignal während seines Wartens empfangen hat. notify: Ein am Monitor wartender Thread wird deblockiert. Im Falle mehrerer an diesem Monitor wartender Threads hängt es von der Implementierung der virtuellen Maschine ab, welcher Thread deblockiert wird. notifyAll: Alle am Monitor wartenden Threads werden wieder freigegeben. Die Threads bekommen der Reihe nach den synchronisierten Abschnitt zugewiesen, so dass sichergestellt ist, dass sich immer nur ein Thread in dem kritischen Abschnitt befindet. In Java 5 wurden weitere Klassen zur Synchronisation eingeführt. Diese befinden sich im Paket java.util.concurrent und dessen Unterpaketen. 7.4.1 Synchronisierte Methoden Sie stellen sicher, dass nur ein Thread zu einem Zeitpunkt die Methode betreten kann. Beispiel für synchronisierte Methoden: public class Timer { // ... public synchronized void set(long time) { /* ... */ } public synchronized void set(Date date) { /* ... */ } } 107 7.5 Threads als Daemons Befinden sich mehrere Methoden mit dem Merkmal synchronized in einer Klasse, so ist sichergestellt, dass immer nur eine dieser Methoden zu einem Zeitpunkt aufgerufen werden kann. Die Methode verwendet das Objekt, auf dem sie aufgerufen werden, als Monitor. Die set-Methode aus dem vorherigen Beispiel entspricht somit: public void set(long time){ synchronized (this) { /* ... */ } Solche synchronisierten Blöcke sind im Folgeabschnitt beschrieben. Wird eine statische Methode als synchronized markiert, so wird als Monitor das Klassenobjekt der Klasse verwendet (Klasse java.lang.Class). So ist sichergestellt, dass nur eine synchronisierte, statische Methode der Klasse zu einem Zeitpunkt ausgeführt werden kann, 7.4.2 Synchronisierte Blöcke Es handelt sich um Monitore, die einen gegenseitigen Ausschluss mehrerer Threads in diesem Block bewirken. Die Synchronisation findet immer an einem Objekt statt, wobei das Objekt als der Monitor agiert. Beispiel, in dem object eine Referenz auf das Monitorobjekt ist: synchronized (object) { // Kritischer Abschnitt } 7.5 Threads als Daemons Ein Thread wird durch den Aufruf der Methode setDaemon als so genannter Daemon („Dämon“) markiert. Dieses hat eine Auswirkung auf die Lebensdauer der Anwendung. Normalerweise beendet die JVM (Java Virtual Machine) das laufende Programm erst dann automatisch, wenn alle Threads beendet wurden. Die Ausnahme sind die Daemons, die hierzu nicht berücksichtigt werden. So bietet es sich an, gewisse Teilaufgaben, die als Hintergrundthreads implementiert sind, als Daemons zu markieren. Sie haben keinen Einfluss auf die Lebensdauer des Programmes und müssen daher auch nicht manuell beendet werden. Im Zusammenhang mit Swing stellt sich die Frage der Beendigung häufig nicht, da der Event-Thread ohnehin nicht als Daemon läuft und so eine direkte Beendigung der Anwendung durch das Verlassen der main-Methode verhindert. 108 8 Abbildungsverzeichnis 2.1 2.2 2.3 2.4 2.5 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10 3.11 3.12 3.13 3.14 3.15 3.16 3.17 3.18 3.19 3.20 3.21 3.22 3.23 3.24 3.25 3.26 3.27 Kommunikation bei einem Thin-Client . . . . . . . . . . . Kommunikation bei einem Rich Thin Client . . . . . . . . . Kommunikation bei einem Rich Fat Client . . . . . . . . . Architektur einer Fat-Client-Anwendung . . . . . . . . . . Architekturschichten anhand eines Beispiels . . . . . . . . Struktur der Oberfläche . . . . . . . . . . . . . . . . . . . Beispiel unter Windows 7, MacOS X und Linux (GTK) . . Struktur einer SWT-Anwendung . . . . . . . . . . . . . . . Zielplattform einstellen . . . . . . . . . . . . . . . . . . . . Zielplattform einrichten . . . . . . . . . . . . . . . . . . . . Abhängigkeit von Dateien der Zielplattform . . . . . . . . Struktur einer JFace-Anwendung . . . . . . . . . . . . . . Windows 7 . . . . . . . . . . . . . . . . . . . . . . . . . . LinuX, GNOME 2.28.2 . . . . . . . . . . . . . . . . . . . . Windows XP . . . . . . . . . . . . . . . . . . . . . . . . . MacOS X . . . . . . . . . . . . . . . . . . . . . . . . . . . Englische Texte . . . . . . . . . . . . . . . . . . . . . . . . Deutsche Texte . . . . . . . . . . . . . . . . . . . . . . . . Vergrößerter Dialog . . . . . . . . . . . . . . . . . . . . . Verkleinerter Dialog . . . . . . . . . . . . . . . . . . . . . Hierarchie Button . . . . . . . . . . . . . . . . . . . . . . Hierarchie Label . . . . . . . . . . . . . . . . . . . . . . . Anordnung im Fill-Layout . . . . . . . . . . . . . . . . . . . Horizontale Anordnung im Fill-Layout . . . . . . . . . . . . Manuelle Verkleinerung im Fill-Layout . . . . . . . . . . . Vertikale Anordnung im Fill-Layout . . . . . . . . . . . . . Anordnung im Row-Layout . . . . . . . . . . . . . . . . . . Horizontale Anordnung im Row-Layout . . . . . . . . . . . Manueller Verkleinerung im Row-Layout (ohne Umbruch) Manueller Verkleinerung im Row-Layout (mit Umbruch) . . Vertikale Anordnung im Row-Layout . . . . . . . . . . . . Row-Layout mit veränderter Widget-Breite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 8 9 11 12 14 16 17 19 19 20 21 25 25 25 25 26 26 26 26 27 27 27 28 28 28 29 29 30 30 30 32 Abbildungsverzeichnis 3.28 3.29 3.30 3.31 3.32 3.33 3.34 3.35 3.36 3.37 3.38 3.39 3.40 3.41 3.42 3.43 3.44 3.45 3.46 3.47 3.48 3.49 3.50 3.51 3.52 3.53 3.54 3.55 3.56 3.57 3.58 3.59 3.60 3.61 3.62 3.63 3.64 3.65 3.66 3.67 3.68 3.69 3.70 3.71 3.72 3.73 3.74 Anordnung im Grid-Layout . . . . . . . . . . . . . . . . . . . . Beispieldialog für das Grid-Layout . . . . . . . . . . . . . . . Rasterhilfslinien auf dem Dialog . . . . . . . . . . . . . . . . . Platzierung des Labels im Grid-Layout . . . . . . . . . . . . . Platzierung des Textfeldes im Grid-Layout . . . . . . . . . . . Platzierung einer Taste im Grid-Layout . . . . . . . . . . . . . Platzierung einer Taste im Grid-Layout ohne Streckung . . . . Verschachtelung von Containern mit jeweils eigenen Layouts Beispiel für ineinander geschachtelte Layouts . . . . . . . . . Größenänderungen bei ineinander geschachtelten Layouts . Anordnung im Form-Layout . . . . . . . . . . . . . . . . . . . Beispiel 1 für die Anordnung im Form-Layout . . . . . . . . . Beispiel 2 für die Anordnung im Form-Layout . . . . . . . . . Beispiel 3 für die Anordnung im Form-Layout . . . . . . . . . Automatische Randerkennung im Form-Layout . . . . . . . . Keine automatische Randerkennung im Form-Layout . . . . . Screenshot des Beispiels . . . . . . . . . . . . . . . . . . . . Einfügen des Titels . . . . . . . . . . . . . . . . . . . . . . . . Einfügen der Abbruch-Taste . . . . . . . . . . . . . . . . . . . Einfügen der Ok-Taste . . . . . . . . . . . . . . . . . . . . . . Einfügen des Bildes . . . . . . . . . . . . . . . . . . . . . . . Einfügen des Textfeldes . . . . . . . . . . . . . . . . . . . . . Dialog mit Form-Layout . . . . . . . . . . . . . . . . . . . . . Einfügen des Titels . . . . . . . . . . . . . . . . . . . . . . . . Einfügen des Textfeldes . . . . . . . . . . . . . . . . . . . . . Einfügen der Ok-Taste . . . . . . . . . . . . . . . . . . . . . . Einige Widgets aus dem Nebula-Projekt . . . . . . . . . . . . Einige Widgets aus der RCPToolbox . . . . . . . . . . . . . . Komponenten in einem Menü . . . . . . . . . . . . . . . . . . Radio-Tasten in einem Menü . . . . . . . . . . . . . . . . . . Ausgabe des ersten Menü-Beispiels . . . . . . . . . . . . . . Hinzufügen der Checkbox zum Menü . . . . . . . . . . . . . . Hinzufügen der Radio-Taste zum Menü . . . . . . . . . . . . Begriffe zur Tastatursteuerung in Menüs . . . . . . . . . . . . Popup-Menü auf verschiedenen Plattformen . . . . . . . . . . Menü in einem System-Tray auf verschiedenen Plattformen . Toolbar auf verschiedenen Plattformen . . . . . . . . . . . . . Elemente einer Coolbar . . . . . . . . . . . . . . . . . . . . . Beispiel einer Coolbar . . . . . . . . . . . . . . . . . . . . . . Vereinfachte Klassenhierachie . . . . . . . . . . . . . . . . . . Relative Positionierung von Controls . . . . . . . . . . . . . . Hintergrundfarbe eines Controls . . . . . . . . . . . . . . . . . Tooltip an einem Control . . . . . . . . . . . . . . . . . . . . . Gesperrtes Control . . . . . . . . . . . . . . . . . . . . . . . . Allgemeines Bobachter-Muster . . . . . . . . . . . . . . . . . Ablauf im Bobachter-Muster . . . . . . . . . . . . . . . . . . . Ereigniszustellung unter SWT . . . . . . . . . . . . . . . . . . 110 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 35 36 36 37 37 38 38 38 39 40 42 42 43 43 43 44 44 45 45 46 46 47 49 49 49 53 54 54 55 56 57 57 58 59 60 61 62 63 63 65 65 65 65 68 69 69 Abbildungsverzeichnis 3.75 3.76 3.77 3.78 3.79 3.80 3.81 3.82 3.83 3.84 3.85 3.86 3.87 3.88 3.89 3.90 3.91 3.92 3.93 3.94 3.95 3.96 3.97 Aktion für Menüeintrag und Toolbar-Taste . . . . . . . . . . Darstellungen aus einer Aktion . . . . . . . . . . . . . . . . Ereigniszustellung unter JFace . . . . . . . . . . . . . . . . Aktion zur Dokumenterzeugung . . . . . . . . . . . . . . . . Menü aus Aktionen erzeugen . . . . . . . . . . . . . . . . . Coolbar aus Aktionen erzeugen . . . . . . . . . . . . . . . . Ergebnis auf einigen Fenstersystemen . . . . . . . . . . . . Struktur im MVC-Ansatz . . . . . . . . . . . . . . . . . . . . Ablauf im MVC . . . . . . . . . . . . . . . . . . . . . . . . . Wichtige Klassen für eine Tabelle . . . . . . . . . . . . . . . Wichtige Klassen für eine JFace-Tabelle . . . . . . . . . . . Modellklassen für die JFace-Tabelle . . . . . . . . . . . . . JFace-Tabelle für eine Liste von Personen . . . . . . . . . . Interaktion zwischen JFace-Tabelle und Modell . . . . . . . Ausgabe der JFace-Tabelle (noch unvollständig) . . . . . . Ansichtsklassen für die JFace-Tabelle . . . . . . . . . . . . Ausgabe der JFace-Tabelle mit Formatierer . . . . . . . . . Fehlende dynamische Spaltenanpassung bei einer Tabelle Korrekte dynamische Spaltenanpassung bei einer Tabelle . Sortierung einer Tabelle . . . . . . . . . . . . . . . . . . . . Eingabe in eine JFace-Tabelle . . . . . . . . . . . . . . . . . Interaktion mit einem Editor in einer JFace-Tabelle . . . . . Anzeige und Eingabe in eine JFace-Tabelle . . . . . . . . . 111 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 75 75 76 78 78 79 80 80 81 85 86 87 87 89 90 91 92 93 93 96 97 98 9 Literaturverzeichnis [Dau07] [ISO3166] Daum B.: Rich-Client-Entwicklung mit Eclipse 3.3, dpunkt-Verlag, 2007 ISO-Norm 3166 (Ländercodes), http://www.iso.org/iso/country_codes/ iso_3166_code_lists/ english_country_names_and_code_elements.htm [ISO639] ISO-Norm 631 (Sprachcodes), http://www.loc.gov/standards/iso639-2/php/ code_list.php [JavaBsp] Java-Quelltextbeispiele (alles Mögliche): http://www.java2s.com [JFaceBsp] Code-Beispiele zu JFace http://wiki.eclipse.org/index.php/JFaceSnippets [Layouts] http://www.eclipse.org/articles/article.php? file=Article-Understanding-Layouts/index.html [Mar06] Marinilli M.: Professional Java User Interfaces, Wiley & Sons, 2006 [McA10] McAffer J., Lemieux J. M.: Eclipse Rich Client Platform, Addison-Wesley Longman, 2010 [RCPOnline] Online-Version der ersten Seiten eines neuen RCP-Buches http://www. ralfebert.de/rcpbuch/ [Sca05] Scarpino M, et.al.: SWT/JFace in Action, Manning Publications Co., 2005 [SWT] Tutorials und Artikel zu SWT http://www.eclipse.org/swt/ [SWTBsp] Code-Beispiele zu SWT http://www.eclipse.org/swt/snippets/ [SurrPa] Behandlung von Zeichenketten bei Internationalisierung http://java.sun.com/mailers/techtips/corejava/2006/ tt0822.html [UIGuide] Design-Richtlinien für grafische Oberflächen mit SWT und JFace http://www.eclipse.org/articles/Article-UI-Guidelines/ Index.html [War07] Warner R., Harris R.: The Definite Guide to SWT and JFace, Apress, 2007 Abbildungsverzeichnis [Wue08] Wütherich G., Hartmann N., Kolb B., Lübken M.: Die OSGi Service Platform, dpunkt-Verlag, 2008 113 10 Stichwortverzeichnis Symbole .NET. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .9 Action . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74, 76 run . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 ApplicationWindow . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 Browser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 Button . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 addSelectionListener . . . . . . . . . . . . . . . . . . . 71 CCombo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51, 96 CLabel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 Canvas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 CellConstraints . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 CellEditor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 CellLabelProvider . . . . . . . . . . . . . . . . . . . . . . . . 90, 96 CheckboxCellEditor . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 ColorCellEditor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 ColumnPixelData . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 ColumnWeightData . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 ComboBoxCellEditor . . . . . . . . . . . . . . . . . . . . . . . 96, 99 ComboViewer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 Combo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 Composite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38, 53, 67 ControlEvent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 Control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 addSelectionListener . . . . . . . . . . . . . . . . . . . 71 setBackgroundImage . . . . . . . . . . . . . . . . . . . . . . 65 setBackground . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 setEnabled . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 setFont . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 setForeground . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 setLayoutData . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 setMenu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 setToolTipText . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 setVisible . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 Ereignis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 CoolBarManager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 CoolBar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 CoolItem. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .61 Date . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51, 52 DeferredContentProvider . . . . . . . . . . . . . . . . . . . . 95 DialogCellEditor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 DragDetectEvent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 EditingSupport . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 Event index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 FillLayout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 FocusEvent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 FormAttachment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 FormData. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .40 FormLayout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39, 47 GridDataFactory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 GridData. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .34 GridLayoutFactory . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 GridLayout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 Group . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 HelpEvent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 ILazyContentProvider . . . . . . . . . . . . . . . . . . . . . . . . 95 IStructuredContentProvider .. . . . . . . . . . . 87, 95 KeyEvent. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .66 Label . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 Link . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 ListViewer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52, 85 MenuDetectEvent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 MenuItem. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .55 MenuManager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 Menu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 MouseEvent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 MouseMoveEvent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 Object notifyAll . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 notify . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 wait . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 PaintEvent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 PanelBuilder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 ProgressBar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 RowData . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 RowLayoutFactory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 RowLayout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 Scale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 ScrollBar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 Scrollable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 getHorizontalBar . . . . . . . . . . . . . . . . . . . . . . . . .67 getVertikalBar . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 SelectionAdapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 SelectionEvent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 SelectionListener . . . . . . . . . . . . . . . . . . . . . . . . 70, 73 widgetDefaultSelected . . . . . . . . . . . . . . . . . . 71 widgetSelected. . . . . . . . . . . . . . . . . . . . . . . .71, 72 ShellAdapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 ShellEvent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 doIt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 ShellListener . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 shellActivated . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 shellClosed . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 shellDeactivated . . . . . . . . . . . . . . . . . . . . . . . . .74 shellDeiconified . . . . . . . . . . . . . . . . . . . . . . . . .74 shellIconified . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 Shell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 Slider . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 Spinner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 StyledCellLabelProvider . . . . . . . . . . . . . . . . . . . . 96 StyledText . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 TableColumnLayout . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 ColumnPixelData . . . . . . . . . . . . . . . . . . . . . . . . . . 92 ColumnWeightData . . . . . . . . . . . . . . . . . . . . . . . . .92 Stichwortverzeichnis TableColumn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 setRedraw . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 setText . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 setWidth. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .84 TableEditor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 TableItem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81, 82 setText . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 TableViewerColumn . . . . . . . . . . . . . . . . . . . . . . . . 89, 91 TableViewer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85, 88, 91 Table . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53, 81, 84, 85 virtuelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 TextCellEditor. . . . . . . . . . . . . . . . . . . . . . . . . . . . .96, 98 Text . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 Thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 interrupt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 run . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 start . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 ToolBarManager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 ToolBar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 TraverseEvent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 TrayItem. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .59 Tray . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 TreeViewer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53, 85 TypedEvent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 widget . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 ViewerFilter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95 ViewerSorter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 Widget . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 Ereignis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 synchronized . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 volatile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 Listener . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 Ereignisbehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 A Hardware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 F Fat Client plattformabhängig.. . . . . . . . . . . . . . . . . . . . . . . . . . . . .9 plattformunabhängig .. . . . . . . . . . . . . . . . . . . . . . . . . . 9 Fenstersystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Fill-Layout .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 merginHeight . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 merginWidth . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 spacing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 Attribute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 Form-Layout .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 FormAttachment . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 FormData. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .40 Form-Layout (JGoodies) . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 CellConstraints . . . . . . . . . . . . . . . . . . . . . . . . . . 48 PanelBuilder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 Dialog Unit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 Eigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 G Geschäftslogik. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .11 Geschachtelte Layouts . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 Grid-Layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 GridDataFactory . . . . . . . . . . . . . . . . . . . . . . . . . . 35 GridData. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .34 GridLayoutFactory . . . . . . . . . . . . . . . . . . . . . . . 34 H I Accelerator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55, 57 Anwendungskern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 ASP.NET . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 IDE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 B J C K Java Development Kit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Java ME . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Java Runtime Environment . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Java Web Start . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 JDK . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 JFace Tabellen-Modell.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .86 JRE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Baum. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .53 Benutzungsschnittstelle .. . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Beobachter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 Beobachter-Muster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 Betriebssystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Block synchronisierter . . . . . . . . . . . . . . . . . . . . . . . . 107, 108 Browser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 Kalender . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 Klassifikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9, 12 C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9, 10 C#. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .10 Control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 Aktivierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 Farben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 Größe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 Position . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 Tooltip . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79, 80 Coolbar. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .61 L Layout absolutes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 Fill-. . . . . . . . . . . . . . . . . . . . . . . . . .siehe Fill-Layout 27 Form- . . . . . . . . . . . . . . . . . . . . siehe Form-Layout 39 Form- (JGoodies) siehe Form-Layout (JGoodies) 47 Größenänderungen.. . . . . . . . . . . . . . . . . . . . . . . . . .26 Grid- . . . . . . . . . . . . . . . . . . . . . . siehe Grid-Layout 32 Internationalisierung .. . . . . . . . . . . . . . . . . . . . . . . . . 25 Komponentengröße . . . . . . . . . . . . . . . . . . . . . . . . . . 27 Management . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 Row- . . . . . . . . . . . . . . . . . . . . . . siehe Row-Layout 29 Layout-Manager .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 Layouts Geschachtelte . . siehe Geschachtelte Layouts 38 Listenauswahl . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 Listener . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 D Datumseingabe. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .51 Dialogsteuerung .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11, 12 E Eclipse RCP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 Ereignis SelectionAdapter . . . . . . . . . . . . . . . . . . . . . . . . . 73 SelectionEvent . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 Beobachter-Muster . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 Fenster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 115 Stichwortverzeichnis M Sortierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 View. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .89 virtuelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 Taste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 Texteingabefeld einzeiliges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 Thin Client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 Thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104, 106 MAX_PRIORITY . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 NORM_PRIORITY . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 PN_PRIORITY . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 atomare Zugriffe. . . . . . . . . . . . . . . . . . . . . . . . . . . . .106 beenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 blockieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 Daemon. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .108 erzeugen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 freigeben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 gegenseitiger Ausschluss . . . . . . . . . . . . . . . . . . . 108 gemeinsame Ressourcen . . . . . . . . . . . . . . . . . . . 106 Priorität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 starten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 synchronisieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 unterbrechen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 Toolbar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 Menü . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 Accelerator . . . . . . . . . . . . siehe Accelerator 55, 58 Bilder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 Checkbox . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 Eintrag . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 Mnemonic . . . . . . . . . . . . . . siehe Mnemonic 55, 58 Popup- . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 Radio-Taste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 System-Tray . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 Tastatursteuerung .. . . . . . . . . . . . . . . . . . . . . . . . . . . 57 Trennstrich. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .55, 57 Methode synchronized . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 synchronisierte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 Mnemonic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55, 57 Modell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79, 80 Monitor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 freigeben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 warten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 Wartezeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .107 Multithreading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 MVC Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79, 80 Modell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79, 80 View. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .79 V View. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .79 N W NetBeans Platform . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 Widget . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11, 16, 64 Windows CE/Mobile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Windows-Forms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 O Objekt Monitor. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .108 Synchronisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 X XAML. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .9 P Z Passworteingabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 Polling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 Popup-Menü. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .59 Zeiteingabe .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .52 Q QT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9, 14 R Rich Client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 Rich Fat Client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 Rich Thin Client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 Row-Layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 RowData . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 Attribute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 Runnable. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .104 S Schieberegler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 Sequential Consistency . . . . . . . . . . . . . . . . . . . . . . . . . . 106 Smart Client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Swing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9, 14 SWT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Symbian OS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Synchronisationsobjekt .. . . . . . . . . . . . . . . . . . . . . . . . . . 107 Synchronisierter Abschnitt . . . . . . . . . . . . . . . . . . . . . . . . 107 System-Tray . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 T Tabelle. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .53, 81 Ansicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 Eingabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 Filterung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .95 JFace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 Modell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 116