Aus dem letzten Kapitel wissen wir schon ziemlich viel darüber, wie man eine GUI programmiert. In diesem Kapitel lernen wir Details zur internen Organisation (Szenengraph) kennen, weitere Kontrollen und Layouts und noch etwas über CSS-Klassen.
23.1 Szenengraph und Controls
JavaFX verwendet einen Szenengraph, um die Bestandteile
einer GUI zu verwalten. Der Szenengraph wird in der Klasse
Scene
gespeichert. Der Graph folgt einer
Baumstruktur, der aus Knoten (engl. node) besteht.
Ein Knoten kann "Kinder" haben (im Bild unterhalb des Knotens
gezeigt und mit Linien mit dem Elternknoten verbunden).
Einen Knoten mit Kindern nennt man branching node (branch = Zweig),
einen Knoten ohne Kinder nennt man leaf node (leaf = Blatt).
Den "obersten" Knoten nennt man root node (root = Wurzel).
Was bedeutet das für unsere GUIs? Die Blätter (leaf nodes) sind die sichtbaren Komponenten (Buttons, Textfelder, Slider etc.), die Zweigknoten (branching nodes) sind die unsichtbaren, strukturellen Elemente (BorderPane, HBox, VBox etc.):
Man kann sich das auch wie eine Verzeichnisstruktur vorstellen. Die Blätter sind die Dateien, also etwas Konkretes und die Verzeichnisse sind die Zweigknoten, die zur Strukturierung da sind.
In der theoretischen Informatik nennt man Blattknoten auch Terminale und Zweigknoten Nicht-Terminale.
In der Computergrafik, insbesondere auch in Game Engines, werden Szenengraphen oft eingesetzt, um die Transformationen zu verwalten: die Zweigknoten enthalten dort Transformationen (Translation, Rotation, Skalierung), wohingegen die Blattknoten konkrete Formen (Rechteck, Ellipse, Quader, Kugel ...) darstellen. Im Gegensatz zu Processing, wo jede Transformation "von Hand" direkt im Code vorgenommen wird, nennt man die Vorgehensweise eines Szenengraphen auch retained mode. Das bedeutet, dass intern ein "Modell" der Zeichnung vorgehalten (= retained) wird, welches erst zum Zeitpunkt des Renderns in konkrete Anweisungen umgewandelt wird.
Nachdem wir wissen, wie ein Szenengraph strukturell aufgebaut ist, beschäftigen wir uns zunächst mit einigen konkreten "Blatt-Typen" oder "Controls".
CheckBox
Eine CheckBox (engl. to check = abhaken) erlaubt es, eine oder mehrere Optionen auszuwählen:
Im Gegensatz zu einem Radiobutten kann beliebig viele oder keine der Optionen markieren.
Im Code platzieren Sie einfach irgendwo Ihre Boxen:
public class CheckboxDemo extends Application { @Override public void start(Stage stage) { CheckBox check1 = new CheckBox("Milch"); CheckBox check2 = new CheckBox("Brot"); CheckBox check3 = new CheckBox("Butter"); Button kaufen = new Button("Kaufen"); VBox pane = new VBox(check1, check2, check3, kaufen); pane.setPadding(new Insets(15)); pane.setSpacing(25); Scene scene = new Scene(pane); stage.setTitle("Schöner Einkaufen"); stage.setScene(scene); stage.show(); } }
Anschließend können Sie den Zustand einer Box mit
der Methode isSelected()
abfragen, die
true (ausgewählt) oder false (nicht ausgewählt)
zurückgibt. Hier ein Beispiel, wo wir
eine Aktion an den Button hängen und für jede
Auswahl das entsprechende "Produkt" ausdrucken:
kaufen.setOnAction(e -> { System.out.println("Sie kaufen:"); if (check1.isSelected()) { System.out.println("Milch"); } if (check2.isSelected()) { System.out.println("Brot"); } if (check3.isSelected()) { System.out.println("Butter"); } });
In CSS wird das Innere der Box wie folgt gestylt:
.check-box .box { -fx-background-color: white; }
Die Box besitzt nochmal eine eigene CSS-Klasse. Mit obiger Syntax wählt man also Elemente der Klasse .box
innerhalb eines Elements der Klasse .check-box
aus..
RadioButton
Ein radio button ist wie eine Checkbox mit dem Unterschied, dass eine Reihe von Radiobuttons zusammenhängen, so dass immer nur einer zur Zeit angewählt sein kann:
Im Code muss man die RadioButton
Objekte
in eine Gruppe hineinstecken. Diese
Gruppe ist ein Objekt vom Typ ToggleGroup
:
public class RadioButtonDemo extends Application { public void start(Stage stage) { Label label = new Label("Getränk wählen:"); ToggleGroup getraenke = new ToggleGroup(); RadioButton radio1 = new RadioButton("Cappucino"); radio1.setToggleGroup(getraenke); radio1.setSelected(true); RadioButton radio2 = new RadioButton("Latte"); radio2.setToggleGroup(getraenke); RadioButton radio3 = new RadioButton("Espresso"); radio3.setToggleGroup(getraenke); Button kaufen = new Button("Kaufen"); VBox pane = new VBox(label, radio1, radio2, radio3, kaufen); pane.setPadding(new Insets(15)); pane.setSpacing(25); Scene scene = new Scene(pane); stage.setTitle("Automat"); stage.setScene(scene); stage.show(); } }
Ähnlich wie bei den Checkboxen, können Sie die Methode isSelected()
verwenden, um den Zustand abzufragen:
kaufen.setOnAction(e -> { System.out.println("Sie kaufen:"); if (radio1.isSelected()) { System.out.println("Cappucino"); } if (radio2.isSelected()) { System.out.println("Latte"); } if (radio3.isSelected()) { System.out.println("Espresso"); } });
Alternativ können Sie die ToggleGroup fragen, welches Objekt denn ausgewählt ist. Da ToggleGroup verschiedene Typen erlaubt, müssen Sie zuvor casten:
kaufen.setOnAction(e -> { String produkt = ((RadioButton)getraenke.getSelectedToggle()).getText(); System.out.println("Sie kaufen" + produkt); });
In CSS wird das Innere des Radiozirkels wie folgt gestylt:
.radio-button .radio { -fx-background-color: white; }
ComboBox
Ein Dropdown-Menü heißt in JavaFX ComboBox
:
Das ComboBox-Objekt verwaltet intern eine Liste für die
Einträge. Diese Einträge können beliebige Objekte sein,
der Einfachheit halber verwenden wir hier Strings.
Man setzt die Einträge, indem man zunächst mit getItems()
die Liste aus der ComboBox holt und dann addAll()
auf dieser Liste anwendet. Mit setValue()
gibt man
an, welcher Eintrag zu Beginn angezeigt werden soll:
public class ComboBoxDemo extends Application { public void start(Stage stage) { Label label = new Label("Choose Nationality"); label.setFont(new Font(20)); ComboBox comboBox = new ComboBox(); comboBox.getItems().addAll("German", "American", "French"); comboBox.setValue("German"); // Layout BorderPane pane = new BorderPane(comboBox); pane.setTop(label); pane.setBottom(ok); pane.setPadding(new Insets(30)); Scene scene = new Scene(pane); stage.setTitle("Where from"); stage.setScene(scene); stage.show(); } }
Button ok = new Button("OK"); ok.setOnAction(e -> { String choice = (String)comboBox.getValue(); System.out.println("Selected: " + choice); });
Wenn man die Hintergrundfarbe der ComboBox ändert...
.combo-box { -fx-background-color: moccasin; }
...betrifft das nur das vordergründig sichtbare Element.
Um den Hintergrund des aufklappbaren (popup) Menüs zu stylen, greift man auf die CSS-Klasse .combo-box-popup und deren enthaltenen Klassen zu:
.combo-box-popup .list-view .list-cell { -fx-background-color: moccasin; }
Jetzt sind stimmt jedoch das Styling nicht mehr für das jeweils markierte Element. Dazu müssen wir noch auf Pseudoklassen zugreifen (siehe Abschnitt 23.4):
.combo-box-popup .list-view .list-cell { -fx-background-color: moccasin; -fx-text-fill: black; } .combo-box-popup .list-view .list-cell:filled:hover { -fx-background-color: goldenrod; -fx-text-fill: white; }
Jetzt haben wir unsere ComboBox vernünftig eingefärbt:
Sie sehen hier, dass komplexere (zusammengesetzte) GUI-Elemente ein differenzierteres Styling erfordern.
Übungsaufgaben
(a) Shopping Filter
Programmieren Sie eine Suchmaske bzw. einen Suchfilter für einen Bekleidungs-Shop. Stylen Sie das Fenster mit CSS. (Im Beispiel sind die Farben darkgoldenrod und moccasin verwendet.) Die Optionen bei Warengruppe sind: Socken, Mützen, Handtaschen.
Wenn man auf "Suche" klickt, sollte ein Suchtext erzeugt werden. Zum Beispiel bei
erscheint dieser Text:
Suche Mützen (rot gelb) zwischen 10 und 100€
Tipp: Beim Zusammensetzen des Strings erleichtert der ternäre Operator (Kap. 18.2) das Leben. Es geht aber auch mit If-Anweisungen.
(b) Shopping Backend
Nachdem wir die GUI haben, programmieren wir ein kleines Backend dazu: Einen Shop mit Produkten, die wir mit unserer GUI durchsuchen können.
Dazu benötigen wir lediglich zwei Klassen: Shop
und Product
.
Die Klasse Product
hat vier Instanzvariablen:
- category
- color
- price
- title (hier kommt ein griffiger Produkttitel rein, z.B. "Falke Wandersocke F11")
Zusätzlich brauchen wir einen Konstruktor mit den vier Parametern und eine aussagekräftige toString()
-Methode.
Die Klasse Shop
enthält eine Liste von Product
-Objekten und kann diese Liste auch mit einer Getter-Methode zurückgeben.
Zum Testen können Sie im Konstruktor von Shop
auch einfach die Produktliste befüllen.
Erstellen Sie einfach in Ihrer GUI-Klasse einen Shop (z.B. direkt in der start()-Methode), der mit mindestens 5 Artikeln befüllt sein sollte. Wenn der Benutzer auf "Suche" klickt, sollten die Produkte ausgegeben werden, auf die die Eigenschaften zutreffen (können natürlich mehrere sein).
Die Ausgabe erfolgt auf der Konsole. Die Ausgabe sollte eine Zusammenfassung der Filtereinstellung beinhalten (Ergebnis von a) und dann die Artikel aufzählen. Abschließend können Sie noch anzeigen, wie viele Artikel gefunden wurden. Zum Beispiel:
Suche Socken (rot blau) unter 10€ > Wandersocke F11 (Socken) rot 9.5 EUR > Tennissocke X13 (Socken) blau 5.5 EUR > Supersocke 15 (Socken) blau 9.0 EUR Found 3 products.
23.2 Events
Bislang kennen wir den Fall, dass wir auf einen Button-Click
reagieren wollen. Wir hängen dann ein Stück Code (Lambda-Ausdruck)
mit Hilfe von setOnAction
an den Button.
Den Button-Click nennt man auch Event (engl.
für Ereignis). Ein Event ist ein punktuelles Ereignis
in der Zeit, wo ganz präzise definiert ist, wann dieser Zeitpunkt
eintritt. Bei einem Mausklick zum Beispiel muss man genau
angeben, ob man auf das Runterdrücken oder auf das Loslassen
der Maustaste reagieren möchte.
Wenn man mit Code auf solche Events reagiert, spricht man auch von Event-basierter Programmierung. In diesem Abschnitt lernen wir über die neuen Controls Slider (Schieberegler) und TextArea (Texteingabebereich) auch neue Arten von Events kennen.
Slider und Mausevents
Ein Slider ist ein Schieberegler, mit dem sich ein Wert einstellen lässt, der über eine größeres Spektrum läuft (z.B. von 0 bis 100) oder kontinuierlich ist (z.B. eine gebrochene Zahl zwischen 0 und 1).
Slider kann man z.B. verwenden, um eine Farbe mit Regler für rot, grün und blau einzustellen:
Ein Slider hat einen Konstruktor, dem man das gewünschte Wertspektrum übergibt (MIN und MAX) sowie den anfangs eingestellten Wert (VALUE):
new Slider(MIN, MAX, VALUE);
Im Code sehen wir drei Slider für die Farben Rot, Grün und Blau:
public class SliderDemo extends Application { @Override public void start(Stage stage) { Slider redSlider = new Slider(0, 1, 0); Slider greenSlider = new Slider(0, 1, 0); Slider blueSlider = new Slider(0, 1, 0); VBox sliderPane = new VBox(redSlider, greenSlider, blueSlider); Rectangle colorField = new Rectangle(200, 100); // Layout BorderPane pane = new BorderPane(colorField); pane.setTop(sliderPane); pane.setPadding(new Insets(30)); Scene scene = new Scene(pane, 300, 250); stage.setTitle("Colors"); stage.setScene(scene); stage.show(); } }
Mausevents
Beim Slider wollen immer dann reagieren, wenn der Schieberegler verschoben wird und zwar noch während er verschoben wird. Denn es soll sich während des Verschiebens die Hintergrundfarbe anpassen.
Das Event, das wir suchen, ist also, dass der Regler um ein kleines Stück verschoben wurde. Dazu muss die Maus über dem Regler gedrückt worden sein und die Maustaste muss sich noch im niedergedrücketen Zustand befinden. Man nennt dies auch "dragging" (engl. für ziehen/schleifen).
Wir können also eine Funktion an das Event mouse dragged hängen. Das bedeutet, dass der User auf das Element klickt und die Maus im gedrückten Zustand bewegt.
Als Reaktion auf dieses Event stellen wir eine neue Farbe für das Rechteck ein. Der Einfachheit halber hängen wir dreimal die gleiche Funktion an die jeweiligen Slider (eigentlich unschön wegen Code-Duplizierung):
// Aktionen redSlider.setOnMouseDragged(e -> { double red = redSlider.getValue(); double green = greenSlider.getValue(); double blue = blueSlider.getValue(); colorField.setFill(new Color(red, green, blue, 1.0)); }); greenSlider.setOnMouseDragged(e -> { double red = redSlider.getValue(); double green = greenSlider.getValue(); double blue = blueSlider.getValue(); colorField.setFill(new Color(red, green, blue, 1.0)); }); blueSlider.setOnMouseDragged(e -> { double red = redSlider.getValue(); double green = greenSlider.getValue(); double blue = blueSlider.getValue(); colorField.setFill(new Color(red, green, blue, 1.0)); });
Funktionen an Variablen binden
Um die Code-Duplizierung oben zu vermeiden, müsste man die selbe Funktion an alle Sliderobjekte binden. Wenn wir zunächst den Lambda-Ausdruck einer lokalen Variable zuweisen könnten, könnte das klappen.
Doch welchen Typ hätte diese Variable? Wenn wir uns die Dokumentation von setOnMouseDragged ansehen, finden wir "EventHandler<? super MouseEvent>". Das heißt, unsere Funktion ist vom Typ "EventHandler" und ist ein Generic.
Wir definieren also unsere Variable mit dem Code wie folgt:
EventHandler<MouseEvent> sliderHandler = e -> { double red = redSlider.getValue(); double green = greenSlider.getValue(); double blue = blueSlider.getValue(); colorField.setFill(new Color(red, green, blue, 1.0)); };
Jetzt ist unser Lambda-Ausdruck an die Variable sliderHandler
gebunden und wir können dies drei Mal den entsprechenden Slider-Funktionen
übergeben:
redSlider.setOnMouseDragged(sliderHandler); greenSlider.setOnMouseDragged(sliderHandler); blueSlider.setOnMouseDragged(sliderHandler);
Unsere Code-Duplizierung ist verschwunden, d.h. wenn wir den Handler-Code verändern, müssen wir dies nicht an drei Stellen tun, sondern nur noch an einer.
TextArea und Tastaturevents
Sie kennen bereits das Element TextField
,
um einzeilige Texteingaben zu erlauben.
Eine TextArea
erlaubt die Eingaben
von mehrzeiligen Texten, z.B. für Kommentare oder
Kurzbeschreibungen.
Code-Beispiel:
public class TextAreaDemo extends Application { public void start(Stage stage) { // Controls Label label = new Label("Kommentar"); label.setFont(new Font(20)); Button ok = new Button("OK"); TextArea textArea = new TextArea(); // Layout BorderPane pane = new BorderPane(textArea); pane.setPadding(new Insets(10)); pane.setTop(label); pane.setBottom(ok); Scene scene = new Scene(pane, 300, 200); // Fenster stage.setTitle("Kommentar eingeben"); stage.setScene(scene); stage.show(); } }
Wenn der Platz zur Seite oder nach unten nicht ausreicht, fügt das Element automatisch Scroll-Schaltflächen hinzu.
Man kann auch einstellen, dass Text wortweise umgebrochen wird:
.text-area { -fx-wrap-text: false; }
Dann sieht das so aus:
Tastaturevents
Sie können die Tatsache, dass ein Benutzer ein Zeichen eingibt auch nutzen, um den aktuellen Text zu analysieren und gegebenenfalls zu manipulieren, z.B. um bestimmte Eingaben zu erzwingen (korrekte e-Mail-Adresse) oder um Rechtschreibkorrekturen durchzuführen.
Es gibt zwei wichtige Events, die man dazu verwenden kann:
textArea.setOnKeyPressed(e -> { System.out.println("Taste gedrückt."); }); textArea.setOnKeyReleased(e -> { System.out.println("Taste losgelassen."); }
Der folgende Code prüft, ob ein Wort "foo" eingegeben wurde und ersetzt das Wort durch "BUMM":
textArea.setOnKeyReleased(e -> { System.out.println("Taste losgelassen."); String text = textArea.getText(); if (text.contains("foo")) { text = text.replace("foo", "BUMM"); textArea.setText(text); textArea.positionCaret(text.length()); } });
Beachten Sie, dass wir hier mit Absicht auf das Event "loslassen" reagieren. Warum? Testen Sie es mal mit "gedrückt" und vergleichen Sie, was passiert.
ListView und SelectionModel
Eine weitere häufige Komponente ist eine Listendarstellung, wo eine oder mehrere Option/en angewählt werden können. In unserem MyTunes-Beispiel könnte das wie folgt aussehen:
Das links markierte Element soll im rechten Teil des Fensters beschrieben werden.
Ähnlich wie bei der ComboBox
werden die Optionen
von Java als Liste verwaltet. Im einfachsten Fall kann man
eine Liste von Strings verwenden. Hier ein Beispiel:
Man holt sich die Liste mit getItems()
und befüllt
sie mit Strings:
public class ListDemo extends Application { @Override public void start(Stage stage) { ListView listView = new ListView(); listView.getItems().add("One"); listView.getItems().add("Two"); listView.getItems().add("Three"); listView.getItems().add("Four"); listView.getItems().add("Five"); StackPane root = new StackPane(); root.getChildren().add(listView); Scene scene = new Scene(root, 180, 110); stage.setTitle("List Demo"); stage.setScene(scene); stage.show(); } public static void main(String[] args) { launch(args); } }Man kann aber auch eine Liste von beliebigen Objekten nehmen. Dann wird für die GUI jeweils die
toString()
Methode der Objekte verwendet, um
das Objekt als Option darzustellen.
Im folgenden Code ist dies zu sehen. Die Klasse
MyTunes
stellt eine Liste vom Medium-Objekten
bereit, die direkt dem ListView übergeben wird.
public class MyTunesUI extends Application { private MyTunes myTunes = new MyTunes(); @Override public void start(Stage stage) { myTunes.initSampleData(); // title Label titleL = new Label("MyTunes"); titleL.setId("title"); HBox titleP = new HBox(titleL); titleP.setId("titlepane"); // buttons Button closeB = new Button("Close"); Button newB = new Button("New"); HBox buttonP = new HBox(newB, closeB); buttonP.getStyleClass().add("buttonpane"); // liste ListView mediaL = new ListView(); mediaL.getItems().addAll(myTunes.getMedia()); // description Label typeL = new Label(); Label songL = new Label(); Label personL = new Label(); VBox descP = new VBox(typeL, songL, personL); descP.setPadding(new Insets(0, 10, 10, 20)); descP.setSpacing(10); // layout BorderPane root = new BorderPane(); root.setTop(titleP); root.setBottom(buttonP); root.setLeft(mediaL); root.setCenter(descP); root.setId("root"); // actions mediaL.getSelectionModel().selectedItemProperty().addListener((obsValue, oldValue, newValue) -> { Medium m = (Medium) mediaL.getSelectionModel().getSelectedItem(); typeL.setText(m instanceof Song ? "Song" : "Movie"); songL.setText("Title: " + m.getTitle()); personL.setText(m instanceof Song ? "Artist: " + ((Song) m).getArtist() : "Director: " + ((Movie) m).getDirector()); }); closeB.setOnAction(e -> { Platform.exit(); }); newB.setOnAction(e -> { NewMediumDialog dialog = new NewMediumDialog(); dialog.show(); }); mediaL.getSelectionModel().select(0); Scene scene = new Scene(root, 600, 400); scene.getStylesheets().add("style.css"); stage.setTitle("MyTunes!"); stage.setScene(scene); stage.show(); } public static void main(String[] args) { launch(args); } }
Eine weitere Besonderheit an diesem Code: Es wird hier nicht auf das "setOnAction"-Event gelauscht (schließlich kann sich eine Option in einer Liste auch durch Betätitung einer Cursortaste ändern). Stattdessen hängen wir uns an eine "Property". Das ist ein besonderer Mechanismus in Java, der erlaubt, die Veränderung von Werten zu verfolgen. In diesem Fall verfolgen wir die Änderung der Eigenschaft "selected item", d.h. jedesmal wenn die markierte Option sich ändert, wird unser Code im Lambda-Ausdruck ausgeführt.
Das dazugehörige CSS sieht wie folgt aus:
#title { -fx-font-size: 34px; } #titlepane { -fx-alignment: CENTER; -fx-padding: 5px 10px 20px 10px; } .radio-button { -fx-font-size: 16px; -fx-padding: 10px; } #root { -fx-padding: 20px; } .label { -fx-font-size: 20px; } .button { -fx-font-size: 20px; } .buttonpane { -fx-padding: 5px; -fx-spacing: 10px; -fx-alignment: CENTER_RIGHT; }
Dialogfenster: FileChooser
Bislang haben Sie immer mit einem Fenster gearbeitet, aber normalerweise können mehrere Fenster aufgehen. Wenn ein Fenster aufgeht, um eine relativ kleine Menge Informationen abzufragen, nennt man das ein Dialogfenster.
Eine spezielle Art Dialogfenster ist ein modales Dialogfenster, das heißt, das neue Fenster blockiert alle anderen Fenster, solange bis die Eingabe in neuen Fenster abgeschlossen ist oder abgebrochen wird.
Eine typische Situation für einen solchen Dialog ist die Eingabe eines Dateipfads. Das Dialogfenster wird über einen Button (hier: Datei wählen) gestartet. Alternativ kann man den Pfad in einem Textfeld eingeben.
Hier ist das Verhalten beim Ändern der Fenstergröße wichtig. Es sollte immer das Textfeld vergrößert bzw. verkleinert werden:
Wenn man auf den Button drückt,
kommt das Dialogfenster hoch. Dieses Fenster wird
von JavaFX mit der Klasse FileChooser
zur Verfügung gestellt.
Beachten Sie die Möglichkeit, nach
Dateitypen zu filtern (durch Auswahl in
der ComboBox unten).
Nach Auswahl der Datei soll der komplette Pfad im Textfeld erscheinen:
Im Code sieht das wie folgt aus. Beachten Sie die Verwendung von BorderPane, damit das Textfeld sich vergrößert. Der maßgebliche Code für den FileChooser steht in der Aktion, die an den Button geheftet ist.
public class FileChooserDemo extends Application { @Override public void start(Stage stage) { Label label = new Label("Ziel:"); TextField textfield = new TextField(); Button dateiWaehlen = new Button("Datei wählen.."); // BorderPane, damit Textfeld vergrößert wird BorderPane pane = new BorderPane(textfield); pane.setLeft(label); pane.setRight(dateiWaehlen); pane.setPadding(new Insets(20)); // Statische Methoden für Alignierung BorderPane.setAlignment(label, Pos.CENTER); BorderPane.setAlignment(dateiWaehlen, Pos.CENTER); // Aktion dateiWaehlen.setOnAction(e -> { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Datei wählen"); fileChooser.getExtensionFilters().addAll( new ExtensionFilter("Textdateien", "*.txt"), new ExtensionFilter("Bilddateien", "*.png", "*.jpg", "*.gif"), new ExtensionFilter("Alle Dateien", "*.*")); File selectedFile = fileChooser.showOpenDialog(stage); if (selectedFile != null) { textfield.setText(selectedFile.toString()); } }); Scene scene = new Scene(pane, 400, 120); stage.setTitle("Dateiwahl"); stage.setScene(scene); stage.show(); } }
23.3 Layout
Oracle layout tutorialIm letzten Kapitel haben Sie gelernt, dass das Layout über Layout-Klassen wie BorderPane gehandhabt wird:
In diesem Kapitel haben Sie das Konzept des Szenengraphen kennengelernt. Die Layout-Klassen können Sie sich als Zweigknoten vorstellen, die konkrete Elemente wie Buttons enthalten oder wiederum Layout-Klassen. Im letzten Kapitel haben Sie bereits ein Beispiel eines "verschachtelten" Layouts gesehen:
Im folgenden erweitern wir unser Repertoire an Layout-Klassen um FlowPane und GridPane.
FlowPane
Eine FlowPane ordnet die Komponenten als flexibles Gitter an. Die Elemente werden von links nach rechts angeordnet. Wenn kein Platz mehr da ist, wird in der nächsten "Zeile" fortgefahren. Die Elemente werden also wie Wörter bei einer Textverarbeitung angeordnet und "umgebrochen".
Hier ein Beispiel mit vier Komponenten:
Diese werden umgeordnet, wenn man die Größe des Fensters verändert:
Im folgenden Code werden vier Bilder geladen, die sich im src-Verzeichnis
befinden müssen und "pic-1.jpg" und "pic-2.jpg" etc. heißen müssen. Ein Bild wird
mit Hilfe der Klasse ImageView
geladen und dargestellt. Die Klasse
FlowPane
funktioniert so, dass man mit getChildren
die
Liste von Komponenten bekommt und dann mit add
dort die neuen Komponenten hinzufügt.
public class LayoutDemos extends Application { public void start(Stage stage) { FlowPane flow = new FlowPane(); flow.setPadding(new Insets(20)); flow.setVgap(10); flow.setHgap(10); flow.setStyle("-fx-background-color: gray;"); ImageView pages[] = new ImageView[8]; for (int i = 0; i < 4; i++) { pages[i] = new ImageView( new Image("pic-" + (i+1) + ".jpg")); flow.getChildren().add(pages[i]); } Scene scene = new Scene(flow, 450, 450); stage.setTitle("Flow"); stage.setScene(scene); stage.show(); } }
Man kann den "Flow" auch von oben nach unten gehen lassen. Dann verwendet man den Konstruktor:
new FlowPane(Orientation.VERTICAL);
GridPane
Ein GridPane erlaubt eine Ausrichtung an einem Gitter. Man kann die Abstände und Zwischenräume bestimmen und dann Komponenten in die entsprechenden Zellen packen mit (grid enthalte ein Objekt des Typs GridPane):
grid.add(KOMPONENTE, SPALTE, ZEILE);
Wobei SPALTE und ZEILE mit Null beginnen.
Will man, dass ein Objekt mehrere Spalten oder Zeilen umspannt, verwendet man:
grid.add(KOMPONENTE, SPALTE, ZEILE, SPALTENZAHL, ZEILENZAHL);
Das heißt, wenn SPALTENZAHL und ZEILENZAHL gleich 1 sind, entspricht dies dem obigen Kommando. Andernfalls werden mehrere Zellen für die Komponente aufgewendet.
Ein GridPane entwirft man am besten auf dem Papier:
Im Code sieht das dann wie folgt aus. Für die zwei Buttons
rechts unten benötigen wir eine HBox. Die Alignierung der
beiden Labels werden mit der statischen Methode
setHalignment
von GridPane angegeben. Wenn Sie
setGridLinesVisible
auf true setzen, sehen
Sie die Gitterlinien, was beim Erstellen sehr nützlich sein kann.
public class GridDemo extends Application { @Override public void start(Stage stage) { GridPane grid = new GridPane(); //grid.setGridLinesVisible(true); grid.setHgap(10); grid.setVgap(10); grid.setPadding(new Insets(10, 20, 10, 20)); Label label = new Label("Geben Sie Ihre Infos ein"); label.setFont(new Font(18)); GridPane.setHalignment(label, HPos.CENTER); grid.add(label, 0, 0, 2, 1); Label l1 = new Label("Name"); Label l2 = new Label("Matrikelnr."); GridPane.setHalignment(l1, HPos.RIGHT); GridPane.setHalignment(l2, HPos.RIGHT); grid.add(l1, 0, 1); grid.add(l2, 0, 2); TextField tf1 = new TextField(); TextField tf2 = new TextField(); grid.add(tf1, 1, 1); grid.add(tf2, 1, 2); Button help = new Button("?"); Button ok = new Button("OK"); Button close = new Button("Close"); grid.add(help, 0, 3); HBox buttons = new HBox(ok, close); buttons.setAlignment(Pos.CENTER_RIGHT); buttons.setSpacing(10); grid.add(buttons, 1, 3); Scene scene = new Scene(grid); stage.setTitle("GridPane"); stage.setScene(scene); stage.show(); } }
Die fertige Implementation sieht so aus:
Übungsaufgabe
(a) Adressbuch Reloaded
Ändern Sie Ihr Adressbuch-Layout so, dass die Texteingabe-Felder aligniert sind:
Hinweise:
Verwenden Sie eine GridPane
. Sie können das gesamte Panel zentrieren mit setAlignment(Pos.CENTER)
.
23.4 CSS, Teil 2
Wir lernen noch ein paar weitere Eigenschaften von CSS.
Styling im Code
Wenn Sie auf die Schnelle einen einzelnen Button oder ähnliches stylen wollen, können Sie CSS-Code auch direkt im Code definieren. Dies sollte man nur in Ausnahmefällen tun, da dies eigentlich der Philosophie von CSS widerspricht:
Button button = new Button("OK"); button.setStyle("-fx-background-color: yellow;");
Eigene Klassen
Sie wissen, dass Sie für einzelne Elemente einen ID erzeugen können:
Button b = new Button("OK"); b.setId("foo");
Im CCS-File können Sie sich dann mit dem Hash-Symbol darauf beziehen:
#foo { -fx-font-size: 20px; }
Sie können auch eigene Klassen erzeugen. Der Unterschied zu IDs: eine Klasse kann von mehreren Elementen verwendet werden, ein ID darf nur einmal verwendet werden.
Nehmen wir an, Sie wollen zwei Buttons stylen, dann weisen Sie diesen Ihre neue Klasse zu:
Button b1 = new Button("OK"); b1.getStyleClass().add("footastic"); Button b2 = new Button("Close"); b2.getStyleClass().add("footastic");
Im CSS-File verwenden Sie den Punkt, um auf Klassen zu verweisen:
.footastic { -fx-font-size: 20px; }
Eine GUI-Komponente darf mehreren Klassen angehören. Zum Beispiel gehört jeder Button der CSS-Klasse .button an. Im obigen Beispiel fügen wir noch die CSS-Klasse .footastic hinzu. Wir könnten noch weitere hinzufügen.
Eine GUI-Komponente darf natürlich auch sowohl einen ID haben als auch einer oder mehrern Klassen angehören.
Pseudoklassen
Um verschiedene Zustände z.B. eines Button unterschiedlich stylen zu können, gibt es Pseudoklassen. Bei einem Button kann man mit der Pseudoklasse .button:hover
den Zustand, dass die Maus über dem Button schwebt stylen:
.button:hover { -fx-background-color: yellow; }
In der CSS-Referenz finden Sie weitere Beispiele für Pseudoklassen.
HBox und VBox
Es gibt keine Klassen für HBox
und VBox
. Wenn Sie diese stylen wollen, müssen Sie selbst eine Klasse hinzufügen, z.B. wie hier:
HBox hbox = new HBox(); hbox.getStyleClass().add("hbox");
Anschließend können Sie die Boxen stylen, die dieser Klasse angehören:
.hbox { -fx-background-color: white; -fx-padding: 15; -fx-spacing: 10; }
Hintergrundbilder
Sie können Ihren Layout-Objekten ein Hintergrundbild hinzufügen, das sieht dann in CSS so aus, wenn es für das gesamte Fenster (root) gelten soll:
.root { -fx-background-image: url("bg.jpg"); -fx-background-size: 270, 160; -fx-background-repeat: no-repeat; }
Sie können auch Layout-Objekte wie HBox
und VBox
mit einem Hintergrundbild versehen (mit entsprechendem Selektor).
Das Bild (hier "bg.jpg") muss sich im Verzeichnis src befinden, es wird im obigen Beispiel auf die Größe 270x160 skaliert und nicht wiederholt (d.h. der Hintergrund ist nicht gekachelt). Wenn eine Kachelung erwünscht ist, müssen Sie lediglich die letzte Zeile weglassen.
Übersicht: CSS-Befehle
Hier sind häufige CSS-Befehle tabellarisch aufgeführt, ohne Anspruch auf Vollständigkeit. Eine vollständige Dokumentation finden Sie in in der CSS-Referenz von Oracle.
Text und Schrift
CSS-Befehl | Beschreibung | Beispielwert/e |
---|---|---|
-fx-background-color
|
Hintergrundfarbe |
black, white, yellow, ...
|
-fx-text-fill
|
Textfarbe | s.o. |
-fx-font-size
|
Schriftgröße |
12px
|
-fx-font-family
|
Schriftart |
"Helvetica"
(Anführungszeichen beachten)
|
-fx-font-weight
|
Schrifthervorhebung |
bold, normal
|
Kästen
CSS-Befehl | Beschreibung | Beispielwert/e |
---|---|---|
-fx-padding
|
Abstand zwischen Rand und Inhaltselementen |
15px
|
-fx-spacing
|
Abstand zwischen den Inhaltselementen |
10px
|
-fx-alignment
|
Ausrichtung der Inhaltselemente |
baseline-center, baseline-left, center-right, ...
|
-fx-border-style
|
Randlinie |
solid, dashed, none
|
-fx-border-width
|
Dicke der Randlinie |
3px
|
-fx-border-color
|
Farbe der Randlinie |
black, red, ...
|
-fx-border-radius
|
Abgerundete Ecken |
8px
|
Effekte
CSS-Befehl | Beschreibung | Beispielwert/e |
---|---|---|
-fx-opacity
|
Solidität/Transparenz | Wert zwischen 0 (komplett transparent) und 1 (komplett solide) |
Übungsaufgabe
(a) Karteikarten-Lernsystem
Entwerfen Sie eine GUI für Ihr Karteikarten-Lernsystem aus Kapitel 23.
Sie benötigen Interface-Elemente für folgende Funktionen:
- Laden und Speichern eines Karteikastens
- Anzeigen der Frage und Antwort
- Buttons etc. zum Steuern einer Lernsitzung
Gut wäre es, den aktuellen Füllzustand der Karteikasten-Fächer zu sehen.
Skizzieren Sie die GUI am besten auf Papier und legen Sie anschließend die Layout-Klassen fest (BoderPane, HBox, VBox, GridPane etc.).
Bei erfolgreicher Programmierung können Sie Ihr Programm nutzen, um damit zu lernen (zum Beispiel für die Java 2 Klausur)...
23.5 Links zum Thema
Leider allesamt englischsprachig:
- Erklärung des Szenengraph-Modells (Oracle)
- Überblick aller UI Controls (Oracle)
- Tutorial zum Thema "Layout" (Oracle)
- CSS-Referenz