JavaFX ist ein auf Java basierendes UI-Framework insbesondere für Desktop-Anwendungen. Es kann auch zur Entwicklung mobiler Anwendungen für spezifische eingebettete Systeme und für die sehr verbreiteten Zielplattformen Android und iOS eingesetzt werden. Als Laufzeitumgebung bietet sich für diesen Fall die Gluon VM an.

JavaFX war seit Version 7 (2012) bis inkl. Version 10 (2018) Teil der Java SE und damit das standardmäßig empfohlene UI-Framework. Im Zuge der Modularisierung und Verschlankung der Java SE ist auch JavaFX seit Java 11 nicht mehr Teil des JDK und muss als separate Bibliothek eingebunden werden. Oracle erläutert diese Entscheidung in einem Blog-Artikel. Die Weiterentwicklung von JavaFX erfolgt heute über die Open Source-Community OpenJFX. Da OpenJFX unter der GPL veröffentlicht ist, lassen sich mittels OpenJFX auf Basis des OpenJDK nun freie, nicht durch Lizenzen eingeschränkte Desktop-Anwendungen mit Java konstruieren, was nicht nur für freie Linux-Distributionen erfreulich ist. In der Dokumentation zu OpenJFX finden sich ausführliche und bebilderte Installationsanleitungen für verschiedene Entwicklungsumgebungen und Build-Management-Tools.

JavaFX stellt eine Vielzahl von grundlegenden Steuerungselementen (sogenannte UI Controls) zur Interaktion mit dem Anwender bereit. Dazu zählen Button, Text Field, Radio Button, Checkbox, List View, Table View, Slider, Progress Bar usw. Die Darstellung der Steuerungselemente kann individuell über ein Look and Feel-Theme angepasst werden – z.B. mittels JFoenix an das Material Design von Google. Zum Einstieg bietet sich der grafische Editor SceneBuilder an, der es erlaubt das UI per Drag & Drop zusammenzustellen. SceneBuilder lässt sich in geläufige Java-Entwicklungsumgebungen wie IntelliJ oder Eclipse integrieren, um schnell zwischen der grafischen Editoransicht und dem generierten deklarativen Code wechseln zu können. Für JavaFX-Anfänger bietet der SceneBuilder-Editor die Möglichkeit die vorhandenen Steuerungselemente und ihre anpassbaren Eigenschaften explorativ zu erkunden und zu beobachten, welcher Code durch die Zusammenstellung per Drag & Drop entsteht. Weiter unten folgt ein Video, das eine Einführung in den SceneBuilder-Editor bietet.

Trennung von Präsentation und Steuerung einer View

Bevor wir die Motivation hinter der Trennung von Präsentation und Steuerungslogik in einem UI erläutern, klären wir kurz den allgemeinen Begriff "View": Als eine View bezeichnen wir hier den Ausschnitt des gesamten UI, der dem Anwender aktuell angezeigt wird. Das gesamte UI besteht also aus mehreren Views, zwischen deren Anzeige der Anwender i.d.R. durch eigene Interaktion, wie das Anklicken eines Buttons, navigiert. Die möglichen Transitionen zwischen den Views einer Anwendung können als Zustandsautomat modelliert werden. Je nach UI-Framework werden die Views unterschiedlich bezeichnet: In der Web-Entwicklung wird klassischerweise von Seiten (engl. Pages) oder in neueren Web-Frameworks wie Angular, Vue.js oder React generisch von Komponenten (engl. Components) gesprochen. Im Android SDK navigiert der Anwender zwischen unterschiedlichen Activities (oder ihren Fragmenten). In JavaFX wurde zur Bezeichnung der Views eine Analogie zur Theaterbühne (engl. Stage) gewählt, auf der mehrere Szenen (engl. Scenes) mit unterschiedlichem Bühnenbild ablaufen. Das Fenster, in dem eine Anwendung dargestellt wird, ist dabei die Stage. Der Anwender kann durch Interaktion zwischen unterschiedlichen Scenes wechseln.

