Search code examples
javacssjavafxpseudo-class

How to change the style of components by their attributes?


I want to display different labels in javafx and i want to style them depending on their text. I added a css file and set the class of the Labels. I then checkd the fxml and found out that the text is saved in the text attribute.

I looked into normal css and found out that you can there change the style by the attributes. You need to use [] for this. I tried this in my code and it didnt work.

My Code: FXML:

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

<?import java.net.URL?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>

<HBox xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1"
      fx:controller="controller">
    <stylesheets>
        <URL value="@../css/loadingScreen.css"/>
    </stylesheets>
    <Label styleClass="field" text="1" />
    <Label styleClass="field" text="2" />
    <Label styleClass="field" text="3" />
</HBox>

CSS:

.field {
    -fx-text-alignment: center;
    -fx-pref-height: 64px;
    -fx-min-width: 64px;
    -fx-pref-width: 64px;
    -fx-min-height: 64px;
    -fx-background-color: blue;
}

.field[text="1"]{
    -fx-background-color: red;
}

.field[text="2"]{
    -fx-background-color: yellow;
}

.field[text="3"]{
    -fx-background-color: green;
}

i tried the same with normal css and html and it worked there. HTML:

<!DOCTYPE html>
<html>
<head>
<style>
.field[text="1"]{
    background-color: red;
}

.field[text="2"]{
    background-color: yellow;
}

.field[text="3"]{
    background-color: green;
}
</style>
</head>
<body>

<div class="field" text="1" >1</div>
<div class="field" text="2" >2</div>
<div class="field" text="3" >3</div>

</body>
</html>

What do i have to do to make this work in fxml?


