In diesem Beitrag möchten wir einige Vorteile der JavaFX-Bibliothek erläutern, die besonders im Bezug auf das Userinterface eine Rolle spielt.
Als einer der wichtigen Vorteile der JavaFX Bibliothek gilt die Möglichkeit der Nutzung von Styles (CSS), die uns aus dem WWW bekannt sind.
Dadurch verliert sich die Abhängigkeit zu der Bibliothek Look & Feel. Man kann jedoch selbst das Aussehen der Applikation bestimmen, was auch ziemlich flexibel und schön möglich ist. Die Gestaltung kann dynamisch erfolgen, eine Animation beinhalten oder auch 3D Grafik.
Durch die Benutzung der Style-Konzeption ist es nun möglich, die Applikationen mit so genannten „Skins“ zu erstellen, durch die das Aussehen der Applikation komplett von der Businesslogik losgelöst wird und mehr Individualtät erhält. Solche individuellen Skins können sogar separat von einem Designer erstellt werden.
Erstellen wir ein einfaches Beispiel eines Dialogfensters mit einem Button:
public class JavaFXDialog1 extends Application { @Override public void start(Stage stage) { final VBox vbox = new VBox(); final Button button = new Button("test"); vbox.getChildren().addAll(button); final Scene scene = new Scene(vbox, 150, 100); stage.setScene(scene); stage.show(); } public static void main(String[] args) { launch(args); }
Styles kann man unterschiedlich anwenden:
1) Unmittelbar im Code, um z.B. die Schriftfarbe in dem Button zu ändern:
button.setStyle("-fx-text-fill: red");
2) Mit Hilfe einer CSS-Datei, auf die die Klasse Scene ausgerichtet werden soll:
Dafür wird eine Datei mit der Erweiterung .css erstellt und unter dem Projektverzeichnis abgelegt, z.B. /css/styles.css.
Inhalt der Datei:
.button { -fx-text-fill: blue; }
Dabei ist es sehr wichtig, die Entwicklungsumgebung so einzurichten, dass sie diese CSS-Dateien beim Bilden der Applikation auch mitkopiert.
Unter IntelliJ IDEA wird es beispielsweise folgendermaßen gemacht:
Nun ist alles fertig, um die Style-Datei einzubinden:
scene.getStylesheets().add((getClass().getResource("/css/styles.css")).toExternalForm());
Wir starten das Projekt und bekommen folgendes Dialogfenster:
Instruktion .button in der CSS-Datei sagt aus, dass nun alle Knöpfe eine blaue Schriftfarbe haben werden:
final Button button1 = new Button("button1"); final Button button2 = new Button("button2"); vbox.getChildren().addAll(button1, button2);
Und was, wenn das nicht das ist, was wir brauchen? Was, wenn wir einen konkreten Knopf definieren wollen?
3) Abhilfe schafft die userbezogene Definition des Knopf-Styles:
In styles.css schreiben wir:
.button1 { -fx-text-fill: green; }
Und im Code:
button1.getStyleClass().add("button1");
Das Dialogfenster sieht so aus:
Nun haben alle Knöpfe, die mit der Style-Klasse verbunden sind, grüne Schriftfarbe, wobei die Methode add() uns dabei Hinweise gibt, dass wir mehrere von solchen Styles hinzufügen können, wodurch verschiedene Elementeigenschaften erweitert, vordefiniert oder überladen werden.
4) User-Style kann man auch durch den so genannten ID definieren:
In der styles.css schreiben wir:
#button2 { -fx-text-fill: yellow; }
Und im Code:
button2.setId("button2");
Als Ergebnis bekommen wir folgendes Dialogfenster:
D.h. alle Elemente mit der gleichen ID sehen gleich aus.
Was kann man sonst noch Interessantes mit den Styles machen?
Styles können auch die so genannten Verhaltenstrigger bearbeiten, die aus der XAML Welt kommen.
Wenn wir bei dem Beispiel mit dem Knopf bleiben, gehören zu diesen Triggern solche Ereignisse der GUI wie Fokus, Selektion, Mousedown, Mouseover usw. also alles, was man nicht im Dialogfenster haben möchte. Es beinhaltet nur den Buisinesslogin bei den kundenseitigen Änderungswünschen und es wird nur die CSS-Datei geändert und nicht die Logik.
So kann man z.B. mit der folgenden CSS-Definition die Farbe des Knopfes ändern, wenn der User mit der Maus darüber fährt.
.button:hover { -fx-background-color: orange; }
So sieht das Dialogfenster bei Mouseover aus:
Wie bereits oben erwähnt, führt das zum gleichen Verhalten von allen Button-Klassen. Mit diesem Code:
.button1:hover { -fx-background-color: orange; }
werden die Trigger nur für die Elemente angewendet, die auf die Klasse «button1» verweisen.
Diese Vorgehensweise kann man bei vielen Trigger anwenden, bei Knöpfen gibt es z.B. auch noch foused, selected oder presset.
Leider kann das nicht direkt im Code genutzt werden:
button.setStyle(":hover -fx-text-fill: red");
Vielleicht wird diese Möglichkeit in der Zukunft durch JavaFX Developer realisiert.
Wozu brauchen wir das alles? Im Internet kann man noch eine Reihe an Beispielen finden, die deutlicher sind als dieses. Das Ziel des Beitrags war nicht, diese zu kopieren. Uns geht es darum, dass die Konzeption von Syles und Trigger auch für den eigenen Bedarf erweitert werden kann, das ist für uns von Interesse.
Als Beispiel können wir folgende Ausgangssituation betrachten:
wir wollen mit JavaFX eine visuelle Komponente umsetzen, die für die Auswahl der Größe einer im Text eingefügten Tabelle dienen soll. Das Aussehen der Komponente, das Farbschema, die Größe usw., sollen dabei kein Bestandteil der Businesslogik der Komponente sein, sondern über eine externe CSS-Datei konfigurierbar sein.
Das sollte ungefähr so aussehen:
Der User geht mit dem Mauszeiger über die Tabelle und sieht seine ausgewählten Elemente, in dem Fall eine Tabelle mit 7 x 8 Zellen. Beim Klick auf die Komponente sollte die Auswahl an das Programm übermittelt werden, um eine entsprechende Tabelle einzufügen.
Sicherlich kann man auf die Auswahl über den Code reagieren, aber was wenn der eine Kunde eine bestimmte Farbe bevorzugt, der andere aber ganz andere Farben bevorzugt? Möglich ist auch, dass das Farbschema durch ein «Skin» oder sonst wie vordefiniert wird – was dann?
Hier wird nur eine Komponente für die Zellenselektion benötigt, um deren Aussehen es an dieser Stelle aber nicht gehen soll.
Hier können wir auf das vorangegangene Beispiel mit dem Trigger «hover» für den Button anknüpfen. Der wirkt allerdings für jede Zelle, wenn man mit dem Mauszeiger über sie fährt. Die Zellen, die der Mauszeiger verlassen hat, entsprechen dem Trigger nicht mehr und fallen somit aus der Auswahl. Wie können wir den gesamten Bereich ausgewählt behalten?
Für die Lösung dieser Aufgabe wird ein eigener Trigger erstellt, der auf eine bestimmte Eigenschaft des Objektes reagiert, z.B. «bin im Diapason der Selektion» oder inRange, die wir folgendermaßen in der CSS-Datei definieren können:
.MyCell:inRange { -fx-border-width: 0.5; -fx-border-color: #ffffff; -fx-background-color: lightskyblue }
An der Stelle muss man aber sagen, dass dies ist keine einfache Aufgabe ist. Zudem ist die Lösung für die JavaFX Versionen 1.7 und 1.8 gänzlich unterschiedlich. Für die Lösung in der Version 1.7. sind wir auf die Nutzung einer Menge der deprecated-Methoden angewiesen.
Für den Anfang schauen wir uns die Komponente an:
public class JavaFXDialog2 extends Application { @Override public void start(Stage stage) { final VBox vbox = new VBox(); final GridPaneEx table = new GridPaneEx(); table.init(10, 10); final Label label = new Label(); label.setMaxWidth(Double.MAX_VALUE); label.setAlignment(Pos.CENTER); label.setTextAlignment(TextAlignment.CENTER); label.setStyle("-fx-padding: 3 0 5 0"); label.textProperty().bind(table.text); vbox.getChildren().addAll(label, table); final Scene scene = new Scene(vbox, 350, 300); scene.getStylesheets().add((getClass().getResource("/css/styles.css")).toExternalForm()); scene.setFill(null); stage.setScene(scene); stage.show(); } public static void main(String[] args) { launch(args); } private void fireCreateTable(final int cols, final int rows){ System.out.println("cols = " + cols + ", rows = " + rows); } protected class GridPaneEx extends GridPane { public final StringProperty text = new SimpleStringProperty("cancel"); private int cols; private int rows; public GridPaneEx(){ this.setOnMouseExited(new EventHandler() { @Override public void handle(MouseEvent mouseEvent) { text.setValue("cancel"); deselectAll(); } }); } public void init(final int cols, final int rows){ getChildren().clear(); this.cols = cols; this.rows = rows; for (int col = 0; col < cols; col++){ for (int row = 0; row < rows; row++){ final Button rect = new Button(); rect.setMinSize(30, 10); add(rect, col, row); final int selectedCol = col; final int selectedRow = row; rect.setOnMouseMoved(new EventHandler() { @Override public void handle(MouseEvent mouseEvent) { selectRange(selectedCol, selectedRow); text.setValue((selectedCol + 1) + " x " + (selectedRow + 1)); } }); rect.setOnAction(new EventHandler() { @Override public void handle(ActionEvent actionEvent) { fireCreateTable(selectedCol + 1, selectedRow + 1); deselectAll(); } }); } } deselectAll(); } private Node getNodeFromGridPane(int col, int row) { for (Node node : getChildren()) { if (GridPane.getColumnIndex(node) == col && GridPane.getRowIndex(node) == row) { return node; } } return null; } private void selectCell(int col, int row, final boolean select){ final Node node = getNodeFromGridPane(col, row); if (select){ node.setStyle("-fx-border-width: 0.5; -fx-border-color: #ffffff; -fx-background-color: lightskyblue"); } else { node.setStyle("-fx-border-width: 0.5; -fx-border-color: #000000; -fx-background-color: #ffffff"); } } public void deselectAll(){ for (int col = 0; col < cols; col++){ for (int row = 0; row < rows; row++){ selectCell(col, row, false); } } } private void selectRange(int selectedCol, int selectedRow){ deselectAll(); for (int col = 0; col <= selectedCol; col++){ for (int row = 0; row <= selectedRow; row++){ selectCell(col, row, true); } } } } }
Besonders interessant ist die Methode selectCell, in der die Zellenfärbung unmittelbar im Code realisiert wird:
Für normale Zellen:
node.setStyle("-fx-border-width: 0.5; -fx-border-color: #000000; -fx-background-color: #ffffff");
Für Zellen im ausgewählten Bereich:
node.setStyle("-fx-border-width: 0.5; -fx-border-color: #ffffff; -fx-background-color: lightskyblue");
Weil es in der Aufgabenbeschreibung steht, dass klare Farbenzuweisung unmöglich ist, versuchen wir die mit Hilfe eines eigenen Styles in styles.css zu definieren:
#MyCellNormal { -fx-border-width: 0.5; -fx-border-color: #000000; -fx-background-color: #ffffff; } #MyCellInRange { -fx-border-width: 0.5; -fx-border-color: #ffffff; -fx-background-color: lightskyblue }
Und in der Methode selectCell:
private void selectCell(int col, int row, final boolean select){ final Node node = getNodeFromGridPane(col, row); if (select){ node.setId("MyCellNormal"); } else { node.setId("MyCellInRange"); } }
Schon besser, nicht war? D.h. wenn der Kunde mit der Farbwahl unzufrieden sein sollte, kann man sie direkt in der Datei styles.css ändern. Die Logik der Komponente bleibt dabei unverändert.
Allerdings gibt es eine noch elegantere Lösung der Aufgabe: Platzieren wir in styles.css noch zusätzlich 2 Sytles:
.MyCell { -fx-border-width: 0.5; -fx-border-color: #000000; -fx-background-color: #ffffff; } .MyCell:inRange { -fx-border-width: 0.5; -fx-border-color: #ffffff; -fx-background-color: lightskyblue }
Das bedeutet, dass die Zellen sich auf den Style «MyCell» orientieren und wenn der Trigger «inRange» greift, analog zu «hover» oder «presset», ändert sich die Farbe entsprechend.
Aber wie bringen wir der Zelle bei, den Trigger zu starten?
Da wir in unserem Beispiel für die Zellen, Button-Elemente nutzen, ist es erforderlich ihr Verhalten in der so genannten Pseudo-Klasse neu zu definieren. In JavaFX 1.7 wird das so gemacht:
protected static class RangeButton extends Button { public RangeButton(){ getStyleClass().add("MyCell"); } private BooleanProperty inRange = new BooleanPropertyBase() { @Override protected void invalidated() { impl_pseudoClassStateChanged("inRange"); } @Override public Object getBean() { return RangeButton.this; } @Override public String getName() { return "inRange"; } }; public boolean isInRange() { return inRange.get(); } public void setInRange(boolean value) { inRange.set(value); } private static final long IN_RANGE_PSEUDOCLASS_STATE = StyleManager.getInstance().getPseudoclassMask("inRange"); @Override public long impl_getPseudoClassState() { long mask = super.impl_getPseudoClassState(); if (isInRange()) mask |= IN_RANGE_PSEUDOCLASS_STATE; return mask; } }
Wie wir sehen, sind alle Methoden «…PseudoClass…» – deprecated.
Jetzt nutzen wir statt Button unseren RangeButton. Und die Methode selectCell sieht nun so aus:
private void selectCell(int col, int row, final boolean select){ final Node node = getNodeFromGridPane(col, row); ((RangeButton)node).setInRange(select); }
D.h. die Änderung der Feldeigenschaft «InRange» führt dazu, dass der Style-Trigger greift und die Farbe der ausgewählten Zellen sich entsprechend ändert.
Das ist genau das, was wir brauchen!
In JavaFX 1.7 funktioniert das. In JavaFX 1.8 ist das leider verboten. Der Code ist nicht mehr kompilierbar sobald JVM 1.8 hinzugeschaltet wird.
Was bietet uns dann die neue Version an der Stelle?
Wie bereits erwartet, wurden die deprecated Methoden entfernt und die Architektur vereinfacht. Jetzt reicht es daher aus, wenn wir folgendes tun:
protected static class RangeButton extends Button { protected final PseudoClass pcInRange = PseudoClass.getPseudoClass("inRange"); public RangeButton(){ getStyleClass().add("MyCell"); } protected final BooleanProperty inRange = new BooleanPropertyBase() { @Override protected void invalidated() { pseudoClassStateChanged(pcInRange, getValue()); } @Override public Object getBean() { return RangeButton.this; } @Override public String getName() { return "inRange"; } }; public boolean isInRange() { return inRange.get(); } public void setInRange(boolean value) { inRange.set(value); } }
Alles funktioniert wie bisher.
Man kann den Code noch etwas vereinfachen…:
protected static class RangeButton extends Button { protected final BooleanProperty inRange; public RangeButton(){ getStyleClass().add("MyCell"); final PseudoClass pcInRange = PseudoClass.getPseudoClass("inRange"); inRange = new SimpleBooleanProperty(); inRange.addListener(new ChangeListener() { @Override public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) { pseudoClassStateChanged(pcInRange, newValue); } }); }
…und die Methode entsprechend ändern:
private void selectCell(int col, int row, final boolean select){ final Node node = getNodeFromGridPane(col, row); ((RangeButton)node).inRange.setValue(select); }
Zusammenfassung: Mit Hilfe der beschriebenen Lösung auf JavaFX ist es möglich, spezielle userbezogene Eigenschaften der Komponenten zu erstellen und diese mit den Styles zu verknüpfen,. Das ist besonders bequem, wenn die Anforderung besteht, die Businesslogik komplett von der GUI zu abstrahieren.