Visualisierung in Java mit JavaFX

Grundlagen

Wie erwähnt ist JavaFX unabhängig von Java Swing/AWT und arbeitet mit einem sogenannten Scene Graph. Bei diesem, kommt das sogenannte RetainedMode Rendering zum Einsatz. Hierbei kümmert sich ein Framework beziehungsweise eine Library um das Rendering, wohingegen beim sogenannten ImmediateMode Rendering, dass Java Swing/AWT einsetzt, der Entwickler in der Hand hat, welcher Inhalt wo und wann zu rendern ist.

Der JavaFX Scene Graph (Abb. 2)


Der JavaFX Scene Graph ist ein vorwärtsgerichteter Graph, der sogenannte Nodes umfasst. Dabei unterscheidet man drei verschiedene Typen:

  • Root Node
  • Branch Node
  • Leaf Node

Der Root Node ist der erste Node im Scene Graph und auch der einzige, der keinen Elternknoten hat. Der Branch Node hat die Möglichkeit, andere Nodes aufzunehmen und somit als Elternknoten (Parent) zu operieren. Aus diesem Grund werden zumeist JavaFX-Layoutcontainer (Pane, StackPane, etc.) als Branch Nodes verwendet. Der Leaf Node kann hingegen keine weiteren Nodes aufnehmen und ist meist etwas wie ein grafisches Primitiv (Rechtecke, Kreise, Linien, etc.), ein Steuerelement, ein Bild oder ein Media-Objekt.

Auch der Aufbau einer JavaFX-Applikation spiegelt den Szenengraphen wieder. Als Vergleich und zum besseren Verständnis lässt sich eine Theaterbühne heranziehen. Hier gibt es eine Bühne, auf der sich das Theaterstück abspielt. In JavaFX spricht man deshalb auch von einer Stage (deutsch: Bühne), auf der man seine Applikation anzeigt. In einem Theaterstück gibt es meistens verschiedene Szenen, die auf der Bühne gespielt werden. Im JavaFX Scene Graph gibt es dementsprechend sogenannte Scenes, die sich auf die Stage laden lassen. Man kann sich also zum Beispiel vorstellen, dass man die verschiedenen Views in seiner Software über mehrere Scenes erstellt und sie zum Zeitpunkt des Verwendens auf die Stage lädt. Die jeweilige Scene enthält die entsprechenden Layoutcontainer (Branch Nodes) mit ihrem Inhalt (weitere Branch und Leaf Nodes).

Wichtig ist noch zu wissen, dass eine Stage ein Fenster darstellt und man in einer JavaFX-Applikation zumindest mit einer Stage anfängt. Von dieser initialen Stage kann man weitere Stages (Fenster) öffnen, sollte man sie benötigen. Es folgt das schon obligatorische "Hello World"-Beispiel in JavaFX:

public class HelloWorld extends Application {
private Label label;

@Override public void init() {
label = new Label("Hello World");
}

@Override public void start(Stage stage) {
StackPane root = new StackPane();
root.getChildren().add(label);

Scene scene = new Scene(root, 200, 200);

stage.setTitle("Hello World Example");
stage.setScene(scene);
stage.show();
}

@Override public void stop() {}

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

Das Ergebnis des Programms zeigt Abbildung 3.

Ausgabe des "Hello World"-Programms (Abb. 3)

Die Programmstruktur des Beispiels spiegelt den typischen Aufbau einer JavaFX-Applikation wieder. Dabei wird zunächst die Klasse Application erweitert, was erfordert, dass zumindest eine Methode start() vorhanden ist. Des Weiteren gibt es noch drei optionale Methoden init(), stop() und main().

Die main()-Methode ist nötig, um die JavaFX-Applikation startbar zu machen und Applikationsparameter mit zu übergeben. Daraufhin wird automatisch die init()-Methode aufgerufen, in der Entwickler dann zum Beispiel Member-Variablen initialisieren kann. Ist das geschehen, ruft das Programm die start()-Methode auf, der automatisch eine Stage übergeben wird. Dabei handelt es sich um das Anwendungsfenster, in dem das ausführende Gerät die Applikation anzeigt.

Durch Beenden des Programms (zum Beispiel durch den Aufruf von Platform.exit()), ruft man die Methode stop() auf. In ihr besteht die Gelegenheit, verwendete Ressourcen wie Netzverbindungen wieder freizugeben.

Weitere bedeutende Features in JavaFX sind die sogenannten Properties und Bindings. Letztere setzen das Konzept um, die Werte von Variablen an andere Variablen zu binden. Ist der Wert der Variablen A an den Wert der Variablen B gebunden, wird A automatisch den Wert von Variable B erhalten, ohne dass man A explizit setzen muss. Damit das funktioniert, benötigt man die sogenannten Properties, die sich wie folgt verwenden lassen:

// Initialization of the property
private DoubleProperty value = new SimpleDoubleProperty(0);

// The getter method
public double getValue() {
return value.get();
}

// The setter method
public void setValue(double newValue) {
value.set(newValue);
}

// The property method (enables the binding)
public DoubleProperty valueProperty() {
return value;
}

Im Codebeispiel sieht man, dass, anstelle eines primitiven Datentyps vom Typ double, ein neuer Datentyp vom Typ DoubleProperty verwendet wurde. Diese Typen "wrappen" einen primitiven Datentyp (hier double) und stellen zusätzliche Funktion zur Verfügung. Jede Property verfügt zum Beispiel standardmäßig über eine get()- und eine set()-Methode. Die Konvention sieht vor, dass man zum Verwenden von Binding eine Methode zur Verfügung stellen muss, die mit Property endet (hier valueProperty()) und das Property-Objekt zurückliefert. Der folgende Quelltextausschnitt soll das erwähnte Binding mit dieser Methode erklären.

IntegerProperty number1 = new SimpleIntegerProperty(1);
IntegerProperty number2 = new SimpleIntegerProperty(2);
DoubleProperty number3 = new SimpleDoubleProperty(0.5);

// High-Level Binding (Fluent API)
NumberBinding sum1 = number1.add(number2);
NumberBinding result1 = number1.add((number2).multiply(number3));

// High-Level Binding (Binding class)
NumberBinding sum2 = Bindings.add(number1, number2);
NumberBinding result2 = Bindings.add(number1, multiply(number2,number3));

// Low-Level Binding
DoubleBinding db = new DoubleBinding() {
{
super.bind(number1, number2, number3);
}

@Override protected double computeValue() {
return (number1.get() + number2.get() * number3.get());
}
}

Wie sich erkennen lässt, gibt es mehrere Möglichkeiten, das Binding in JavaFX anzuwenden. Man unterscheidet prinzipiell zwischen High-Level- und Low-Level-Binding, wobei sich das High-Level-Binding in die sogenannte Fluent API und das Verwenden der Bindings-Klasse aufteilt. Bindings und Properties einzusetzen erfordert auf Seiten der Entwickler etwas mehr Aufwand durch die zusätzlichen Property-Methoden. Auf der Anwenderseite ermöglichen sie es jedoch, vergleichsweise übersichtlichen Code zu schreiben, da man keine zusätzlichen Listener für Variablen-Änderungen mehr erstellen muss.