Multiple views
Bisher haben unsere grafischen Benutzungsoberflächen immer nur eine Ansicht enthalten. Nun werden wir Benutzungsoberflächen mit mehreren Ansichten kennenlernen.
Im Allgemeinen werden die Ansichten als Scene-Objekte erstellt, und der Wechsel zwischen ihnen erfolgt durch Ereignisse, die an die Anwendung gebunden sind. Im folgenden Beispiel gibt es zwei Scene-Objekte, die jeweils ihren eigenen Inhalt und ein Ereignis haben, das mit dem Inhalt verknüpft ist. Anstelle eines Objekts zur Anordnung von Komponenten (wie BorderPane) enthält jedes Scene-Objekt nur eine Benutzungsoberflächenkomponente.
package application;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;
public class BackAndForthApplication extends Application {
@Override
public void start(Stage window) {
Button back = new Button("Back ..");
Button forth = new Button(".. forth.");
Scene first = new Scene(back);
Scene second = new Scene(forth);
back.setOnAction((event) -> {
window.setScene(second);
});
forth.setOnAction((event) -> {
window.setScene(first);
});
window.setScene(first);
window.show();
}
public static void main(String[] args) {
launch(BackAndForthApplication.class);
}
}
Beim Starten der obigen Anwendung wird eine Benutzungsoberfläche erstellt, in der durch das Drücken einer Taste von einer Ansicht zur anderen gewechselt werden kann.
Eigenes Layout für jede Ansicht
Lassen Sie uns ein Beispiel betrachten, das zwei verschiedene Ansichten enthält. In der ersten Ansicht wird der oder die Benutzer:in nach einem Passwort gefragt. Wenn ein falsches Passwort eingegeben wird, informiert die Anwendung über den Fehler. Wenn das richtige Passwort eingegeben wird, wechselt die Anwendung zur nächsten Ansicht. Die Anwendung funktioniert wie folgt:

Der Wechsel zwischen den Ansichten erfolgt wie im vorherigen Beispiel. Das konkrete Wechselereignis ist an den Login-Button gebunden. Beim Drücken der Taste überprüft die Anwendung das in das Passwortfeld eingegebene Passwort – hier wird erwartet, dass die Eingabe „password“ lautet. Wenn das Passwort korrekt ist, wird die Ansicht des Fensters geändert. In unserem Beispiel enthält die Ansicht nur den Text „Welcome, this is the beginning!“.
package application;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class PasswordProtectedApplication extends Application {
@Override
public void start(Stage window) throws Exception {
// 1. Ansicht zum Abfragen eines Passworts erstellen
// 1.1 Erstellen der zu verwendenden Komponenten
Label instructionText = new Label("Write the password and press Log in");
PasswordField passwordField = new PasswordField();
Button startButton = new Button("Log in");
Label errorMessage = new Label("");
// 1.2 Layout erstellen und Komponenten hinzufügen
GridPane layout = new GridPane();
layout.add(instructionText, 0, 0);
layout.add(passwordField, 0, 1);
layout.add(startButton, 0, 2);
layout.add(errorMessage, 0, 3);
// 1.3 Layout stylen
layout.setPrefSize(300, 180);
layout.setAlignment(Pos.CENTER);
layout.setVgap(10);
layout.setHgap(10);
layout.setPadding(new Insets(20, 20, 20, 20));
// 1.4 Die Ansicht selbst erstellen und das Layout festlegen
Scene passwordView = new Scene(layout);
// 2. Ansicht zum Anzeigen der Willkommensnachricht erstellen
Label welcomeText = new Label("Welcome, this is the beginning!");
StackPane welcomeLayout = new StackPane();
welcomeLayout.setPrefSize(300, 180);
welcomeLayout.getChildren().add(welcomeText);
welcomeLayout.setAlignment(Pos.CENTER);
Scene welcomeView = new Scene(welcomeLayout);
// 3. Ereignis-Handler für den Login-Button hinzufügen.
// Die Ansicht wird geändert, wenn das Passwort korrekt ist.
startButton.setOnAction((event) -> {
if (!passwordField.getText().trim().equals("password")) {
errorMessage.setText("Unknown password!");
return;
}
window.setScene(welcomeView);
});
window.setScene(passwordView);
window.show();
}
public static void main(String[] args) {
launch(PasswordProtectedApplication.class);
}
}
Das Beispiel nutzt die integrierten Methoden setPrefSize
und setAlignment
von GridPane
und StackPane
für das Layout. Die Methode setPrefSize
nimmt die bevorzugte Größe des Layouts als Argument an, und die Methode setAlignment
wird verwendet, um zu definieren, wie der Inhalt des Layouts ausgerichtet werden soll. Der Parameter Pos.CENTER
wird verwendet, um den Inhalt in der Mitte der Anwendung zu platzieren.
Ansichten mit derselben Hauptrichtung
Manchmal möchte man eine Anwendung mit einer permanenten Ansicht haben, deren Teile bei Bedarf ausgetauscht werden. Typischerweise funktionieren Anwendungen mit Menüs auf diese Weise.
Im folgenden Beispiel gibt es eine Anwendung mit einem Hauptmenü und einem Bereich mit variablem Inhalt. Beim Drücken der Schaltflächen im Hauptmenü ändert sich der Inhalt des Inhaltsbereichs.
package application;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class ExampleApplication extends Application {
@Override
public void start(Stage window) throws Exception {
// 1. Hauptlayout erstellen
BorderPane layout = new BorderPane();
// 1.1 Menü für das Hauptlayout erstellen
HBox menu = new HBox();
menu.setPadding(new Insets(20, 20, 20, 20));
menu.setSpacing(10);
// 1.2 Schaltflächen für das Menü erstellen
Button first = new Button("First");
Button second = new Button("Second");
// 1.3 Schaltflächen zum Menü hinzufügen
menu.getChildren().addAll(first, second);
layout.setTop(menu);
// 2. Unteransichten hinzufügen und diese mit den Menü-Schaltflächen verknüpfen
// 2.1 Layout der Unteransichten erstellen
StackPane firstLayout = createView("First view");
StackPane secondLayout = createView("Second view");
// 2.2 Unteransichten den Schaltflächen hinzufügen. Durch Drücken der Schaltflächen wird die Ansicht geändert
first.setOnAction((event) -> layout.setCenter(firstLayout));
second.setOnAction((event) -> layout.setCenter(secondLayout));
// 2.3 Initiale Ansicht festlegen
layout.setCenter(firstLayout);
// 3. Hauptszene mit Layout erstellen
Scene scene = new Scene
(layout);
// 4. Hauptszene anzeigen
window.setScene(scene);
window.show();
}
private StackPane createView(String text) {
StackPane layout = new StackPane();
layout.setPrefSize(300, 180);
layout.getChildren().add(new Label(text));
layout.setAlignment(Pos.CENTER);
return layout;
}
public static void main(String[] args) {
launch(ExampleApplication.class);
}
}
Die Anwendung funktioniert wie folgt:

Trennung von Anwendungslogik und Benutzungsoberfläche
Die Anwendungslogik und die Benutzungsoberfläche in derselben Klasse oder in denselben Klassen zu belassen, ist normalerweise keine gute Idee. Es erschwert das Testen und Ändern der Anwendung erheblich und macht den Quellcode auch schwieriger lesbar. Das Prinzip „Jede Klasse sollte nur eine klare Verantwortung haben“ gilt auch hier sehr gut.
Schauen wir uns die Trennung von Anwendungslogik und Benutzungsoberfläche an. Nehmen wir an, wir haben ein Objekt, das das folgende Interface verwendet, und wir wollen eine Benutzungsoberfläche zum Speichern von Personen implementieren.
public interface PersonWarehouse {
void save(Person person);
Person search(String socialSecurityNumber);
void delete(Person person);
void delete(String socialSecurityNumber);
void deleteAll();
Collection<Person> getAll();
}
Wenn eine Benutzungsoberfläche implementiert wird, ist ein guter Ausgangspunkt das Zeichnen der Schnittstelle, gefolgt vom Hinzufügen geeigneter Benutzungsoberflächenkomponenten. Beim Speichern von Personen in einer Datenbank benötigen wir ein Feld für den Namen, ein Feld für die Sozialversicherungsnummer und eine Schaltfläche zum Hinzufügen der Person.
Verwenden wir die Klasse GridPane
für das Layout. Die Benutzungsoberfläche besteht aus 3 Zeilen und 2 Spalten. Wir fügen später die Ereignisbehandlung hinzu. Die Initialisierungsmethode sieht folgendermaßen aus:
@Override
public void start(Stage window) {
Label nameText = new Label("Name: ");
TextField nameField = new TextField();
Label secText = new Label("Social security number: ");
TextField secField = new TextField();
Button addButton = new Button("Add person!");
GridPane components = new GridPane();
components.add(nameText, 0, 0);
components.add(nameField, 1, 0);
components.add(secText, 0, 1);
components.add(secField, 1, 1);
components.add(addButton, 1, 2);
// Stil hinzufügen
components.setHgap(10);
components.setVgap(10);
components.setPadding(new Insets(10, 10, 10, 10));
Scene scene = new Scene(components);
window.setScene(scene);
window.show();
}