import javafx.application.Application;
import javafx.stage.Stage;
    
public class HelloWorldApp extends Application {

    public static void main(String[] args) {
        launch(args);
    }
    
    public void start(Stage stage) {
        stage.setTitle("Hello World!");
        stage.show();
    }
}

Der Inhalt einer Scene besteht in JavaFX aus mehreren Knoten (die entsprechende Klasse heißt Node), die in einer Baumstruktur ineinander verschachtelt sind. Diese Baumstruktur wird in JavaFX als Scene Graph bezeichnet und entspricht konzeptionell dem Document Object Model (DOM) in der Web-Entwicklung. Container-Knoten, die mehrere andere Knoten als Kindelemente aufnehmen können, werden als Parent bezeichnet. Jede Scene hat einen Wurzelknoten, den Root Parent Node. Eine beliebig tief geschachtelte Baumstruktur entsteht dadurch, dass ein Container-Knoten auch andere Container-Knoten als Kindelemente aufnehmen kann. Grafische Animationen, wie z.B. Verschiebungen, Rotationen und Skalierungen oder andere Effekte, die auf einen Parent angewendet werden, gelten auch für ihre untergeordneten Kindelemente. Konkrete Knoten, d.h. die Unterklassen der allgemeinen Klasse Node sind sämtliche Klassen für Steuerungselemente, Bilder oder Texte, die im UI angezeigt werden. Die folgende Abbildung und das folgende Code-Beispiel verdeutlichen den Aufbau eines Scene Graph in JavaFX.

HBox rootNode = new HBox(); // HBox is a concrete Parent subclass
Scene scene = new Scene(rootNode);

Node node = new Button("Button 1"); // Button is a concrete Node subclass
rootNode.getChildren().add(node);

stage.setScene(scene);
stage.show();

Wie bei den anderen oben genannten UI-Frameworks aus dem Web- und Mobile-Bereich hat es sich auch in JavaFX bewährt, die Präsentation einer View von ihrer Steuerungslogik zu trennen. In nahezu alle modernen UI-Frameworks wird aktuell der Ansatz verfolgt, die grundlegende Präsentation einer View als Baumstruktur deklarativ in XML oder HTML zu formulieren und die zugehörige Steuerungslogik in einer imperativen Programmiersprache wie Java. Vor- und Nachteile dieses Ansatzes sollen kurz zusammengefasst werden:

Ein weiterer Vorteil der klaren Trennung von Präsentation und Steuerung einer View in der Anwendungsentwicklung ist, dass die Rollenaufteilung zwischen spezialisierten Teammitgliedern vereinfacht wird: UI-Designer sind i.d.R. den Umgang mit deklarativen Auszeichnungssprachen wie HTML gewohnt und müssen sich nicht mehr mit der imperativen Programmiersprache auseinandersetzen. Im MVC-Kapitel ist bereits vorgestellt worden, wie das MVC-Muster zum MVVM-Muster ausgebaut werden kann, um die Steuerungslogik der View (= ViewModel in MVVM) unabhängig von einer konkreten Präsentation (= View in MVVM) zu gestalten. Die Präsentation kann dann relativ einfach ausgetauscht werden, ohne die Steuerungslogik der View anpassen zu müssen – z.B. von FXML zu HTML oder umgekehrt. Im Kapitel MVC und MVVM in JavaFX ist bereits dargestellt worden, wie eine einfache View inkl. Controller bzw. ViewModel in JavaFX aufgebaut werden kann.

Layout Panes

Ein Pane ist in JavaFX ein bestimmter Container-Knoten, in dem mehrere andere Knoten vereint und nach einem bestimmten Layout-Prinzip im UI angeordnet werden. Die wichtigsten der vorgefertigten Layout Panes sollen im Folgenden kurz erläutert werden.

Navigation zwischen Views

Die verschiedenen Views einer Anwendung teilen sich häufig bestimmte inhaltliche Bereiche wie einen Header, einen Footer oder eine Sidebar. Das Navigationsmenu selbst ist i.d.R. in einem dieser Bereiche untergebracht. Es ergeben sich zwei alternative Ansätze, um die Navigation zwischen den Views einer Anwendung zu realisieren, ohne den Code für die geteilten Bereiche zu duplizieren:

