JavaFX: MVVM

JavaFX Serie

Wir haben uns im letzten Beitrag etwas mit der Aufteilung in Model / View / Controller beschäftigt und mussten in der View für das Binding teilweise auf Code ausweichen. Dies haben wir in Form von kleinen JavaScript Elementen eingefügt, was natürlich nicht unbedingt optimal ist.

Besonders schwerwiegend war, dass wir uns vom SceneBuilder verabschiedet haben, der hier mit gewissen Teilen Probleme bekommen hat.

Um diese Problematik aufzulösen, kommen wir automatisch zu einem neuen Pattern: Aus dem Model View Controller (MVC) Pattern wird ein Model – View – View Model (MVVM) Pattern.

Die erste wichtige Veränderung: Unsere View wird aus zwei Bestandteilen bestehen: Wie gewohnt das fxml aber es kommt auch eine Java Datei hinzu. Dies macht Sinn, denn wir haben bereits festgestellt, dass wir Code in der View benötigen und daher können wir dies in eine separate java Datei packen.

Desweiteren bekommen wir ein View Model. Für unser Binding brauchten wir in den vorhergehenden Beiträgen ein Model, welches mit JavaFX spezifischen Properties arbeitete. Wenn wir aber Software Entwickeln, dann hatten unsere Models (hoffentlich) statt z.B. (Simple)StringProperties immer Strings. Wir hatten hier also immer einen massiven Mehraufwand und den können wir jetzt gezielt abbilden: Wir haben ein dediziertes View Model. In diesem verwenden wir dann unser Model, das im Rahmen der „normalen Java Entwicklung“ entstanden ist.

Library für MVVM

Rund um MVVM gibt es sehr viel Dokumentation. Microsoft hat sehr stark auf MVVM zugegriffen z.B. in seinem Windows Presentation Framework (WPF). Ein Projekt hat dies auf JavaFX adaptiert: mvvmFX. Im Wiki finden sich zu MVVM auch gute Links, die das Thema sehr ausführlich beschreiben und ich kann nur raten, dies im Detail zu lesen um ein tiefes Verständnis zu bekommen.

Anpassen unseres Projekts

Solltest Du der Serie nicht gefolgt sein: Der Code der ganzen JavaFX Serie findet sich unter https://github.com/kneitzel/blog-javafx-series. Der MVC Code in „04 helloworld-mvc“ und die mvvm Version in „05 helloworld-mvvm“

Abhängigkeit

Wir wollen die mvvmFX Library nutzen, daher tragen wir diese als Abhängigkeit in unsere build.gradle Datei ein, indem wir in dem dependency Block einfügen:

compile group: 'de.saxsys', name: 'mvvmfx', version: '1.8.0'

Neue Klasse: helloworld.entity.Greeting

Im Namespace helloworld legen wir eine neue Klasse an: entity.Greeting, in der wir unsere Business Logik / Daten für eine Begrüßung hinterlegen:

package helloworld.entity;

public class Greeting {
private String greeting;

public Greeting() {
greeting = "Hallo Welt!";
}

public Greeting(final String name) {
greetName(name);
}

public String getGreeting() { return greeting; }

public void greetName(final String name) {
if (name == null || name.isEmpty()) {
greeting = "Hallo Welt!";
} else {
greeting = "Hallo " + name + "!";
}
}
}

Dies ist eine einfache Klasse mit Daten und Business Logik, ganz ohne irgend welche JavaFX Spezialitäten wie StringProperties und Co. Die Klasse wirkt von Design etwas gekünstelt, aber wir nehmen dies an dieser Stelle einfach so hin.

Umbenennen von Dateien

Nun wollen wir einige Dateien umbenennen:

a) View

Zu der View gehören ab jetzt das fxml File und das bisherige Controller File. Daher benennen wir diese um in HelloWorldView(.java|.fxml)

b) ViewModel

Das bisherige Model wird zum ViewModel, daher benennen wir die Datei um in HelloWorldViewModel.java

Das Umbenennen machen wir mit dem Refactoring der IDE, damit es immer gleich an allen Stellen geändert ist.

Anpassungen der View

Als erstes passen wir die HelloWorldView.fxml an:

  • Wir löschen die Referenz auf JavaScript <?language javascript?>
  • Das Label mit dem Greeting bekommt ein fx:id=“greetingLabel“ und das Binding für text wird gelöscht (text=“${controller.model.greeting}“)
  • Das fx:script wird komplett entfernt.

Dann passen wir die HelloWorldView.java an:

package helloworld;

import de.saxsys.mvvmfx.FxmlView;
import de.saxsys.mvvmfx.InjectViewModel;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;

import java.net.URL;
import java.util.ResourceBundle;

public class HelloWorldView implements FxmlView<HelloWorldViewModel>, Initializable {

/**
* Model with our data.
*/
@InjectViewModel
private HelloWorldViewModel model;


/**
* TextField for name.
*/
@FXML private TextField nameTextField;

/**
* Label for greeting
*/
@FXML private Label greetingLabel;

/**
* Close application.
* @param e ActionEvent (unused).
*/
public void closeApplicationAction(final ActionEvent e) {
model.closeApplication();
}

/**
* Updates Greeting in model.
* @param e ActionEvent (unused).
*/
public void updateGreeting(final ActionEvent e) { model.updateGreeting();}

@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
nameTextField.textProperty().bindBidirectional(model.nameProperty);
greetingLabel.textProperty().bind(model.greetingProperty);
}
}