Als nächstes fügen wir ein Objekt hinzu, das das ActionEvent-Interface implementiert und die Feldwerte in das PersonWarehouse
-Interface speichert.
@Override
public void start(Stage window) {
// ...
addButton.setOnAction((event) -> {
warehouse.save(new Person(nameText.getText(), secText.getText()));
});
// ...
}
Aber woher bekommen wir das konkrete PersonWarehouse
-Objekt? Es wird zu Beginn der start
-Methode erstellt. Hier ist der gesamte Code:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
public class PersonApp extends Application {
@Override
public void start(Stage window) {
PersonWarehouse warehouse = new MyPersonWarehouse();
Label nameText = new Label("Name: ");
TextField nameField = new TextField();
Label secText = new Label("Social security number: ");
TextField secField = new TextField();
Button addButton = new Button("Add person!");
addButton.setOnAction((event) -> {
warehouse.save(new Person(nameField.getText(), secField.getText()));
});
GridPane components = new GridPane();
components.add(nameText, 0, 0);
components.add(nameField, 1, 0);
components.add(secText, 0, 1);
components.add(secField, 1, 1);
components.add(addButton, 1, 2);
// Stil hinzufügen
components.setHgap(10);
components.setVgap(10);
components.setPadding(new Insets(10, 10, 10, 10));
Scene scene = new Scene(components);
window.setScene(scene);
window.show();
}
public static void main(String[] args) {
launch(PersonApp.class);
}
}
Eine etwas größere Anwendung: Vokabeltraining
Skizzieren wir eine Anwendung, die zum Üben von Fremdsprachenvokabeln verwendet werden kann. Die Anwendung bietet dem oder der Benutzer:in zwei Funktionen: das Eingeben von Wörtern und deren Übersetzungen sowie das Üben mit den gespeicherten Wörtern. Wir erstellen vier verschiedene Klassen für die Anwendung: Die erste Klasse bietet die Kernlogik, also die Pflege des Wörterbuchs. Die zweite und dritte Klasse enthalten die Eingabeansicht und die Übungsansicht, und die vierte Klasse enthält das Hauptmenü sowie die Funktionalität, die zum Starten der Anwendung erforderlich ist.
Wörterbuch
Das Wörterbuch wird mithilfe einer HashMap und einer Liste implementiert. Die HashMap enthält die Wörter und deren Übersetzungen, und die Liste wird verwendet, um zufällig ein Wort zum Üben auszuwählen. Die Klasse enthält die erforderlichen Methoden zum Hinzufügen einer Übersetzung, zum Abrufen einer Übersetzung und zum Ziehen eines zufälligen Wortes.
package application;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
public class Dictionary {
private List<String> words;
private Map<String, String> translations;
public Dictionary() {
this.words = new ArrayList<>();
this.translations = new HashMap<>();
add("sana", "word");
}
public String get(String word) {
return this.translations.get(word);
}
public void add(String word, String translation) {
if (!this.translations.containsKey(word)) {
this.words.add(word);
}
this.translations.put(word, translation);
}
public String getRandomWord() {
Random random = new Random();
return this.words.get(random.nextInt(this.words.size()));
}
}
Sie könnten das Wörterbuch auch so implementieren, dass das Zurückgeben eines zufälligen Wortes immer eine neue Liste von Wörtern aus den Schlüsseln der Übersetzungs-HashMap generiert. In diesem Fall wäre keine separate Liste von Wörtern erforderlich. Dies würde jedoch die Leistung des Programms beeinflussen (oder es hätte dies vor der Jahrtausendwende getan – Computer sind heutzutage etwas schneller...).
Neue Wörter eingeben
Erstellen wir nun die Funktionalität, die zum Eingeben von Wörtern benötigt wird. Dafür benötigen wir eine Referenz zum Wörterbuch-Objekt sowie Textfelder für das Wort und dessen Übersetzung. Das GridPane-Layout funktioniert gut für die Felder. Erstellen Sie eine Klasse namens InputView
, die die Methode getView
enthält, die die Ansicht erstellt, die zum Eingeben neuer Wörter erforderlich ist. Diese Methode sollte eine Referenz zu einem Parent-Objekt zurückgeben. Parent ist eine Superklasse vieler Klassen, darunter alle für Layouts verwendeten Klassen. Daher kann jedes Layout als Parent-Objekt dargestellt werden.
Die Klasse definiert auch, was passiert, wenn eine Schaltfläche in der Benutzungsoberfläche gedrückt wird. Wenn die Schaltfläche gedrückt wird, wird das neue Wort zum Wörterbuch hinzugefügt. Die Textfelder werden ebenfalls geleert, sodass das nächste Wort eingegeben werden kann.
package application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
public class InputView {
private Dictionary dictionary;
public InputView(Dictionary dictionary) {
this.dictionary = dictionary;
}
public Parent getView() {
GridPane layout = new GridPane();
Label wordInstruction = new Label("Word");
TextField wordField = new TextField();
Label translationInstruction = new Label("Translation");
TextField translationField = new TextField();
layout.setAlignment(Pos.CENTER);
layout.setVgap(10);
layout.setHgap(10);
layout.setPadding(new Insets(10, 10, 10, 10));
Button addButton = new Button("Add the word pair");
layout.add(wordInstruction, 0, 0);
layout.add(wordField, 0, 1);
layout.add(translationInstruction, 0, 2);
layout.add(translationField, 0, 3);
layout.add(addButton, 0, 4);
addButton.setOnMouseClicked((event) -> {
String word = wordField.getText();
String translation = translationField.getText();
dictionary.add(word, translation);
wordField.clear();
translationField.clear();
});
return layout;
}
}
Vokabeltraining
Nun erstellen wir die Funktionalität, um das Wissen über die gespeicherten Wörter zu üben. Wir benötigen eine Referenz zum Wörterbuch-Objekt, damit wir eine Quelle für die Übungswörter haben und überprüfen können, ob die Übersetzung korrekt ist. Zusätzlich zum Wörterbuch benötigen wir eine Textkomponente, die den oder die Benutzer:in darüber informiert, welches Wort übersetzt werden soll, und
ein Textfeld, in das die Übersetzung eingegeben werden kann. Auch hier funktioniert das GridPane-Layout gut, um die Felder anzuordnen.
Das zu übersetzende Wort ist jeweils eine Objektvariable der Klasse. Die Objektvariable kann auch in der Methode verwendet und geändert werden, die im Kontext eines Ereignis-Handlers definiert ist.
package application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
public class PracticeView {
private Dictionary dictionary;
private String word;
public PracticeView(Dictionary dictionary) {
this.dictionary = dictionary;
this.word = dictionary.getRandomWord();
}
public Parent getView() {
GridPane layout = new GridPane();
Label wordInstruction = new Label("Translate the word '" + this.word + "'");
TextField translationField = new TextField();
layout.setAlignment(Pos.CENTER);
layout.setVgap(10);
layout.setHgap(10);
layout.setPadding(new Insets(10, 10, 10, 10));
Button addButton = new Button("Check");
Label feedback = new Label("");
layout.add(wordInstruction, 0, 0);
layout.add(translationField, 0, 1);
layout.add(addButton, 0, 2);
layout.add(feedback, 0, 3);
addButton.setOnMouseClicked((event) -> {
String translation = translationField.getText();
if (dictionary.get(word).equals(translation)) {
feedback.setText("Correct!");
} else {
feedback.setText("Incorrect! The translation for the word '" + word + "' is '" + dictionary.get(word) + "'.");
return;
}
this.word = this.dictionary.getRandomWord();
wordInstruction.setText("Translate the word '" + this.word + "'");
translationField.clear();
});
return layout;
}
}
Übungsanwendung
Die Übungsanwendung vereint die zuvor erstellten Klassen und bietet das Hauptmenü der Anwendung. Die Struktur der Übungsanwendung sieht wie folgt aus:
package application;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class PracticeApplication extends Application {
private Dictionary dictionary;
@Override
public void init() throws Exception {
// 1. Wörterbuch erstellen, das die Anwendung verwendet
this.dictionary = new Dictionary();
}
@Override
public void start(Stage stage) throws Exception {
// 2. Ansichten ("Unteransichten") erstellen
PracticeView practiceView = new PracticeView(dictionary);
InputView inputView = new InputView(dictionary);
// 3. Layout auf höherer Ebene erstellen
BorderPane layout = new BorderPane();
// 3.1 Menü für das allgemeine Layout erstellen
HBox menu = new HBox();
menu.setPadding(new Insets(20, 20, 20, 20));
menu.setSpacing(10);
// 3.2 Menütasten erstellen
Button enterButton = new Button("Enter new words");
Button practiceButton = new Button("Practice");
// 3.3 Schaltflächen zum Menü hinzufügen
menu.getChildren().addAll(enterButton, practiceButton);
layout.setTop(menu);
// 4. Unteransichten mit den Schaltflächen verbinden. Ein Klick auf die Menütasten ändert die Unteransicht.
enterButton.setOnAction((event) -> layout.setCenter(inputView.getView()));
practiceButton.setOnAction((event) -> layout.setCenter(practiceView.getView()));
// 5. Zuerst die Eingabeansicht anzeigen
layout.setCenter(inputView.getView());
// 6. Hauptansicht erstellen und das Layout auf höherer Ebene hinzufügen
Scene view = new Scene(layout, 400, 300);
// 7. Anwendung anzeigen
stage.setScene(view);
stage.show();
}
public static void main(String[] args) {
launch(PracticeApplication.class);
}
}