Beispielanwendung "Yacht Shop"

In folgendem Video wird eine JavaFX-Beispielanwendung vorgestellt, die mit der im Spring-Kapitel erstellten REST-API kommuniziert. Spezifische JavaFX-UI-Elemente werden im Video hervorgehoben. Der vollständige Code der Beispielanwendung findet sich im Verzeichnis /ui/javafx-shop des Modul-Repository.

Die Anwendung folgt dem Ansatz "Single scene, dynamic content" und lädt dynamisch 3 unterschiedliche Views in Form von FXML-Dateien in den Inhaltsbereich eines ViewHolder-Pane. Die 3 Views bilden folgende Anwendungsfälle ab: Zusätzlich besteht die Anwendung noch aus 2 weiteren FXML-Dateien: Im Folgenden wird der Code auszugsweise und teilweise vereinfacht anhand ausgewählter Aspekte erläutert.

FXML, Controller und CSS in JavaFX

Das folgende Video zeigt, wie das Anmeldeformular einer Anwendung mittels des SceneBuilder-Editors ohne Programmierkenntnisse entworfen werden kann. Als Container werden im Video exemplarisch nur eine VBox und eine HBox verwendet, die verschiedene UI-Steuerungselemente aufnehmen und ineinander geschachtelt werden.

Die Ausgabe des Entwurfsvorgangs im SceneBuilder ist eine FXML-Datei, hier namens login.fxml, die noch nicht an einen Controller gebunden ist. Der Login-Button hat dadurch noch keine Funktionalität. In dem folgenden Code-Beispiel wird dem Anmeldeformular durch die Referenz auf die Klasse LoginController in Zeile 1 ein Controller zugeordnet, der automatisch durch das JavaFX-Framework instantiiert wird. Die Methode initialize versehen mit der Annotation @FXML (Zeilen 12-15 in LoginController) wird beim Erzeugen des Controllers durch das Framework ausgeführt. In Zeile 1 der FXML-Datei wird zusätzlich die CSS-Datei app.css eingebunden, so dass alle UI-Elemente die dort definierten Stylesheet-Klassen über das Attributs styleClass adressieren können.

public class LoginController {

    @FXML
    private TextField username;

    @FXML
    private PasswordField password;

    @FXML
    private Label loginFailure;
    
    @FXML
    public void initialize() {
        // executed when the controller object is created by the framework
    }
    
    public void loginAction(ActionEvent e1) {
        // execute login request in async task
        PostLoginTask loginTask = new PostLoginTask(username.getText(), password.getText());
        new Thread(loginTask).start();

        // login task response handler
        loginTask.setOnSucceeded((WorkerStateEvent e2) -> {
            // login is successfull if the return value is not null
            if (loginTask.getValue() == null) {
                loginFailure.setVisible(true);
                return;
            }
            // navigate to catalog view
            MainController.getInstance().changeView("catalog");
        });
    }
}
.button-raised {
    -jfx-button-type: RAISED;
    -fx-background-color: deeppink;
    -fx-text-fill: white;
    -fx-font-size: 14; 
    -fx-padding: 8 20;       
}

.input {
    -fx-max-width: 200;
}

#loginFailure {
    -fx-text-fill: red;
    -fx-font-size: 16;
}
Die UI-Elemente aus der FXML-Datei können an Attribute in der Controller-Klasse gebunden werden (Two-Way-Binding). Auch diese Attribute werden per Dependency Injection durch das Framework instantiiert. Dazu muss ...

Die Methode loginAction (Zeilen 17-32 in LoginController) ist durch das Attribut onAction="#loginAction" in der FXML-Deklaration des Login-Buttons als EventHandler an diesen Button gebunden. Die Implementierung dieser Methode, inbesondere die Klasse PostLoginTask, wird weiter unten genauer erläutert.