Die folgenden wichtigen Änderungen sind sichtbar:

  • Es wird nun FxmlView<HelloWorldViewModel>, Initializable implementiert.
  • Das Model wird nicht mehr selbst erzeugt sondern kommt per Injection.
  • Die Controls sind nun in der View bekannt (greetingLabel und nameTextField).
  • Die Business-Logik (Beenden der Applikation) ist in das ViewModel gewandert.
  • Das Binding ist nun in der initialize Methode.

Die ViewModel Klasse wird auch angepasst:

package helloworld;

import de.saxsys.mvvmfx.ViewModel;
import helloworld.entity.Greeting;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

/**
* Our Model for Hello World.
*/
public class HelloWorldViewModel implements ViewModel {

/**
* Our Model to use.
*/
private Greeting greeting;

/**
* Greeting to show.
*/
public StringProperty greetingProperty = new SimpleStringProperty();
public StringProperty nameProperty = new SimpleStringProperty("Bitte Namen eingeben");

public HelloWorldViewModel() {
greeting = new Greeting("Welt");
updateViewModel();
}

/**
* Updates the greeting to use the name given.
*/
public void updateGreeting() {
if (nameProperty.get().length() > 0) {
greeting.greetName(nameProperty.get());
nameProperty.setValue("Bitte Namen eingeben");
} else {
greeting.greetName("Welt");
}

updateViewModel();
}

/**
* Updates the ViewModel when Model was changed.
*/
protected void updateViewModel() {
greetingProperty.setValue(greeting.getGreeting());
}

/**
* Close application.
*/
public void closeApplication() {
System.exit(0);
}
}

Die wichtigen Änderungen sind nun:

  • die Klasse implementiert ViewModel
  • closeApplication als Business Logik ist dazu gekommen.
  • Die Greeting Entity wird verwendet. Wir haben jedoch unseren eigenen Code geschrieben, um Änderungen an den Properties auszulesen / zu setzen.

Unsere Applikation muss auch geändert werden:

package helloworld;

import de.saxsys.mvvmfx.FluentViewLoader;
import de.saxsys.mvvmfx.ViewTuple;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

public class HelloWorld extends Application {

public static void main(final String[] args) {
launch(args);
}

@Override
public void start(final Stage primaryStage) throws IOException {
primaryStage.setTitle("Hello World Application");
ViewTuple<HelloWorldView, HelloWorldViewModel> viewTuple = FluentViewLoader.fxmlView(HelloWorldView.class).load();
Parent root = viewTuple.getView();
primaryStage.setScene(new Scene(root));
primaryStage.show();
}

}

Der Aufruf einer MVVM Applikation sieht nun leicht verändert aus. So müssen wir ein ViewTupel erzeugen und nutzen dazu einen neuen, speziellen FluentViewLoader. 

Damit wäre die Applikation erst einmal umgestellt auf das MVVM Pattern mit Hilfe der mvvmFX Library. 

Links

Edit

  • 2020-07-03 Wechsel zu einem Repository für die ganze Serie.

 

JavaFX: Übersicht / Planung

JavaFX Serie

Ich beschäftige mich in einer Blog Serie mit JavaFX und möchte einen kurzen Überblick geben über den Aufbau der Serie sowie die Intention der einzelnen Teile:

Installation / erste Applikation

Ursprünglich war dies „JavaFX: Übersicht“. Hier zeige ich den absoluten Einstieg angefangen von der Installation und Nutzung mit Hilfe von Gradle.

Dieser schnelle Einstieg ist mir sehr wichtig, die Erfahrung im Java-Forum zeigt immer wieder, dass dies recht komplex werden kann, wenn man versucht, alles direkt zu installieren (JDK + openJFX installieren und dann Modul-Pfade und co korrekt angeben…. Dies ist mit Gradle und auch Maven nicht notwendig!).

Dieser Beitrag soll also in erster Linie ein einfacher Ansatz sein, um schnell und unproblematisch seine erste JavaFX Anwendung zum laufen zu kriegen.

Scene Builder

Der Scene Builder ist ein GUI Editor, mit dem man auf einfache Art und Weise eine Oberfläche zusammen klicken kann. Diese ist ein separates Tool und ist damit das Mittel meiner Wahl, da es die Aufwände extrem klein hält und eigentlich (fast) keine Stolpersteine bereit hält.

Erläuterung der Applikation

Ich habe in den ersten Teilen einfach Code präsentiert, ohne dies näher zu erläutern (um die Beiträge fokussiert auf das eigentliche Thema zu halten). Das hole ich hier nach.

Model / View / Controller

JavaFX verfolgt ein MVC Konzept. Dies findet sich leider in den meisten Applikationen nicht wieder. Daher gebe ich hier meine Sicht darauf wieder und zeige auf, die so eine Applikation bei mir aussehen würde. (Die Lösung funktioniert super, aber ist in meinen Augen nicht „sauber“)

MVVM

Da das MVC Patten nicht wirklich sauber abgebildet werden kann (Scene Builder kommt damit nicht klar, JavaScript Code im fxml File), gehe ich auf eine Lösung ein, die diese Problematik aus meiner Sicht gut löst.

Komplette MVVM Applikation

