Properties und Binding in JavaFXv.7.0
Inhalt
Properties
Properties ermöglichen es, Eigenschaften einer Klasse so zu fassen, dass sie an andere Daten-Objekte (z.B. Komponenten, Nodes, andere Eigenschaften, etc.) gekoppelt werden können. So kann etwa die Anzeige der Höhe eines Kontostandes auf einem Label, Textfeld o.ä. bei jeder Änderung seines Standes automatisch aktualisiert werden.
Betrachten wir als Beispiel eine Klasse Konto
mit einer
privaten Instanzvariablen stand
. Die Anzeige der
Höhe dieses Kontostandes soll zeitgleich zu jeder Änderung
aktualisiert werden. Bislang wurde ein solcher Fall etwa durch ein Observer-Entwurfsmuster
gelöst, ein Verfahren, das je nach Implementierung durchaus
seine Grenzen haben konnte.
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class Konto {
private DoubleProperty stand;
public final double getStand() {
if (stand != null)
return stand.get();
return 0;
}
public final void setStand(double hoehe) {
this.standProperty().set(hoehe);
}
public final DoubleProperty standProperty() {
if (stand == null) {
stand = new SimpleDoubleProperty(0);
}
return stand;
}
}
Im Beispiel fällt zunächst auf, dass nicht wie erwartet,
ein primitiver double
als Datentyp für den
Kontostand gewählt wurde, sondern ein Objekt vom Typ DoubleProperty
.
Hierbei handelt es sich um ein den primitiven double
-Typ
kapselndes Objekt, das besonders die
Benachrichtigungsfunktionalität einer Property bereitstellt.
Die Klassen des Packages javafx.beans.property
implementieren hierzu z.B. alle das Interface Observable
und/oder ObservableValue
, das einen Wert kapselt.
JavaFX kennt solche Wrapper für die Datentypen int, long,
float
und double
.
Die Klasse weist drei Methoden auf, von denen zwei die üblichen
Getter- und Setter-Methoden sind, die gleichwohl insofern von der
bislang bekannten Syntax abweichen, als sie den gekapselten
primitiven Typ über die Methoden DoubleProperty.get()
und DoubleProperty.set()
ansprechen. Um eine eventuelle
NullPointerException
zu vermeiden, muss im Getter
natürlich vorher die Existenz des kapselnden Objektes abgefragt
werden.
Die dritte Methode stellt die eigentliche Besonderheit
dar, da sie die Properties-Funktionalität gewährleistet.
Sie gibt das Property
-Objekt selbst zurück. Es
muss darauf geachtet werden, dass der Bezeichner der Methode aus dem
Namen der Variablen und einem nachfolgenden Property
zusammengesetzt wird.
Auch hier wird zunächst auf die
Existenz des Objektes geprüft und dies initial als SimpleDoubleProperty
erzeugt, einer vollständigen Implementierung der abstrakten
Klasse DoubleProperty
.
Koppeln eines Wertes mit einer Komponente mittels Property und ChangeListener.
Einer der offensichtlichsten Vorteile von Properties besteht darin, dass primitive Datentypen mit einer Funktionalität versehen werden können, die sonst nur Referenztypen vorbehalten bleibt. So ist es in unserem Fall möglich, den Kontostand mit einem ChangeListener zu versehen, sodass seine Änderung etwa auf einem Label angezeigt werden kann.
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
public class BindingsBsp1 extends Application {
@Override
public void start(Stage primaryStage) {
final Konto konto = new Konto();
konto.setStand(0);
Button incButt = new Button("+1");
final Label label = new Label(new Double(konto.getStand()).toString());
label.setMinSize(incButt.getMinWidth(), incButt.getMinHeight());
label.setAlignment(Pos.CENTER);
konto.standProperty().addListener(new ChangeListener<Object>() {
@Override
public void changed(ObservableValue<?> o, Object oldVal,
Object newVal) {
label.setText(new Double(konto.getStand()).toString());
}
});
incButt.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
konto.setStand(konto.getStand() + 1);
}
});
GridPane grid = new GridPane();
grid.setAlignment(Pos.CENTER);
grid.setVgap(10);
grid.add(label, 0, 0);
grid.add(incButt, 0, 1);
Scene scene = new Scene(grid, 150, 100);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Im Beispiel wird ein einfaches, kleines JavaFX-Fenster mit einem
Button und einem Label erzeugt. Das Label zeigt den Kontostand, der
durch den Button inkrementiert werden kann. Dem Property-Objekt des
Kontostandes wird ein ChangeListener
hinzugefügt,
der die Methode setText()
des Labels anspricht und mit
den geänderten Werten versieht.
Der Button macht nichts
anderes, als den aktuellen Kontostand selbst abzufragen und zu
inkrementieren. Es wird deutlich, dass das Property-Objekt des
Kontostandes selbst für die Aktualisierung der Labeldarstellung
verantwortlich zeichnet.
Binding
Bindings umfassen einen oder mehrere Werte (dependencies), deren Zustand überwacht und bei Änderungen automatisch angepasst wird.
Binding zwischen Werten.
Erweitern wir obiges Eingangs-Beispiel und stellen uns zwei Konten vor, deren Summe bei Änderung eines Kontostandes angepasst wird.
import javafx.beans.binding.NumberBinding;
public class BindingsBsp2 {
public static void main(String[] args) {
Konto konto1 = new Konto();
konto1.setStand(100);
Konto konto2 = new Konto();
konto2.setStand(50);
NumberBinding sum = konto1.standProperty().add(konto2.standProperty());
System.out.println(sum.getValue()); // 150.0
konto2.setStand(20);
System.out.println(sum.getValue()); // 120.0
}
}
Nach der Bildung zweier Konto-Objekte werden hier zunächst
deren Stände auf Ausgangswerte gesetzt. In der folgenden Zeile
werden die Werte beider Properties durch add()
addiert
und in einem Objekt vom Typ NumberBinding
gespeichert,
auf dessen gekapselten Wert durch getValue()
zugegriffen werden kann. Die beiden Methodenaufrufe standProperty()
greifen bei der Berechnung automatisch auf die jeweils gekapselten
Werte zu.
Die Änderung des zweiten Kontostandes und die
anschließende Abfrage beweisen die Aktualisierung der Summe
durch das Binding.
Unidirectional Binding (einseitige Bindung) eines Wertes an eine oder mehrere Komponenten
In JavaFX besitzen eine ganze Reihe an Klassen bereits eingebaute
Properties-Funktionalität. So auch alle Komponenten, die von javafx.scene.Node
abgeleitet sind. Zu ihnen gehören alle Knoten-Elemente,
insbesondere auch die Controls, die Interaktionen mit dem
User entgegennehmen können.
Das Beispiel zeigt ein einfaches
Fenster, in dem der Stand eines Kontos durch zwei Buttons
erhöht, bzw. erniedrigt werden kann. Der jeweilige Stand wird
durch zwei Anzeigeelemente, eine ProgressBar
und einen
ProgressIndicator
angezeigt. Um das Beispiel so einfach
wie möglich zu halten, werden die Kontostand-Werte im
Standard-Anzeigebereich der Controls zwischen 0 und 1
gehalten und eine Initialisierung von 0.3 vorgenommen. Die Buttons
führen Änderungen von jeweils 0.01 Punkten durch.
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
public class BindingsBsp3 extends Application {
private static final double INITIAL_STAND = 0.3;
@Override
public void start(Stage primaryStage) {
final Konto konto = new Konto();
konto.setStand(INITIAL_STAND);
Button incButt = new Button("+0.01");
incButt.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
konto.setStand(konto.getStand() + .01);
}
});
Button decButt = new Button("-0.01");
decButt.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
konto.setStand(konto.getStand() - .01);
}
});
ProgressBar pb = new ProgressBar(INITIAL_STAND);
pb.progressProperty().bind(konto.standProperty());
ProgressIndicator pi = new ProgressIndicator(INITIAL_STAND);
pi.progressProperty().bind(konto.standProperty());
GridPane grid = new GridPane();
grid.add(incButt, 0, 0);
grid.add(decButt, 1, 0);
grid.add(pb, 0, 1);
grid.add(pi, 1, 1);
grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(10));
Scene scene = new Scene(grid);
primaryStage.setScene(scene);
primaryStage.setTitle("Binding-Beispiel");
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
In den anonymen EventHandler
-Klassen der Buttons wird
der Wert des Kontostandes erhöht und erniedrigt, indem der
jeweilige Stand abgefragt und neu gesetzt wird. Das Binding an die
Anzeige erfolgt so, dass durch die Methode bind()
die
Property des Kontostandes an die eingebaute progressProperty
von ProgressBar
und ProgressIndicator
gebunden wird. Da es sich bei den Fortschrittswerten der
Kontrollelemente um numerische Werte handelt, gelingt dies
natürlich wie im Falle des Kontostandes nur mit der Bindung an
einen ebenfalls numerischen Wert. Welche Elemente über welche
Properties verfügen, kann der API-Dokumentation im Abschnitt Property
Summary der jeweiligen Klasse/des jeweiligen Interfaces
entnommen werden.
Bidirectional Binding (wechselseitige Bindung)
Ein Binding kann auch dergestalt erfolgen, dass sich zwei Elemente
gegenseitig aktualisieren. Dies soll an einem Beispiel mit einem Textfield
und einem Slider
demonstriert werden.
import java.text.NumberFormat;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
public class BindingsBsp5 extends Application {
private static final double INITIAL_STAND = 0.3;
@Override
public void start(Stage primaryStage) {
Slider slider = new Slider();
slider.setValue(INITIAL_STAND);
TextField field = new TextField();
field.setText(new Double(INITIAL_STAND).toString());
field.textProperty().bindBidirectional(slider.valueProperty(), NumberFormat.getNumberInstance());
GridPane grid = new GridPane();
grid.add(slider, 0, 0);
grid.add(field, 0, 1);
grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(10));
Scene scene = new Scene(grid);
primaryStage.setScene(scene);
primaryStage.setTitle("Binding-Beispiel");
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Das Beispiel zeigt ein einfaches Fenster lediglich mit einem Textfield
und einem Slider
. Der Quelltext entspricht weitgehend
dem vorhergehenden Beispiel. Lediglich auf die Einbindung des Kontos
wurde verzichtet. Die entscheidende Codezeile ist
field.textProperty().bindBidirectional(slider.valueProperty(), NumberFormat.getNumberInstance());
Hier wird der Wert des Sliders bidirektional an die textProperty
des Textfeldes gebunden. Die Methode nimmt zwei Parameter entgegen:
natürlich die valueProperty
des Sliders und
zusätzlich ein Objekt vom Typ java.text.NumberFormat
.
Es sorgt dafür, dass die Konvertierung zwischen der
String-Repräsentanz des Textfeldes und dem double
-Wert
des Sliderwertes gewährleistet ist. Zur Verwendung in
unterschiedlichen Anwendungsfällen ist die Methode mehrfach
überladen.
Wenn Ihnen javabeginners.de gefällt, freue ich mich über eine Spende an diese gemeinnützigen Organisationen.