In diesem Kapitel lernen wir die Grundkonzepte von grafischen Benutzerschnittstellen kennen. Auf Englisch nennt man das graphical user interfaces oder kurz GUI. Dafür benutzen wir das noch sehr junge JavaFX, welches die alte Interface-Technologie Swing ablösen wird.
Grafische Benutzerschnittstellen folgen dem Model-View-Controller (MVC) Prinzip des Software Engineering. Dieses besagt, dass die grafische Sicht (View) von den Daten (Model) getrennt sein sollte. Das "Model" könnte ein Buchladen oder eine Personalverwaltung sein. Diese Modellschicht sollte unabhängig vom grafischen Zugriff existieren, damit auch bei einer komplett anderen GUI die Modellschicht weiter verwendet werden kann. Der "Controller" verbindet Model und View. Wir sehen uns zunächst die View-Schicht an und kommen dann zur Controller-Schicht (über Lambda-Ausdrücke).
Als erstes empfehle ich Ihnen, die API-Dokumentation der JavaFX-Klassen runterzuladen und in Ihrem Browser zu bookmarken. Dazu müssen Sie auf der Java-Downloads-Seite bei "Java SE 8 Documentation" (relativ weit unten) auf den Download-Button klicken. Auf der folgenden Seite sehen Sie dann den Download für die "JavaFX API Documentation".
Sie benötigen außerdem unbedingt Java in Version 8, genauer gesagt das Java SDK 8 (nicht JRE).
Noch ein technischer Hinweis: Verwenden Sie im Code niemals Umlaute (ä, ü, ö) oder das scharfe s (ß). Natürlich dürfen Sie Umlaute in Strings verwenden, aber nicht für Klassen-, Paket, Methoden- oder Variablennamen. Umlaute verursachen leider immer wieder Probleme.
22.1 Erste Schritte mit JavaFX
Wir erstellen die ersten Oberflächen. NetBeans macht uns hier das Leben leichter, indem es uns den Anfangscode spendiert.
JavaFX-Applikation in NetBeans
Wenn wir in NetBeans eine JavaFX-Application erstellen, spendiert uns NetBeans ein Stück Beispielcode. Dazu müssen Sie folgende Auswahl treffen, wenn Sie ein neues Projekt erstellen:
Sie sehen dann folgenden Code, wenn Sie "Create Application Class" markiert hatten. Beachten Sie, dass
Ihre Klasse eine Unterklasse von
javafx.application.Application
ist. Daher erbt Ihre Klasse eine Menge Funktionalität, die in
der Oberklasse implementiert wurde. Das ist eine gängige Praxis,
Funktionalität über Oberklasse bereit zu stellen.
import javafx.application.Application; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.layout.StackPane; import javafx.stage.Stage; public class MyTest extends Application { @Override public void start(Stage primaryStage) { Button b = new Button(); b.setText("Say 'Hello World'"); b.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { System.out.println("Hello World!"); } }); StackPane root = new StackPane(); root.getChildren().add(b); Scene scene = new Scene(root, 300, 250); primaryStage.setTitle("Hello World!"); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
(Selbst dieser Code ist schon wieder veraltet. Sie können die statische main löschen. Dannach müssen Sie allerdings NetBeans neu starten!)
Sie sehen nach dem Start:
Wenn Sie auf den Knopf drücken, erscheint auf der Konsole:
Hello, World!
Bestandteile eines Fensters
Ein Fenster in JavaFX besteht aus Objekten, die ineinander
verschachtelt sind. Auf der obersten Ebene steht eine Objekt
vom Typ
Stage
(engl. für Bühne), welches das Fenster
als Ganzes repräsentiert. Man nennt das auch ein "Top-Level-Fenster".
Für alles Inhaltliche verwendet JavaFX das Konzept eines
Szenengraphs (engl. scene graph), das in der
3D-Grafik häufig verwendet wird.
Daher ist die Basis der inhaltlichen Repräsentation ein Objekt
vom Typ
Scene
(engl. für Szene).
Innerhalb des Scene-Objekts wird so etwas wie ein Setzkasten
aus verschiedenen strukturellen Objekten hergestellt. Hier
einige der Layout-Klassen, die JavaFX dafür anbietet.
Man nennt sie auch Panes (engl. für Fensterleiste),
sie sind alle Unterklassen der Java-Klasse
javafx.scene.layout.Pane
:
Innerhalb eines solchen "Setzkastens" kommen dann die
eigentlichen GUI-Komponenten oder die sogenannten
Controls, z.B. Buttons, Texteingabefelder,
Slider etc. Auch diese Komponenten sind Objekte.
Die Klassen sind sämtlich Unterklassen von
javafx.scene.control.Control
.
Hier einige Beispielklassen:
- Button: Button, der gedrückt werden kann. Der Button kann mit Text oder einem Bild (Icon) oder beidem versehen werden.
- Label: Komponente, die Text oder ein Bild anzeigt, z.B. als Info vor einem Texteingabefeld ("Password eingeben:")
- TextField: Eingabefeld für Text (eine Zeile)
- Slider: Schieberegler, um eine Zahl innerhalb eines Intervals einzugeben
Das Beispielprogramm, das NetBeans zu Beginn erzeugt, hat einen sehr einfachen Aufbau:
Die StackPane
erlaubt das übereinanderstapeln von Komponenten.
In diesem Beispiel wird es aber nur als einzelner Container für
den Button benutzt.
Layout
Mit verschiedenen "Pane"-Klassen können Sie die Komponenten in dem Fenster ordnen und layouten. Ohne diese Layout-Objekte wäre es sehr schwer, die Position der einzelnen Komponenten zu definieren bzw. das Verhalten bei der Größenänderung des Fensters.
Ein einfaches, aber
effektiven Layout ist die BorderPane
.
BorderPane teilt die Fläche in fünf Flächen ein: Center, Top, Bottom, Left, Right.
Dabei die die Region "Center" bevorzugt, d.h. beim Verändern der Fenstergröße
bekommt Center immer den meisten Platz. Zu beachten ist, dass jede Region nur genau ein Objekt aufnehmen kann.
Hier ein einfaches Beispiel, in dem drei GUI-Komponenten erzeugt werden: Label, TextField und Button. Diese werden dann in drei Regionen (Top, Center und Bottom) gesetzt:
public class BorderPaneTest extends Application { @Override public void start(Stage primaryStage) { // (1) Komponenten erzeugen Label label = new Label("Enter your password:"); TextField textField = new TextField(); Button button = new Button("OK"); // (2) Layout-Klasse erzeugen und Komp. einsetzen BorderPane pane = new BorderPane(); pane.setTop(label); pane.setCenter(textField); pane.setBottom(button); // (3) Szene erzeugen und ins Fenster setzen Scene scene = new Scene(pane); primaryStage.setScene(scene); // (4) Fenster konfigurieren und anzeigen primaryStage.setTitle("BorderPane"); primaryStage.show(); } }
Das resultierende Fenster sieht noch nicht sehr schön aus:
Jetzt können Sie Layoutobjekte (BorderPane, HBox, VBox etc.) verschachteln, um
mehrere Elemente in eine Region zu bekommen. Die Klasse
HBox
(auch eine Layoutklasse wie BorderPane) ordnet eine
beliebige Anzahl von Komponenten einfach
von links nach rechts, also horizontal, an.
Wir können jetzt eine HBox mit dem Label und dem TextField befüllen und ins Center schieben. Wir spendieren uns außerdem einen zweiten Button und ordnen die zwei Buttons ebenfalls in einer HBox an.
public class BorderPane2 extends Application { @Override public void start(Stage stage) { // Passwort-Bereich: Label label = new Label("Enter your password:"); TextField textField = new TextField(); HBox entryPane = new HBox(label, textField); entryPane.setAlignment(Pos.CENTER); entryPane.setPadding(new Insets(15)); entryPane.setSpacing(10); // Buttons: Button bOk = new Button("OK"); Button bCancel = new Button("Cancel"); HBox buttonPane = new HBox(bOk, bCancel); buttonPane.setAlignment(Pos.BASELINE_RIGHT); buttonPane.setPadding(new Insets(10)); buttonPane.setSpacing(10); BorderPane pane = new BorderPane(); pane.setCenter(entryPane); pane.setBottom(buttonPane); Scene scene = new Scene(pane, 350, 130); stage.setTitle("BorderPane"); stage.setScene(scene); stage.show(); } }
Beachten Sie, dass der Konstruktor von HBox
(und VBox
) eine beliebige Anzahl von Parametern vom Typ Node
haben kann. Dies erlaubt Ihnen, ganz komfortabel alle Objekte aufzuzählen, die in diese HBox hinein sollen, denn alle GUI-Objekte sind Unterklasse von Node
.
Dass man beliebig viele Parameter übergeben kann, nennt man in Java Varargs. Man erkennt es NetBeans, wenn man ein Autocomplete bei HBox
anstößt:
HBox(Node... children)
Immer wenn der Datentyp eines Parameters drei Punkte aufweist, kann man beliebig viele Parameter dieses Typs mit Kommatrennung hintereinander hängen.
Unser Code erzeugt ein Fenster, das schon besser aussieht:
Wir haben sowohl die Eingabeelemente (Label + TextField) als auch die Buttons (OK, Cancel)
in je eine HBox getan. Diese HBox wurde mit setAlignment
(Anordnung der Elementen),
setPadding
(Innenabstand) und
setSpacing
(Zwischenraum zwischen Elementen) noch konfiguriert.
Hier sieht man die drei Strukturklassen:
Bislang reagieren unsere Knöpfe aber nicht. Wir müssen jetzt Aktionen an sie binden.
Übungsaufgabe
Fenster für ein Adressbuch
Erstellen Sie ein Layout für folgendes Fenster:
Hinweise: Zeichnen Sie zunächst das Basislayout mit BorderPane, HBox und VBox. (Eine VBox funktioniert genauso wie eine HBox, aber vertikal.) Füllen Sie anschließend Ihr Layout mit Objekten vom Typ Label, TextField und Button. Schließlich justieren Sie noch Alignierung, Spacing und Padding.
22.2 Aktionen und Bindung
Jetzt haben wir zwar Buttons, aber wie definieren wir, was bei Druck eines Buttons geschieht?
Lambda-Ausdrücke
Ganz einfach: wir hängen ein Stück Code an einen Button. Das funktioniert seit Java 8 mit sogenannten Lambda-Ausdrücken. Ein Lambda-Ausdruck ist eine Funktion, die nur aus Code besteht, ohne dass sie einen Namen hat.
Nehmen wir eine Funktion, die einen String bekommt und diesen ausgibt:
public void printString(String x) { System.out.println(x); }
Als Lambda-Ausdruck würde man lediglich schreiben:
(String x) -> { System.out.println(x); }
Ein Lambda-Ausdruck hat drei Teile: die Parameterliste, einen "Pfeil" und einen Code-Block.
In Situationen, wo Java den Typ der Parameter erschließen kann (wie es bei GUIs der Fall sein wird), kann man sogar den Typ weglassen:
x -> { System.out.println(x); }
Bindung an GUI-Komponenten
Sie möchten, dass ein Stück Code ausgeführt wird, wenn z.B.
Ihr Button angeklickt wird. Der Klick wird auch als "Ereignis"
(engl. event) aufgefasst und zu jedem Ereignis gibt der Button
einige Informationen mit, die in einem Objekt vom Typ
ActionEvent
gespeichert sind.
Sie können jetzt einen Lambda-Ausdruck schreiben, den Sie dem Button
mitgeben. Der Parameter ist vom Typ
ActionEvent
, allerdings
kann Java das erschließen. Daher schreiben Sie einfach "e ->".
bOk.setOnAction(e -> { System.out.println("OK"); });
Jetzt erscheint auf Ihrer Konsole "OK", sobald Sie auf den OK-Button drücken. Sie können in Ihrem Lambda-Ausdruck natürlich auch auf andere GUI-Komponenten zugreifen.
bOk.setOnAction(e -> { System.out.println("Text: " + textField.getText()); });
Hier wird der vom Benutzer eingegebene Text auf die Konsole geschrieben.
Alternative Schreibweisen
Die oben gezeigte Schreibweise mit einem Lambda-Ausdruck ist die derzeit eleganteste und kürzeste Form. Ich zeige hier kurz die "alten" Schreibweisen, um Events von GUI-Komponenten zu handhaben.
Alternativ können Sie auch den Typ des Parameters e
mit angeben:
bOk.setOnAction((ActionEvent e) -> { System.out.println("Text: " + textField.getText()); });
Ursprünglich wurde eine anonyme Klasse benutzt. Eine anonyme Klasse stellt eine einzige Instanz einer neuen Klasse her, wobei diese neue Klasse gar keinen Namen bekommt.
bOk.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent event) { System.out.println("OK"); } });
Dies war wiederum eine Erleichterung gegenüber der Notwendigkeit, eine eigene Klasse anzulegen, die das Interface EventHandler<ActionEvent> implementiert (das Konzept der Interfaces haben wir noch nicht durchgenommen, ist so etwas ähnliches wie eine abstrakte Klasse):
class MyHandler implements EventHandler<ActionEvent> { public void handle(ActionEvent event) { System.out.println("OK"); } }
Erst nachdem man diese Klasse geschrieben hatte, konnte man ein Handler-Objekt anlegen und dem Button mitgeben:
MyHandler handler = new MyHandler(); bOk.setOnAction(handler);
Mittlerweile hat man erkannt, dass man hier nur eine Funktion übergeben muss und dazu nicht eine ganze Klasse (anonym oder nicht) benötigt - man braucht noch nicht mal einen Namen für diese Funktion. Daher sind die in Java 8 eingeführten Lambda-Ausdrücke ideal für GUI-Handler.
Applikation schließen
Ein andere häufige Aktion ist das Schließen der Applikation. Dies
führen wir mit dem Befehl
Platform.exit()
durch:
bCancel.setOnAction(e -> { Platform.exit(); });
Übungsaufgabe
(a) Adressbuch: Backend und Funktionalität
Verwenden Sie Ihr Fenster von Übung 24.1 (a):
Schreiben Sie zunächst ein Backend, d.h. das eigentliche Adressbuch (Ihre GUI ist dann das Frontend). Programmieren Sie die neuen Klassen direkt in Ihrem JavaFX-Projekt. Ihr JavaFX-Projekt ist ganz regulärer Java-Code, Sie müssen nicht ein neues Projekt anlegen (im Gegenteil, das würde zu Komplikationen führen).
Sie benötigen für das Backend folgende Klassen und Methoden. Es handelt sich wirklich um relativ wenig Code:
Die Klasse Adresse
hat vier Instanzvariablen vorname, nachname, telefon und email (Strings). Außerdem einen entsprechenden Konstruktor mit vier Parametern und eine toString
Methode, die die Adresse ausgibt.
Die Klasse AdressBuch
hat als einzige Instanzvariable eine Liste von Adressen. Sie hat zwei Methoden: addAdresse
fügt eine Adresse hinzu und printAll
gibt per Foreach-Schleife alle Adressen auf der Konsole aus.
Nachdem Sie Ihr kleines Backend geschrieben haben, erzeugen Sie in Ihrem Fenster-Code, direkt in der start
Methode eine Variable adressbuch
mit einem Objekt der Klasse AdressBuch
. Dann
heften Sie mit Lambda-Ausdrücken an jeden Buton die entsprechende Funktionalität:
-
Hinzufügen: Erzeugt ein neues Adresse-Objekt mit den Infos aus den Textfeldern und fügt das Objekt dem
adressbuch
hinzu. - Ausdruck: Schreibt alle Adressen auf die Konsole.
- Schließen: Schließt das Fenster und beendet das Programm.
Testen Sie alle Funktionen.
Hinweise:
Um den Inhalt eines TextField
zu lesen, verwendet man die Methode getText()
(gibt einen String zurück). Um ein TextField zu löschen die Methode clear()
. Werfen Sie auch einen Blick in die TextField-API.
22.3 Styling mit CSS-Stylesheets
JavaFX hat sich eine wichtige Methode aus der Webseiten- Technologie abgeschaut: die Trennung von Layout und Stil. Layout meint hier die Wahl und Anordnung der Elemente (z.B. alle Buttons unten, rechts ausgerichtet). Stil bezieht sich auf Farbe, Form, Schrift etc.
Während das Layout in Java programmiert wird, indem man Layout-Objekte ineinander verschachtelt, wird der Stil in einem separaten Dokument, dem Stylesheet angegeben.
Die Schematik oriertiert sich an CSS (cascading style sheets), welches in der Webtechnologie entwicklet wurde und mit HTML verwendet wird.
Regeln
Ein Stylesheet ist eine einfache Text-Datei mit Endung .css, die Sie am besten in das src Verzeichnis stellen. Sie Datei besteht aus einer Reihe von Regeln. Jede Regel hat das Format:
SELEKTOR { EIGENSCHAFT: WERT; EIGENSCHAFT: WERT; ... }
Der Selektor besagt, für welche Art Elemente diese Regel "feuert". Mit den Eigenschaft-Wert-Paaren setzt man dann die Eigenschaften.
Zum Beispiel setzt man die Schriftgröße aller Buttons mit
.button { -fx-font-size: 14px; }
Kommentare werden wie die Blockkommentare in Java gesetzt und können sich daher auch über mehrere Zeilen erstrecken. Der einfache Zeilenkommentar mit // funktioniert hingegegen nicht!
/* Button Styling */ .button { -fx-font-size: 14px; }
Wie beim Programmieren werden Kommentare auch genutzt, um CSS-Teile zum Testen kurzzeitig auszublenden.
CSS-Klassen
Für jede GUI-Komponente (Button, Label etc.) gibt es eine CSS-Klasse, die so ähnlich heißen wie die Klassen in Java, mit dem Unterschied, dass sie klein geschrieben werden und ein Punkt vorangestellt ist, also .label oder .button (bei getrennten Wörtern wird ein Bindestrich eingefügt, also .text-field für TextField).
Das hat nicht direkt etwas mit
Java-Klassen zu tun, sondern besagt nur, dass
der Bezeichner .button
sich auf mehr als ein Element
bezieht.
Für jede Klasse können Stileigenschaften als Schlüssel-Wert-Paar angegeben
werden.
.label { -fx-font-family: "Helvetica"; -fx-font-size: 14px; } .text-field { -fx-font-family: "Helvetica"; -fx-font-size: 14px; } .button { -fx-font-family: "Helvetica"; -fx-font-size: 14px; -fx-font-weight: bold; }
Mit der Klasse .root
können Sie globale Eigenschaften
definieren, die für alle Komponenten gelten:
.root{ -fx-font-size: 20pt; -fx-font-family: "Courier New"; -fx-base: rgb(132, 145, 47); -fx-background: rgb(225, 228, 203); }
Wenn Sie in .root
die Hintergrundfarbe definieren, gilt diese für das gesamte Fenster.
Einbindung in Java
In NetBeans können Sie ein CSS-File herstellen, indem Sie im Projektfenster (links) mit Rechtsklick auf "Source Packages" die Option "Cascading Style Sheet" wählen.
Es ist wichtig, dass Sie dies auf der Ebene "Source Packages" tun und nicht etwa in einem Ihrer Packages, sonst wird Ihr CSS nicht gefunden.
Sollte die Option "Cascading Style Sheet" fehlen , wählen Sie zunächst "Other..." und im folgenden Dialogfenster dann links "Other" und rechts "Cascading Style Sheet".
Nennen Sie Ihre neue Datei "style.css". Wenn Sie so vorgehen, liegt die Datei im Verzeichnis src. Dort muss sie auch stehen, um vom Java gefunden zu werden.
Im Java-Code wird die CSS-Datei mit folgender Zeile eingebunden:
scene.getStylesheets().add("style.css");
Sie sehen, dass das Stylesheet an der Szene hängt, und dass wir theoretisch auch mehrere Stylesheets an die Szene binden könnten.
Unser Fenster sieht wieder ein bisschen besser aus:
Das Besondere an dem CSS-Mechanismus (und der Grund, warum es sich im Webseiten-Bereich durchgesetzt hat): Sie haben alle Style-Informationen für alle Fenster Ihrer Applikation zusammen in einem File. Es fällt somit leicht, den Look der GUI konsistent zu halten und jederzeit flächendeckend anzupassen.
IDs
In der CSS-Datei haben Sie bislang Klassen wie ".button" gesehen. Eine Klasse betrifft mehrere Einzelelemente (z.B. die zwei Buttons) und wird mit einem Punkt gekennzeichnet. Will man ein einziges Element speziell stylen, so kann man eine ID verwenden. Vielleicht möchte man den OK-Button hervorheben, dann gibt man ihm im Code eine ID:
Button bOk = new Button("OK"); bOk.setId("special");
Eine ID sollte man immer nur für genau ein Element verwenden. In der CSS-Datei kann man dann einen Selektor dafür angeben, nämlich den ID mit einem Hash-Zeichen (#) davor:
.label { -fx-font-family: "Helvetica"; -fx-font-size: 14px; } .text-field { -fx-font-family: "Helvetica"; -fx-font-size: 14px; } .button { -fx-font-family: "Helvetica"; -fx-font-size: 14px; -fx-font-weight: bold; } <!-- großer Button! --> #special { -fx-font-size: 20px; }
Wichtig ist, dass das CSS-File von oben nach unten abgearbeitet wird und dass immer die speziellste Regel verwendet wird. In diesem Fall wird beim OK-Button die speziellere #special-Regel vorgezogen und "überschreibt" den Size-Wert der allgemeineren .button-Regel:
Mit Klassen und IDs können Sie Ihrer GUI einen konsistenten, zeitgemäßen Look geben. Auf der CSS-Referenzseite finden Sie alle CSS-Eigenschaften dokumentiert.
Hier noch einige Einstellungen, die hilfreich sein könnten:
-
-fx-text-fill
(Textfarbe) -
-fx-border-style:solid
(Linie als Umrandung)
Übungsaufgabe
(a) Adressbuch-Styling
Gestalten Sie Ihr Adressbuch-Fenster mit Hilfe von CSS.
Hier nur ein Beispiel:
Hinweise:
Den gesamten Hintergrund sprechen Sie über die Klasse .root
an. Farben werden im einfachsten Fall mit englischen Begriffe benannt wie black, white, red, blue. Im Beispiel wird lightblue verwendet.
22.4 Hilfreiche Links
Die folgenden Links sind allesamt von Oracle (Englisch).
- JavaFX API aller Klassen
- JavaFX-Tutorials
- Überblick über alle GUI-Komponenten (Label, Button, Slider ...)
- Überblick über Layout-Klassen (BorderPane, HBox, VBox ...)
- CSS-Referenzseite (alle CSS-Einstellungen, leider etwas unübersichtlich)
- Tutorial zur Verwendung von CSS in JavaFX