Das Styling der UI-Elemente erfolgt in JavaFX wie aus der Web-Entwicklung bekannt über Cascading Style Sheets (CSS). Es ist demzufolge zu empfehlen, dass eine FXML-Datei nur die strukturelle Anordnung der UI-Elemente definiert und deren Styling über wiederverwendbare Stylesheet-Klassen in eine separate CSS-Datei ausgelagert wird. Eine Styling-Klasse kann über das Attribut styleClass adressiert werden. Über das Attribut style können die CSS-Properties eines UI-Elements genau wie in HTML direkt verändert werden. Eine Referenz über alle verfügbaren CSS-Properties in JavaFX findet sich in der Dokumentation von Oracle.

Das folgende Code-Beispiel zeigt exemplarisch, wie ein Button alternativ aus dem imperativen Java-Code heraus einer View hinzugefügt werden kann. Das ist insbesondere für die Kindelemente von Listen (ListView) und Tabellen (TableView) sinnvoll, die zur Laufzeit dynamisch angelegt werden müssen, weil ihre Ausprägungen zur Entwurfszeit noch nicht bekannt sind.

class LoginController {

    @FXML
    private Pane container;

    @FXML
    protected void initialize() {
        Button loginButton = new Button("LOGIN");
        container.getChildren().add(loginButton); // add button to a parent container

        // register button event handler
        loginButton.setOnAction((e) -> {
            // process login action here ...
        });

        // button styling
        loginButton.getStyleClass().add("button-raised");
        loginButton.setStyle("-fx-background-color: deeppink");
    }
}

Asynchrone Tasks in einer JavaFX-Anwendung

In einer JavaFX-Anwendung wird das UI aus dem sogenannten JavaFX Application Thread heraus gesteuert. Nur aus diesem Thread heraus darf das UI zur Laufzeit manipuliert werden, z.B. durch das Hinzufügen von Knoten zum Scene Graph. Es können in diesem Thread also nur nicht-blockierende EventHandler ausgeführt werden, damit sichergestellt ist, dass das UI stets auf Eingaben des Anwenders reagieren kann. Blockierende Aufrufe (wie z.B. das Senden eines HTTP-Request an eine REST-API) hingegen sind in nebenläufige Threads zu verschieben. Aus Sicht des JavaFX Application Thread wird in diesem Fall eine asynchrone Aufgabe (engl. Task) gestartet, deren Ergebnis – sobald es verfügbar ist – über eine Callback-Funktion verarbeitet wird, um das UI ggf. zu aktualisieren. Es bietet sich für derartige asynchrone Aufgaben in Java an, die Klasse Task zu erweitern. Diese Klasse implementiert die Interfaces Future und Runnable (s. Kapitel Futures und parallele Streams). Das folgende Code-Beispiel zeigt, wie das Senden eines HTTP-Request an eine REST-API und die Verarbeitung der zugehörigen HTTP-Response in eine spezielle, nebenläufig ausgeführte Task ausgelagert wird. In diesem Fall wird der HTTP-Request durch das Klicken auf den Login-Button im JavaFX Application Thread ausgelöst. Serverseitig soll geprüft werden, ob der Anwender eine gültige Kombination aus Username und Passwort eingegeben hat, und der Loginversuch damit erfolgreich war oder eben nicht.

// the following code is executed in the event handler of the login button

PostLoginTask loginTask = new PostLoginTask(username, password);
new Thread(loginTask).start();

// login task response handler
loginTask.setOnSucceeded((WorkerStateEvent e) -> {
    User user = loginTask.getValue();
    if (user != null) {
        // login is successfull
    }
    else {
        // login is not successfull
    }
});
import javafx.concurrent.Task;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.http.HttpResponse;
// ...

public class PostLoginTask extends Task<User> {

    private String username;
    private String password;

    public PostLoginTask(String username, String password) { this.username = username; this.password = password; }