Solution

  • If I would change the text it would also automatically change the style

    Option 1: control style by id
    You can achieve it by using a custom label that changes style when changing text. I'll demonstrat it by changing the label's id. This simplified example uses the text as id :

    import javafx.geometry.Pos;
    import javafx.scene.control.Label;
    
    public class CustomLabel extends Label{
    
        public CustomLabel() {
            setAlignment(Pos.CENTER);
            setPrefSize(50, 25);
        }
    
        void setTextAndId(String s){
            super.setText(s);
            /*To keep this demo simple and clear id is changed.
              If used, care must be taken to keep id unique. 
              Using setStyle() or PseudoClass should be preferred 
            */
            setId(s); 
        }
    }
    

    The custom label can be used in an fxml (Root.fxml):

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.control.Label?>
    <?import javafx.scene.layout.StackPane?>
    <?import tests.CustomLabel?>
    
    <StackPane fx:id="root" xmlns="http://javafx.com/javafx/10.0.1" xmlns:fx="http://javafx.com/fxml/1" 
    fx:controller="tests.Controller">
       <children>
          <CustomLabel fx:id="cLabel" text="&quot;&quot;" />
       </children>
    </StackPane>
    

    A simple css that changes background color based on id (Root.css):

    #1{
        -fx-background-color: red;
        -fx-text-fill: white;
    }
    #2{
        -fx-background-color: yellow;
        -fx-text-fill: red;
    }
    #3{
        -fx-background-color: green;
        -fx-text-fill: yellow;
    }
    

    Test class:

    import java.io.IOException;
    import javafx.application.Application;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    
    public class LabelCssTest extends Application {
    
        @Override public void start(Stage stage) throws IOException {
            Parent root = FXMLLoader.load(getClass().getResource("Root.fxml"));
            stage.setScene(new Scene(root));
            stage.show();
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    

    And test controller :

    import javafx.animation.PauseTransition;
    import javafx.fxml.FXML;
    import javafx.scene.Parent;
    import javafx.util.Duration;
    
    public class Controller {
    
        @FXML
        CustomLabel cLabel;
        @FXML Parent root;
        private static final int MIN_VALUE = 1, MAX_VALUE = 3;
        private int counter = MIN_VALUE;
    
        @FXML
        private void initialize() {
    
            root.getStylesheets().add(getClass().getResource("Root.css").toExternalForm());
            cLabel.setTextAndId(String.valueOf(counter++));
    
            PauseTransition pause = new PauseTransition(Duration.seconds(2));
            pause.setOnFinished(event ->{
                cLabel.setTextAndId(String.valueOf(counter++));
                if(counter > MAX_VALUE) {
                    counter = MIN_VALUE;
                }
                pause.play();
            });
            pause.play();
        }
    }
    

    Option 2: control style by changing style-class
    Use the same test class as option 1.

    Root.fxml:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.control.Label?>
    <?import javafx.scene.layout.StackPane?>
    
    <StackPane fx:id="root" xmlns="http://javafx.com/javafx/10.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="tests.Controller">
       <children>
          <Label fx:id="label" alignment="CENTER" contentDisplay="CENTER" prefHeight="20.0" prefWidth="70.0" text="&quot; &quot;" />
       </children>
    </StackPane>
    

    Root.css:

    .style1{
        -fx-background-color: red;
        -fx-text-fill: white;
    }
    .style2{
        -fx-background-color: yellow;
        -fx-text-fill: red;
    }
    .style3{
        -fx-background-color: green;
         -fx-text-fill: yellow;
    }
    

    And controller:

    import javafx.animation.PauseTransition;
    import javafx.beans.binding.Bindings;
    import javafx.beans.property.SimpleIntegerProperty;
    import javafx.beans.value.ChangeListener;
    import javafx.fxml.FXML;
    import javafx.scene.Parent;
    import javafx.scene.control.Label;
    import javafx.util.Duration;
    
    public class Controller {
    
        @FXML Label label;
        @FXML Parent root;
    
        private static final int MIN_VALUE = 1, MAX_VALUE = 3;
        private SimpleIntegerProperty counter = new SimpleIntegerProperty();
    
        @FXML
        private void initialize() {
    
             root.getStylesheets().add(getClass().getResource("Root.css").toExternalForm());
            counter = new SimpleIntegerProperty();
            counter.addListener((ChangeListener<Number>) (observable, oldValue, newValue) -> {
                label.getStyleClass().clear();
                label.getStyleClass().add("style"+counter.get());
            });
            label.textProperty().bind(Bindings.createStringBinding(()->String.valueOf(counter.get()), counter));
            counter.set(1);
    
            PauseTransition pause = new PauseTransition(Duration.seconds(2));
            pause.setOnFinished(event ->{
                counter.set(counter.get() >= MAX_VALUE ? MIN_VALUE : counter.get()+1);
                pause.play();
            });
            pause.play();
        }
    }
    

    Option 3: control style by using PseudoClass:
    Changes from option 2:
    Root.css:

    .root:style1 #label{
        -fx-background-color: red;
        -fx-text-fill: white;
    }
    .root:style2 #label{
        -fx-background-color: yellow;
        -fx-text-fill: red;
    }
    .root:style3 #label{
        -fx-background-color: green;
        -fx-text-fill: yellow;
    }
    

    And controller:

    import javafx.animation.PauseTransition;
    import javafx.beans.binding.Bindings;
    import javafx.beans.property.SimpleIntegerProperty;
    import javafx.beans.value.ChangeListener;
    import javafx.css.PseudoClass;
    import javafx.fxml.FXML;
    import javafx.scene.Parent;
    import javafx.scene.control.Label;
    import javafx.util.Duration;
    
    public class Controller {
    
        @FXML Label label;
        @FXML Parent root;
    
        private static final int MAX_VALUE = 3;
        private SimpleIntegerProperty counter = new SimpleIntegerProperty(1);
    
        @FXML
        private void initialize() {
    
            root.getStylesheets().add(getClass().getResource("Root.css").toExternalForm());
            counter = new SimpleIntegerProperty();
            counter.addListener((ChangeListener<Number>) (observable, oldValue, newValue) -> {
                updateStates();
            });
    
            label.textProperty().bind(Bindings.createStringBinding(()->String.valueOf(counter.get()), counter));
            counter.set(1);
    
            PauseTransition pause = new PauseTransition(Duration.seconds(2));
            pause.setOnFinished(event ->{
                counter.set(counter.get() >= MAX_VALUE ? 1 : counter.get()+1);
                pause.play();
            });
            pause.play();
        }
    
        private void updateStates() {
            for( int index = 1; index <= MAX_VALUE; index++){
                PseudoClass pc = PseudoClass.getPseudoClass("style"+String.valueOf(index));
                root.pseudoClassStateChanged(pc, index == counter.get()  ? true : false);
            }
        }
    }