DE   |   EN   |   RU

Keltenring 17, 82041 Oberhaching bei München  |  Tel.: +49 89 4554 6533  |  E-Mail: info@intechcore.com 

JavaFX, CSS-Styles sowie Eigenschaften der Komponenten

In dem Beitrag möchten wir einige Vorteile der JavaFX Bibliothek erläutern die besonders im Bezug auf Userinterface eine Rolle spielen.
Als einer der wichtigen Vorteile der JavaFX Bibliothek gilt die Möglichkeit der Nutzung von Styles (CSS), die uns aus der WWW eher bekannt sind.
Dadurch verliert sich die Abhängigkeit zu der Bibliothek Look&Feel. Man kann nun selbst das Aussehen der Applikation bestimmen. Das geschieht ziemlich flexibel und schön, kann dynamisch erfolgen, kann eine Animation beinhalten, sowie 3D Grafik.
Durch die Benutzung der Style-Konzeption ist es nun möglich die Applikationen mit den so genannten „Skins“ zu erstellen, wo das Aussehen der Applikation komplett von der Businesslogik getrennt ist und sogar separat, von einem Designer, erstellt werden kann.

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);
    }

avafx and css styles 1

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");

avafx and css styles 2

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 mit kopiert.
Unter IntelliJ IDEA wird es beispielsweise folgendermassen gemacht:

avafx and css styles 3

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:
avafx and css styles 4

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);

avafx and css styles 5

Und was, wenn das nicht das was wir brauchen? Wass, 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:
avafx and css styles 6

Nun werden alle Knöpfe, die mit der Style-Klasse verbunden sind, grüne Schriftfarbe haben, 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:

avafx and css styles 7

D.h. alle Elemente mit der gleichen ID werden gleich aussehen.

Was kann man denn sonst noch Interessantes mit den Styles machen?

Styles können auch die so genannten Verhaltenstrigger bearbeiten, die zu uns aus der XAML Welt gekommen sind.
Wenn wir bei dem Beispiel mit dem Knopf bleiben, dann gehören zu diesen Trigger solche Ereignisse der GUI wie Fokus, Selektion, Mousedown, Mouseover usw. also alles, was man nicht in dem Dialogfenster haben möchte. Es wird nur Bisinesslogin beinhalten und bei den kundenseitigen Änderungswünschen, wird nur die CSS-Datei gerändert und nicht die Logik.
Z.B. kann man mit der folgenden CSS-Definition die Farbe des Knopfes ändern, wenn der User mit der Maus drüber fährt.

.button:hover {
    -fx-background-color: orange;
}

So sieht das Dialogfenster bei Mouseover aus:

avafx and css styles 8

Wobei, wie bereits oben erwähn, das führt zum gleichen Verhalten von allen Button-Klassen, und 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 anwenden bei vielen Trigger, z.B. bei Knöpfen gibt es auch noch foused, selected, presset.
Leider kann das nicht direkt im Code genutzt werden:

button.setStyle(":hover -fx-text-fill: red");

Vielleicht wird in der Zukunft diese Möglichkeit durch JavaFX Developer realisiert.

Wozu brauchen wir das alles? Im Internet kann man eine Menge an Beispielen finden, die viel mächtiger sind als dieses. Das Ziel von dem Beitrag war nicht, diese zu kopieren. Uns geht es darum, dass die Konzeption von Syles und Trigger kann auch für den eigenen Bedarf erweitert werden, und das stell für uns Interesse dar.

Als Beispiel können wir folgende Ausgangssituation betrachten:
wir wollen mit JavaFX eine visuelle Komponente umsetzen, die für den Auswahl der Größe einer im Text eingefügten Tabelle dienen soll, wobei das Aussehen der Komponente, das Farbschema, die Größe usw., kein Bestandteil von Businesslogik der Komponente sein sollen, sonder über eine externe CSS-Datei konfigurierbar sind.
Das sollte ungefähr so aussehen:

avafx and css styles 9

Der User geht mit dem Mauszeiger über die Tabelle und sieht Selektion. In dem Fall für eine Tabelle mit 7×8 Zellen. Beim Klick auf die Komponente sollte der Auswahl dem Programm übergeben werden, um eine entsprechende Tabelle einzufügen.
Sicherlich, kann man auf die Selektion über den Code reagieren, aber was wenn ein Kunde eine bestimmte Farbe bevorzugt und der andere – eine andere Farbe?! Oder das Farbschema wird durch «Skin» definiert, oder sonst wie – was machen wir dann?

Hier wird nur eine Komponente für die Zellenselektion gebraucht, das Aussehen davon ist nicht ihre Sorge.

Offenbar, können wir hier 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 geht. Die Zellen, der Mauszeiger verlassen hat, entsprechen dem Trigger nicht mehr und verlieren somit die Selektion. Wie können wir den gesamten Bereich ausgewählt behalten?

Für die Aufgabenlösung wird ein eigener Trigger erstellt, der auf eine bestimmte Eigenschaft des Objektes reagiert, z.B. «bin im Diapason der Selektion» oder inRange, die wir folgendermassen 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, dies ist keine einfache Aufgabe. Und die Lösung ist für die JavaFX Versionen 1.7 und 1.8 ganz unterschiedlich. Für die Lösung in der 1.7. Version sind wir auf die Benutzung von 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, dann kann man die 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», soll sich die Farbe entsprechend ändern.
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 wir 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 ändert sich entsprechend.
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 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 von der beschriebenen Lösung auf JavaFX ist es möglich spezielle, userbezogene Eigenschaften der Komponenten zu erstellen und die mit den Styles zu verknüpfen, was besonders bequem ist, wenn die Anforderung besteht, die Businesslogik komplett von der GUI zu abstrahieren.

Veröffentlicht unter Aktuelles, Artikel, News