	@Override
	protected User call() {
		String url = "https://example.com/login";
		String json = "{\"name\": \"" + username + "\", \"passwordHash\": \"" + password + "\"}";

		try {
			HttpResponse<String> res = Unirest.post(url).header("Content-Type", "application/json").body(json).asString();
			if (res.getStatus() != 403) {
				String jsonWebToken = res.getHeaders().getFirst("Authorization");
				return new User(username, jsonWebToken);
			}
		} catch (UnirestException e) {
			e.printStackTrace();
		}
		return null;
	}
}

Animationen in JavaFX

In einem Spiel bewegen sich häufig unterschiedliche Objekte über den Bildschirm, die miteinander im Rahmen der Spielregeln interagieren. In JavaFX wird zur Animation von Objekten die abstrakte Klasse AnimationTimer bereitgestellt. Mit dieser Klasse kann ein Timer erstellt werden, der in jedem Frame aufgerufen wird, während er aktiv ist. Die Frame-Rate ist variabel und hängt von der zugrundeliegenden Ausführungsumgebung und Hardware ab. Grundsätzlich ist es zu empfehlen, die Frame-Rate, in der das UI aktualisiert wird, und die Spielphysik voneinander zu entkoppeln (siehe Diskussion hier). Zur Verwendung eines konkreten AnimationTimer muss die Methode handle sinnvoll überschrieben werden. Die Methoden start und stop erlauben das Starten bzw. Stoppen des Timers. Das folgende Code-Beispiel erzeugt eine Anwendung, in der ein animierter Ball mit fixer Geschwindigkeit an den Kanten des Anwendungsfensters abprallt (siehe SingleBouncingBall.java).

import javafx.application.Application;
import javafx.animation.AnimationTimer;            
// ...

public class SingleBouncingBall extends Application {

    @Override
    public void start(Stage stage) {

        final Pane ballContainer = new Pane();
        final Scene scene = new Scene(ballContainer, 600, 400);
        final Ball ball = new Ball(300, 200, 25, Color.DARKGRAY, 300,  Math.toRadians(45));
        ballContainer.getChildren().add(ball);

        stage.setScene(scene);
        stage.show();

        final LongProperty lastUpdateTime = new SimpleLongProperty(0);
        final AnimationTimer timer = new AnimationTimer() {
            @Override
            public void handle(long now) {
                if (lastUpdateTime.get() > 0) {

                    // change the ball position
                    double elapsedSeconds = (now - lastUpdateTime.get()) / 1_000_000_000.;
                    ball.setCenterX(ball.getCenterX() + elapsedSeconds * ball.velocity * Math.cos(ball.angle));
                    ball.setCenterY(ball.getCenterY() + elapsedSeconds * ball.velocity * Math.sin(ball.angle));

                    // bounce against a vertical wall
                    if (ball.getCenterX() <= ball.getRadius() 
                            || ball.getCenterX() >= (ballContainer.getWidth() - ball.getRadius())) {
                        ball.angle = Math.PI - ball.angle;
                    }
                    // bounce against a horizontal wall
                    if (ball.getCenterY() <= ball.getRadius() 
                            || ball.getCenterY() >= (ballContainer.getHeight() - ball.getRadius())) {{
                        ball.angle = 2 * Math.PI - ball.angle;
                    }
                }
                lastUpdateTime.set(now);
            }
        };
        timer.start();
    }

    public static void main(String[] args) { launch(); }
}
import javafx.scene.shape.Circle;

class Ball extends Circle {
    double velocity;
    double angle;