Beschreibung eine kompletten MVVM Applikation um einige Punkte zu demonstrieren. Die Applikation wird eine kleine Adressverwaltung sein, so dass man das Öffnen und Schließen eines Fensters sowie das saubere Binden an Properties sehen kann.

Layouts

JavaFX bietet viele Möglichkeiten, Layouts zu gestalten. Diese möchte ich etwas erläutern um zu verdeutlichen, dass man kein Scene Builder braucht, um Oberflächen zu gestallten. Dazu stelle ich die einzelnen Container vor (HBox, VBox und diverse Panes)

Formulare einfach erstellen

Es gibt zu JavaFX interessante Erweiterungen. Eine Erweiterung dient der einfachen Erstellung von Formularen und diese Erweiterung möchte ich vorstellen.

Multi Platform

Es gibt die Möglichkeit, JavaFX Applikationen auf vielen Plattformen auszuführen. Diese Möglichkeiten werden in diesem Blog Beitrag erläutert und es wird eine Übersicht über die Möglichkeiten gegeben.

Es fehlt Dir ein Punkt, der Dich bezüglich JavaFX interessiert? Schreib mir einen Kommentar hier im Blog oder schreib mir im Java-Forum: Dort bin ich als JustNobody unterwegs. Natürlich kannst Du mich auch per Email erreichen unter: konrad@kneitzel.de

Links

Edit

  • 2020-07-03 Wechsel zu einem Repository für die ganze Serie.
  • 2020-06-29 MVVM veröffentlicht und komplette MVVM Applikation als weiteren Punkt eingeschoben.

JavaFX: Model / View / Controller

JavaFX Serie

Wir haben bisher den Controller und die View gesehen, konnten Aktionen durchführen, aber wir haben bisher noch nicht mit Daten gearbeitet.

HelloWorldModel

Die Daten liegen in einem Model vor. Dazu erstellen wir uns nun erst einmal eine eigene Klasse: HelloWorldModel

package helloworld;

/**
* Our Model for Hello World.
*/
public class HelloWorldModel {

}

Nun können wir ein erstes Element hinzu fügen: Unser Greeting. Ein Binding setzt voraus, dass man informiert werden kann, wenn sich Werte ändern. Dazu existieren in JavaFX diverse Property Klassen.

Für String Werte gibt es StringProperty und SimpleStringProperty, welche wir nutzen können.

So erstellen wir uns eine Variable greeting:

private StringProperty greeting = new SimpleStringProperty();

Wenn der Controller Werte lesen können soll, dann wird ein Getter benötigt:

public String getGreeting() { return greeting.get(); }

Für Schreibzugriffe wird der Setter benötigt:

public void setGreeting(final String greeting) { this.greeting.setValue(greeting); }

Und falls die Oberfläche Änderungen mitbekommen soll, so muss das Property selbst bekannt gemacht werden:

public StringProperty greetingProperty() { return greeting; }

Nach dem gleichen Schema können wir nun auch noch den Namen hinzu fügen sowie eine Methode, die das Greeting ändert, wenn der Name geändert wurde:

package helloworld;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

/**
* Our Model for Hello World.
*/
public class HelloWorldModel {

private StringProperty name = new SimpleStringProperty();

public void setName(final String name) {
this.name.setValue(name);
}

public String getName() { return name.get(); }

public StringProperty nameProperty() { return name; }

private StringProperty greeting = new SimpleStringProperty();

public void setGreeting(final String greeting) {
this.greeting.setValue(greeting);
}

public String getGreeting() { return greeting.get(); }

public StringProperty greetingProperty() { return greeting; }

public HelloWorldModel() {
setGreeting("Hallo Welt!");
}

public void updateGreeting() {
if (name.get().length() > 0) {
greeting.setValue("Hallo " + name.get() + "!");
name.setValue("Bitte Namen eingeben");
} else {
greeting.setValue("Hallo Welt!");
}
}
}

Damit hätten wir ein fertiges Model, welches wir nutzen können.

HelloWorldController

Den Controller können wir nun noch entsprechend aufbauen:

  • Wir benötigen eine Instanz vom Model sowie ein Getter, damit das Model gelesen werden kann.
  • Wir benötigen die Behandlung von 2 Actions: Einmal zum Beenden der Applikation und zum Anderen um das Greeting anpassen zu können.
package helloworld;

import javafx.event.ActionEvent;

public class HelloWorldController {

/**
* Model with our data.
*/
private HelloWorldModel model = new HelloWorldModel();

/**
* Gets our model.
* @return Our model.
*/
public HelloWorldModel getModel() { return model; }

/**
* Close application.
* @param e ActionEvent (unused).
*/
public void closeApplicationAction(final ActionEvent e) {
System.exit(0);
}

/**
* Updates Greeting in model.
* @param e ActionEvent (unused).
*/
public void updateGreeting(final ActionEvent e) { model.updateGreeting();}
}

HelloWorld.fxml

Nun fehlt zuletzt nur noch die View.

In der View benötigen wir ein Label für unser Greeting, ein Eingabefeld für den Namen und zwei Buttons: Einmal zum Aktualisieren und zum Anderen um die Applikation zu beenden.

Bei mir sieht dies auf die Schnelle so aus:

Achtung: Bezüglich Bindings unterstützt der SceneBuilder leider nicht, was wir hier benötigen. Daher ist es nach der rein optischen Erstellung dieser Maske leider Zeit, uns vom SceneBuilder wieder zu verabschieden (Für diesen Blog-Beitrag – wir benötigen diesen ab dem nächsten Beitrag wieder). Wir werden später auch weitere Möglichkeiten lernen, wie wir Fenster schnell und sauber ohne GUI Editor erstellen können, die dann auch in der Größe veränderbar sind und gut aussehen. Oder wie wir schnell und einfach Formulare erstellen können.

Somit schließen wir den SceneBuilder und editieren nur noch das Textfile direkt z.B. in IntelliJ.

Als erstes binden wir das Label an unsere greeting Property in unserem Model:

<Label ... text="${controller.model.greeting}" ... >

Über die ${} können wir ein Binding erstellen. Wichtig ist, dass der Controller gesetzt ist so dass wir auf das model zugreifen können (Getter muss da sein!) und dann auf die Property greeting zugreifen können (Getter und greetingProperty müssen da sein!).

Die Buttons binden wir an unsere Methoden im Controller:

<Button ... onAction="#closeApplicationAction" text="Beenden" />
<Button ... onAction="#updateGreeting" text="Aktualisiere" />

Nun fehlt nur noch das Binding des TextFields. Hier laufen wir in ein kleines Problem, wenn wir benötigen ein Bidirectionales Binding, denn wir wollen den Text ja verändern können. Das ist leider etwas, das derzeit nicht im fxml möglich ist. In Zukunft ist dies vorgesehen über die # statt dem $.

Nun haben wir Code, den wir nicht in den Controller packen wollen, also müssen wir etwas in die Trickkiste greifen:

Wir können Scripts mit in die fxml Datei packen. Dazu müssen wir als erstes im Kopf der Datei die Sprache angeben, die wir verwenden wollen:

<?language javascript?>

Nun können wir das TextField definieren ohne jedes Binding aber mit einer fx:id:

<TextField fx:id="nameTextField" ... text="Bitte Namen eingeben." />

Nun benötigen wir Code, der ein bidirectional Binding aufbaut. Dazu müssen wir in der Klasse Bindings die Methode bindBidirectional aufrufen und dabei die Property und das Control angeben:

<fx:script>
javafx.beans.binding.Bindings.bindBidirectional(
controller.model.nameProperty(),
nameTextField.textProperty()
);
</fx:script>

Mit diesem Trick haben wir nun auch das Binding in beide Richtungen erstellt und es spricht nichts dagegen, die Applikation einmal zu starten und dann einen Namen anzugeben und zu staunen, wie das Greeting sich ändert, wenn man den Knopf drückt und erneut der Text erscheint: „Bitte Name eingeben“.

Der Code des ganzen Projekts findet sich unter https://github.com/kneitzel/blog-javafx-series im Verzeichnis „04 helloworld-mvc“

Links

Edit

  • 2020-07-03: Wechsel zu einem Repository für die ganze Serie.
  • 2020-06-29: Den Part bezüglich SceneEditor etwas überarbeitet – wir werden diesen in späteren Blog Beiträgen wieder nutzen (können). 

JavaFX: Erläuterungen der Applikation

JavaFX Serie

Die Applikation der letzten Blog-Einträge möchte ich jetzt hier einmal im Detail erläutern, da es mir in den ersten Teilen erst einmal um die Erstellung der Oberfläche in fxml gegangen ist.

Das ganze Projekt findet sich auf GitHub unter:
https://github.com/kneitzel/blog-javafx-serieshttps://github.com/kneitzel/blog-java-helloworld-fxml im Verzeichnis „02 helloworld-fxml“.

Die Klasse HelloWorld

Die Klasse HelloWorld enthält unsere JavaFX Applikation.

package helloworld;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

public class HelloWorld extends Application {

public static void main(String[] args) {
launch(args);
}

@Override
public void start(Stage primaryStage) throws IOException {
Parent root = FXMLLoader.load(getClass().getResource("HelloWorld.fxml"));
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}

}

Unsere Klasse erbt von Application, welches die Funktionalität einer JavaFX Applikation bereit stellt und u.a. die Parameter, die mitgegeben werden, für uns auswertet.

public static void main(String[] args) {
launch(args);
}

In der Main Methode wird lediglich die in Application bereits vorhandene Methode launch aufgerufen, welche dann u.a. die Parameter auswertet und eine Instanz unserer Klasse erzeugt.