    Ball(double centerX, double centerY, double radius, Paint fill, double velocity, double angle) {
        super(centerX, centerY, radius, fill);
        this.velocity = velocity;
        this.angle = angle;
    }
}

Die Klasse Ball erweitert die Klasse javafx.scene.shape.Circle. Ein Circle ist ein spezieller Shape, der als Node dem Scene Graph hinzugefügt werden kann. Weitere Shapes sind Rectangle, Polygon, Text, SVGPath, uvm. Alternativ zur Verwendung von Shapes können animierte Objekte in JavaFX auch auf einer Canvas über deren GraphicsContext gezeichnet werden. Die beiden folgenden Code-Beispiele visualisieren jeweils den Text "Hello World!" und einen roten Kreis – im ersten Code-Beispiel werden dazu Shapes angelegt, im zweiten Code-Beispiel wird auf eine Canvas gezeichnet.

// display text and circle as shapes
Pane shapesContainer = new Pane();

Shape text = new Text(20, 20, "Hello World!"); // x = 20, y = 20
shapesContainer.getChildren().add(text);

Shape circle = new Circle(100, 100, 30, Color.RED); // centerX = 100, centerY = 100, radius = 30
shapesContainer.getChildren().add(circle);
// draw text and circle on canvas
Canvas canvas = new Canvas(300, 200); // canvas is 300 x 200 pixels
GraphicsContext gc = canvas.getGraphicsContext2D();

gc.fillText("Hello World!", 20, 20); // x = 20, y = 20

gc.setFill(Color.RED);
gc.fillOval(100, 100, 60, 60); // x = 100, y = 100, width = 60, height = 60 => radius = 30

Die alternativen Implementierungen in den Klassen BouncingBallsShapes.java und BouncingBallsCanvas.java lassen erkennen, dass im Vergleich von Shapes- und Canvas-Animationen sich keine signifikanten Unterschiede hinsichtlich der Frame-Rate und des Hauptspeicherbedarfs ergeben, aber die Shapes-Animationen eine etwas höhere CPU-Auslastung verursachen. Das folgende Video demonstriert diese Beobachtungen.

Simulation der Spielphysik

Die Spielphysik kann – wie in den obigen Beispielen – durch stark vereinfachte Annahmen bei der Kollision von Objekten eigenständig implementiert werden. Wenn in der Spielphysik allerdings auch spezielle Effekte und Kräfte wie Reibung, Dichte oder Rotationsgeschwindigkeit berücksichtigt werden sollen, bietet sich die Verwendung einer Physik-Engine wie z.B. dyn4j an. Die Physik-Engine ist unabhängig von JavaFX und kann gleichermaßen mit jedem anderen UI-Framework verwendet werden.

In dyn4j wird eine 2D-Welt simuliert. Die Welt (Klasse World) kann diverse Körper (Klasse Body) enthalten, die ihrerseits aus mehreren miteinander verbundenen Körperteilen (Klasse BodyFixture) bestehen (s. Getting Started zu dyn4j). Auf die Körper können Kräfte und Impulse wirken. Das folgende Code-Beispiel zeigt wie einer dyn4j-Welt ein rechteckiger und ein runder Körper hinzugefügt werden.

World world = new World();

Body rect = new Body();
rect.addFixture(Geometry.createRectangle(width, height)); // 1st body fixture is a rectangle
rect.setMass(MassType.INFINITY);
body.translate(0, 0); // sets the rectangle's center (x, y)
world.addBody(rectangle);

Body circle = new Body();
circle.addFixture(Geometry.createCircle(radius), density, friction, restitution);
circle.setMass(MassType.NORMAL);
circle.translate(0, 10); // sets the circle's center (x, y)
world.addBody(circle);

world.setGravity(World.EARTH_GRAVITY); // the circle will fall down, the rectangle is fixed due to an infinite mass

circle.applyImpulse(new Vector2(x, y)); // applies an impulse on the circle in the direction of (x, y)

Mittels der Methode update auf der Klasse World kann die Simulation der Welt in flexiblen zeitlichen Intervallen fortgeführt werden.

ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1);
scheduler.scheduleAtFixedRate(() -> {
    world.update(interval); // simulation of the world model is updated here
}, 0, interval, TimeUnit.MILLISECONDS);

Das folgende Video zeigt eine Beispielanwendung, in der dyn4j zur Kollisionserkennung der animierten Objekte eingesetzt wird. Der Ball kann über die Pfeiltasten gesteuert werden. Durch Mausklicks können herabfallende Rechtecke erzeugt werden. Der vollständige Code der Beispielanwendung findet sich in der Klasse BouncingBallDyn4j im Verzeichnis /ui/javafx-animation des Modul-Repository.