@Override
public void start(Stage primaryStage) throws IOException {

In unserer Klasse überschreiben wir die start Methode, welche uns als Parameter eine Stage mitgibt. Dies ist der oberste Container unserer JavaFX Struktur und entspricht dem Fenster.

Parent root = FXMLLoader.load(getClass().getResource("HelloWorld.fxml"));

In start lesen wir als erstes unser fxml File ein. Die Datei laden wir über die Methode getResource der Klasse, welche den ClassLoader nutzt um die Resource innerhalb des ClassPath zu finden. Dabei wird relativ zum Verzeichnis des Package gesucht. Da das Package unserer Klasse „helloworld“ ist, wird die Resource in einem Verzeichnis helloworld gesucht.

Der FXMLLoader lädt nicht nur die Datei sondern erzeugt auch eine Instanz des Controllers und initialisiert auch alle durch das @FXML Attribut versehende Elemente.

Scene scene = new Scene(root);

Über den geladen Inhalt erzeugen wir eine Scene. Dies entspricht sozusagen dem Inhalt eines Fensters.

primaryStage.setScene(scene);

Wir setzen die erzeugte Scene zum Inhalt des Hauptfensters.

primaryStage.show();

Und zuletzt machen wir das Fenster sichtbar.

Die Klasse HelloWorldController

Der Controller ist für alle Aktionen in der View verantwortlich und wird von der View angesprochen. In dem Beispiel habe ich auch die Möglichkeit gezeigt, wie der Controller auf die View zugreifen kann, aber diese Abhängigkeit sollten wir nicht nutzen!
Wichtig: Die Instanzen dieser Klasse werden vom FXMLLoader erzeugt. Wir erzeugen keine eigenen Instanzen!

package helloworld;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;

public class HelloWorldController {

@FXML private Button greetMeButton;

public void closeApplicationAction(ActionEvent e) {
System.exit(0);
}

public void handleGreetMeButton(ActionEvent e) {
Alert alert = new Alert(Alert.AlertType.INFORMATION, "Hallo Anwender!", ButtonType.OK);
alert.showAndWait();
}

@FXML
public void initialize() {
greetMeButton.setOnAction(this::handleGreetMeButton);
}
}

Die Klasse unseres Controllers muss von keiner anderen Klasse abgeleitet werden.

@FXML private Button greetMeButton;

In dem Controller können wir uns die Elemente, welche im fxml definiert wurden, vom FXMLLoader geben lassen. Damit der FXMLLoader dies macht, muss die Annotation @FXML gesetzt sein.
Desweiteren muss der Typ stimmen (hier Button) und der Name der Variablen muss mit der fx:id überein stimmen (hier fx:id=“greetMeButton“).

Wichtig: Dies erzeugt eine Abhängigkeit vom Controller zu der View. Diese Abhängigkeit sollte vermieden werden so dass nur eine Abhängigkeit von der View zum Controller (und später auch zum Model) existiert. Dies findet sich nur in diesem Beispiel, da dies etwas ist, das sehr viele Entwickler so machen.

public void closeApplicationAction(ActionEvent e) {
public void handleGreetMeButton(ActionEvent e) {

Wir definieren Methoden, die ein ActionEvent als Parameter nehmen. Diese Methoden können Actions behandeln und werden in der fxml Datei referenziert.

Natürlich können wir diese Methoden auch aus unserem Code heraus nutzen.

@FXML
public void initialize() {

Es gibt für einen Controller gewisse Events, die wir nutzen können. Ein Event ist das initialize Event. Das @FXML Annotation sorgt dafür, dass der FXMLLoader diese Methode auch ansteuert.

Dies ist wichtig, denn die Variablen, die vom FXMLLoader gesetzt werden (mit @FXML Annotation) werden erst nach dem Konstruktor gesetzt. Somit stehen diese noch nicht im Konstruktor zur Verfügung. Initialisierungsarbeiten, die auch die Controls benötigen, müssen somit innerhalb der initialize() Methode gemacht werden.

greetMeButton.setOnAction(this::handleGreetMeButton);

Hier setzen wir im Code, was für eine Action bei dem Button ausgeführt werden soll. Dies hätte auch über das fxml gesetzt werden können (so wie beim anderen Button).

Die HelloWorld.fxml Ressource

In der fxml Datei haben wir unsere Oberfläche beschrieben.

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.text.Font?>

<Pane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="145.0" prefWidth="237.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="helloworld.HelloWorldController">
<children>
<Label alignment="CENTER" contentDisplay="CENTER" layoutX="-1.0" layoutY="14.0" prefHeight="53.0" prefWidth="237.0" text="Hallo Welt" textOverrun="CLIP" underline="true">
<font>
<Font size="24.0" />
</font>
</Label>
<Button fx:id="greetMeButton" layoutX="30.0" layoutY="78.0" mnemonicParsing="false" text="Grüße Mich" />
<Button layoutX="137.0" layoutY="78.0" mnemonicParsing="false" onAction="#closeApplicationAction" text="Beenden" />
</children>
</Pane>

Wichtige Elemente sind dabei:

<Pane ... fx:controller="helloworld.HelloWorldController">

Wir definieren in der Pane, welche Klasse als Controller dienen soll. Eine Instanz dieser Klasse wird vom FXMLLoader erzeugt.

<Button fx:id="greetMeButton" ... />

Wir geben dem Button eine feste Id, um dann über den FXMLLoader eine Referenz zu dem Control bekommen zu können.

Wichtig: Wie schon mehrfach gesagt: Dieses Vorgehen ist suboptimal und von mir nicht empfohlen.

<Button ... onAction="#closeApplicationAction" ... />

Wir definieren in der fxml Datei, welche Methode im Controller bei der Action ausgeführt werden soll.

Links

Edit

  • 2020-07-03 Wechsel zu einem Repository für die ganze Serie.

JavaFX: Scene Builder

JavaFX Serie

Als einen wichtigen Unterschied zu Swing habe ich angegeben, dass die Oberflächen deklarativ erstellt werden können. In dem HelloWorld fand sich aber dann davon aber nichts und das einfache Fenster habe ich im Programmcode erstellt.

Dies ändert sich nun und dazu besorgen wir uns noch ein zusätzliches Tool: Den Scene Builder von Gluon. Nach der Installation können wir den Scene Builder auch in IntelliJ integrieren, indem wir in den Settings unter Language & FRameworks -> JavaFX den Pfad zum Scene Builder eintragen.

Eine erste Oberfläche erstellen

Wir erstellen unsere erste Oberfläche, in dem wir einfach in Screen Builder ein neues, leeres Dokument erstellen.

Als erstes ziehen wir aus den Containers ein Pane in unser Dokument. Dieses Element wird alle weiteren Elemente beherbergen.

Als nächstes Ziehen wir einen Label aus den Controls auf unser Pane. Damit können wir jetzt etwas spielen:

  • Wir klicken abwechselnd auf Label oder das Pane: Das angeklickte Element wird markiert und es werden die Properties angezeigt.
  • Wir verschieben die Elemente, ändern am Rand die Größe….
  • Wir ändern Properties. Beim Label können wir den Text ändern und wie der Text dargestellt wird.

Das Label platzieren wir oben im Pane, und verbreitern es, dass es über die ganze Breite geht. Der Text soll „Hallo Welt“ sein, mit 24px Größe und unterstrichen. Und der „Node“ des Label soll ein Alignment CENTER haben.

Unterhalb des Label können wir nun noch zwei Buttons hin ziehen. Diese bekommen den Text „Grüße Mich“ und „Beenden“.

Wir können die alles nun mit der Maus etwas verschieben und die Größe ändern, bis es uns gefällt.

Mein Ergebnis:

Unsere erstellte Oberfläche speichern wir nun als HelloWorld.fxml im Verzeichnis src/main/resources/helloworld/ unseres Testprojekts. Das helloworld Verzeichnis bitte so anlegen.

Hinweis: Hier wird die Trennung deutlich: Unser Java-Code liegt unter src/main/java, aber alle Resourcen wie Bilder oder so eine Scene von JavaFX landen in src/main/resources.

Inhalt der Datei

Die erstellte Datei können wir uns mit einem Texteditor ansehen. Es handelt sich um eine XML Datei und wir können die Struktur, die wir aufgebaut haben sowie die Einstellungen wieder finden.

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.text.Font?>


<Pane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="145.0" prefWidth="237.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1">
<children>
<Label alignment="CENTER" contentDisplay="CENTER" layoutX="-1.0" layoutY="14.0" prefHeight="53.0" prefWidth="237.0" text="Hallo Welt" textOverrun="CLIP" underline="true">
<font>
<Font size="24.0" />
</font>
</Label>
<Button layoutX="30.0" layoutY="78.0" mnemonicParsing="false" text="Grüße Mich" />
<Button layoutX="137.0" layoutY="78.0" mnemonicParsing="false" text="Beenden" />
</children>
</Pane>

Damit aber diese Scene von unserem Programm verwendet werden kann, müssen wir natürlich noch unser Programm anpassen:

package helloworld;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

public class HelloWorld extends Application {

public static void main(String[] args) {
launch(args);
}

@Override
public void start(Stage primaryStage) throws IOException {
Parent root = FXMLLoader.load(getClass().getResource("HelloWorld.fxml"));
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();

}
}

Damit haben wir ein erstes kleines Programm. Die Befehle werde ich später im Detail erläutern. Jetzt nur als groben Überblick: Es wird die fxml Datei geladen und dann daraus ein Applikationsfenster angepasst. Wenn wir es starten, sollte ein Fenster mit dem erstellten Inhalt angezeigt werden, aber noch passiert da nichts, wenn wir einen Button drücken.

Aktionen in SceneBuilder definieren

Eine Möglichkeit ist, dass wir Aktionen in der fxml Datei hinterlegen.

Dazu erstellen wir im ersten Schritt eine neue Klasse HelloWorldController mit einer Methode, die das Programm beenden soll, wenn eine „Action“ statt findet:

package helloworld;

import javafx.event.ActionEvent;

public class HelloWorldController {

public void closeApplicationAction(ActionEvent e) {
System.exit(0);
}
}

IntelliJ zeigt den Klassen- und Methodennamen grau an, um uns zu zeigen: Dies wird bisher nicht benutzt.

Als nächstes müssen wir im SceneBuilder die Aktion hinterlegen. Dazu sind zwei Schritte notwendig:

a) Wir hinterlegen in der Scene, welcher Controller zuständig ist. Dazu müssen wir im linken Bereich unten auf Controller klicken um dann die Controller class auszuwählen. Dort geben wir helloworld.HelloWorldController ein.

b) Im nächsten Schritt wählen wir den Knopf und klicken im rechten Bereich auf Code : Button. Dort finden wir unter Main ein Feld On Action. Dort geben wir closeApplicationAction ein.

Wir können die Scene nun speichern und zum IntelliJ zurück wechseln. IntelliJ sollte diese Änderung mitbekommen haben und nun die Klasse und Methode nicht mehr grau darstellen.

Nun einfach einmal die Applikation starten und prüfen, ob wir über unseren Beenden Knopf die Applikation beenden können.

Wir haben hier etwas aufgebaut, was eine sehr schöne Trennung aufzeigt:

  • Under Controller bietet eine Action an. Ob und wie diese aufgerufen wird, ist erst einmal egal. Das kein Button sein aber auch irgend ein anderes Control, dass ein ActionEvent auslösen kann.
  • Die UI lässt sich jederzeit anpassen. Egal, was da benutzt wird: Die Methoden des Controllers stehen zur Verfügung

Wir haben hier also eine sehr gute Trennung zwischen Controller und View.

Eine so strickte Trennung sollte wenn möglich immer eingehalten werden. Bei dem „Güße Mich“ Button möchte ich aber auch einmal kurz aufzeigen, was man oft bei anderen Entwicklern an Code sieht.

In dem SceneBuilder gehen wir auf den „Grüße Mich“ Knopf und tragen unter Code : Button eine fx:id ein: greetMeButton. Damit vergeben wir eine klare ID an diesen Button, so dass wir einfach auf diesen zugreifen können.

In unserem Controller fügen wir nun ein:

a) Die Methode, die uns grüßen soll:

public void handleGreetMeButton(ActionEvent e) {
Alert alert = new Alert(Alert.AlertType.INFORMATION, "Hallo Anwender!", ButtonType.OK);
alert.showAndWait();
}

b) Eine Instanzvariable hinzu mit Annotation @FXML:

@FXML private Button greetMeButton;

c) Eine Initialisierungs-Methode

@FXML
public void initialize() {
greetMeButton.setOnAction(this::handleGreetMeButton);
}

Wenn wir dies ausführen, dann sehen wir, dass alles funktioniert. Aber jetzt haben wir eine klare Abhängigkeit von dem Controller hin zur View. Es muss ein Button mit fx:id greetMeButton geben. Wenn jemand anderes es Oberfläche baut oder anpasst, dann hat er hier eine klare Einschränkung.

Und im Controller sind wir nicht mehr auf Funktionen konzentriert sondern auf konkrete Bedienelemente. Der Entwickler sollte sich aber in erster Linie um Funktionen auf Basis des Modells (Dazu kommen wir später in der Serie) kümmern.

Evtl. ein kleiner Vergleich: Ein Motor im Auto hat eine Steuerung „Gas“. Da kann man einen Wert setzen. Woher dieser Wert kommt (Gaspedal, Gashebel, Computer, Joystick, ….) ist egal. 

Daher dieser Abschnitt nur als kurzer Hinweis. Wir lassen dies jetzt auch noch so, denn im nächsten Beitrag werde ich die Applikation im Detail erläutern.

Die Sourcen dieses Projektes finden sich unter https://github.com/kneitzel/blog-javafx-series in „02 helloworld-fxml“

Links

Edit

  • 2020-07-03 Wechsel zu einem Repository für die ganze Serie.

JavaFX: Installation / erste Applikation

JavaFX Serie

Übersicht über JavaFX

JavaFX ist ein relativ modernes Framework zur Erstellung von graphischen Oberflächen. Bis einschließlich Java 8 war es Bestandteil vom Java Developer Kit (JDK). Mit Version 9 vom JDK wurde es jedoch ausgelagert und separat als openjfx weiter entwickelt.

Unterschied zu Swing

Ein wichtiger Unterschied zu Swing ist, dass eine Oberfläche in einem separaten File beschrieben werden kann statt diese in Java selbst zusammen zu stellen (deklarative Oberfläche im Gegensatz zu der programmierten Oberfläche).

Desweiteren bietet JavaFX einige technische Aspekte wie z.B. das Binding von Controls an Modelle.

Installation

Das es nicht mehr fester Bestandteil von Java ist, muss man hier manuell eingreifen. Dazu gibt es drei Möglichkeiten:

  1. Eigenständige Installation – dabei muss aber alles korrekt konfiguriert werden, so man kein OpenJDK mit integriertem JavaFX bekommt.
  2. Einbinden in das (Maven oder Gradle) Projekt als Abhängigkeit. Dann ist keine eigenständige Installation notwendig.

Um es hier möglichst einfach nachvollziehbar zu halten, gehe ich hier nur auf die Lösung über Gradle ein. Zum Nachvollziehen empfehle ich die Installation eines JDK in der Version 11 (LTS) oder 14 (aktuelle Version) – z.B. AdoptOpenJDK.
Desweiteren beschreibe ich die Schritte innerhalb von JetBrains IntelliJ Community Edition. Es sollte aber unproblematisch sein, die Vorgehensweise auch auf anderen Entwicklungsumgebungen wie Eclipse oder Netbeans nach zu vollziehen. Diese Entwicklungsumgebungen sind alle für die üblichen Betriebssysteme (Windows, Mac, Linux) verfügbar.

Ich werde im Folgenden aber nur den IntelliJ Weg näher beschreiben. Dazu setze ich derzeit die Version 2020.1 ein.

Ein erstes Gradle Projekt

Um ein neues Projekt zu erstellen, wählen wir „Create New Projekt“ im Start-Dialog von IntelliJ oder im Menü File -> New -> Project.. Nun zeigt uns IntelliJ ein Fenster, bei dem wir Links die Projekt-Technology wählen können und Links Optionen zu dem Projekt auswählen können:

Hier wählen wir Links Gradle aus und stellen sicher, dass Java auf der rechten Seite ausgewählt ist.

Nach „Next“ dürfen wir

  • Einen Namen vergeben. Hier wählen wir JavaFX Test.
  • Das Verzeichnis wird automatisch angepasst. Wenn uns der Ort für das Projekt nicht zusagt, dann können wir den Pfad anpassen.

Hinter Artifact Coordinates versteckt können wir nun noch angeben, unter welchem Namen unser Projekt bekannt sein soll. Diese Informationen umfassen:

  • GroupID, dies ist in der Regel eine Domain des Unternehmens nur umgedrehte Teile, also z.B. de.kneitzel 
  • Artifact ID, die ist sozusagen der interne Name
  • Version, die wir derzeit entwickeln. Während der eigentlichen Entwicklung ist in der Regel ein -SNAPSHOT angehängt.

Nach einem Click auf „Finish“ können wir uns die erzeugte Verzeichnisstruktur ansehen:

Gradle Projektdateien

  • build.gradle ist die Projektbeschreibung
  • settings.gradle enthält Gradle Einstellungen
  • .gradle ist ein Verzeichnis, das Dateien von Gradle zwischen speichern kann.

Gradle Wrapper

Der Gradle Wrapper ist ein kleines Tool, das es uns erlaubt, automatisch die richtige Gradle-Version innerhalb des Projekts herunter zu laden und aufzurufen.

  • gradlew und gradlew.bat sind Startscripte, mit denen wir Gradle starten können.
  • gradle/wrapper – dieses Verzeichnis enthält die Konfiguration für den Gradle Wrapper.

Unser Code

Zu guter letzt haben wir Verzeichnisse für unsere Arbeit:

  • src/main/java und src/test/java enthält unseren Java Code.
  • src/main/resources und src/test/resources enthalten Resourcendateien

JavaFX Abhängigkeit

Damit unser Gradle-Projekt ein JavaFX Projekt wird, müssen wir einige Anpassungen in der build.gradle Datei vornehmen:

plugins

In den Plugins ist bisher nur da Plugin Java aufgeführt. Dies ändern wir auf:

plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.8'
}

Als nächstes müssen wir angeben, welche JavaFX Module wir verwenden wollen. Dazu fügen wir unterhalb der Plugins ein:

javafx {
version = "11.0.2"
modules = [ 'javafx.controls', 'javafx.fxml' ]
}

Wichtig: Falls statt Java 11 Java 14 eingesetzt wird, bitte die Version von „11.0.2“ auf „14“ ändern.

Bei den Dependencies tragen wir noch zusätzlich die Abhängigkeit zu den javafx.controls und javafx-fxml ein, so dass die Dependencies wie folgt aussehen:

dependencies {
compile group: 'org.openjfx', name: 'javafx-controls', version: '11.0.2'
compile group: 'org.openjfx', name: 'javafx-fxml', version: '11.0.2'
testCompile group: 'junit', name: 'junit', version: '4.12'
}

Erneut gilt: Bei Java 14 bitte die 11.0.2 durch 14 ersetzen.

Zuletzt fehlt noch ein Eintrag, der angibt, welches die Hauptklasse unserer Anwendung werden soll:

mainClassName = 'helloworld.HelloWorld'

Diesen Eintrag können wir unterhalb der „group“ und „version“ Zeile einfügen.

Nach dem Speichern dieser Projekt-Änderungen muss IntelliJ die Änderungen einlesen. Dazu öffnen wir über View -> Tool Windows -> Gradle das Gradle Fenster in IntelliJ und drücken in der Symbolleiste gleich auf das erste Symbol mit den zwei Pfeilen im Kreis: Reload All Gradle Projects

IntelliJ / Gradle laden nun die fehlenden Dateien aus dem Internet nach. Dies sind jetzt vor allem die angegebenen Plugins.

Unser erstes Hello World Programm

Nun müssen wir natürlich noch unser Programm schreiben. Den Namen der Klasse haben wir schon vorgegeben: helloworld.HelloWorld, d.h. wir schreiben eine neue Klasse HelloWorld in dem Package helloworld.

Dazu klicken wir mit der rechten Maustaste auf das blaue java Verzeichnis src/main/java und wählen New -> Java Class.

Als Namen für die Klasse geben wir helloworld.Helloworld ein so dass IntelliJ uns einen Rahmen für unsere Klasse erstellt. Den Inhalt der Datei ersetzen wir mit folgendem ersten JavaFX Code von uns:

package helloworld;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class HelloWorld extends Application {
public static void main(String[] args) {
launch(args);
}

@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Hello World!");
StackPane root = new StackPane();
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
}
}

Starten unseres Programmes

Zum Starten gehen wir erneut in das gradle Toolfenster (Evtl. ist es noch am Rand sichtbar, so dass wir nicht erst über das View Menü gehen müssen) und wählen dort im Fenster in der Baumstruktur:

Tasks -> Application -> run

Und machen ein Doppelclick auf run.

IntelliJ sollte nun unseren Code übersetzen und nach kurzer Zeit unser erstes Programm starten:

Herzlichen Glückwunsch! Auch wenn das erste Programm nach nichts aussieht haben wir einen ersten großen Schritt gemeistert: Wir konnten ein JavaFX Programm schreiben, übersetzen und ausführen. Und das mit ein paar einzelnen, einfachen Schritten.

Sollte es bei der Erstellung der Dateien Probleme gegeben haben, so findet man eine Version unter https://github.com/kneitzel/blog-javafx-series im Verzeichnis „01 helloworld“

Links

Edit

  • 2020-07-03 Wechsel zu einem Repository für die ganze Serie.