2

I am attempting to create a Pane object with JavaFX that has three different colors: A color for the background, a color for the text, and a color for the buttons. Each of these three colors is determined dynamically at run-time, based on the values of a custom object passed into the method. I figured out how to implement this behavior directly in my code fairly easily, and it seems like I could make use of a controller and an initialize method to set it up with FXML. But I'm wondering if it would be possible or advisable to set something like this up with CSS.

As far as I can tell, CSS doesn't really make use of variables from the code, just hardcoded values to be set beforehand. Given the sheer number of potential combinations, it doesn't seem like it would be of much worth to make a different sheet for each one. Still, I heard that making use of CSS is modern practice, so I'm wondering if it's possible to use a single sheet to make multiple different types of Panes or if every possible Pane would have to be uniquely defined with its own sheet, even with everything else about the Pane being identical.

MagerBlutooth
  • 23
  • 1
  • 3
  • Why not just use the `setStyle()` method to dynamically apply your CSS? – Zephyr Sep 24 '19 at 21:08
  • You can put all of your styles in a `CSS` and then use `pane.setId("some-style");` to set each pane to a unique style. (If I understand you correctly) https://docs.oracle.com/javafx/2/css_tutorial/jfxpub-css_tutorial.htm – SedJ601 Sep 24 '19 at 21:41

3 Answers3

4

You're lucky, since the only difference are colors. You can use lookedup colors for this puropse: Use my-color-name: <value>; rules for the node itself or for one of the ancestors. This allows you to specify those values inline css and use them in your CSS stylesheet:

@Override
public void start(Stage primaryStage) {
    HBox hBox = new HBox();
    hBox.setMaxHeight(Region.USE_PREF_SIZE);
    hBox.getStyleClass().add("box");

    StackPane root = new StackPane(hBox);

    Stream.of("red", "green", "blue").map(c -> {
        Button b = new Button(c);
        b.setOnAction(evt -> {
            root.setStyle("-my-background:" + c);
        });
        return b;
    }).forEach(hBox.getChildren()::add);

    Scene scene = new Scene(root, 500, 500);
    scene.getStylesheets().add(getClass().getResource("/path/to/my/style.css").toExternalForm());
    primaryStage.setScene(scene);
    primaryStage.show();
}

CSS stlyesheet

.box {
    -fx-background-color: -my-background;
}

You can specify multiple colors this way, e.g.

root.setStyle("-my-color: red; -some-other-color: brown; -color-3: yellow;");
fabian
  • 80,457
  • 12
  • 86
  • 114
3

I agree with the approach of what @Sedrick has mentioned in the comments.

If you want to change only colors without modifying the rest of CSS, you can follow the below approach as well. This can be quite useful if you have a very large css file that needs to be themed.

The basic idea is to have all your css is one base css file. Define all your colors as variables in .root class in that base file. And for each of your theme css, you just need to override the color variables only. And load the theme css file on top of base file. This way you will not encounter any possible copy-paste issues or missing css issues :)

A complete working example is below:

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.stream.Stream;

public class DynamicStyling_Demo extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        VBox root = new VBox();
        root.setAlignment(Pos.CENTER);
        root.setSpacing(10);
        Stream.of("Default", "Type1", "Type2", "Type3").forEach(type -> {
            Button button = new Button("Open " + type);
            button.setOnAction(e -> {
                Stage subStage = buildStage(type);
                subStage.initOwner(stage);
                if (!type.equalsIgnoreCase("default")) {
                    subStage.getScene().getStylesheets().add(this.getClass().getResource(type.toLowerCase() + ".css").toExternalForm());
                }
                subStage.show();
            });
            root.getChildren().add(button);
        });
        Scene sc = new Scene(root, 400, 400);
        sc.getStylesheets().add(this.getClass().getResource("base.css").toExternalForm());
        stage.setScene(sc);
        stage.show();
    }

    private Stage buildStage(String title) {
        Label label = new Label("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.");
        label.setWrapText(true);
        VBox.setVgrow(label, Priority.ALWAYS);
        Button btn = new Button("Sample Button");
        VBox pane = new VBox(label, btn);
        pane.getStyleClass().add("my-pane");

        StackPane subRoot = new StackPane(pane);
        subRoot.setPadding(new Insets(10));

        Stage subStage = new Stage();
        subStage.setTitle(title);
        subStage.setScene(new Scene(subRoot, 300, 300));
        subStage.getScene().getStylesheets().add(this.getClass().getResource("base.css").toExternalForm());
        return subStage;
    }

    public static void main(String[] args) {
        Application.launch(args);
    }
}

base.css:

.root{
   -fx-window-border: #444444;
   -fx-window-color: #999999;
   -fx-window-text: #111111;
   -fx-button-color: #555555;
}

.my-pane{
   -fx-border-width: 2px;
   -fx-border-color: -fx-window-border;
   -fx-background-color: -fx-window-color;
   -fx-padding: 10px;
   -fx-spacing: 10px;
}

.my-pane .label{
  -fx-text-fill: -fx-window-text;
  -fx-font-size: 16px;
}

.my-pane .button{
  -fx-base: -fx-button-color;
}

type1.css:

.root{
   -fx-window-border: red;
   -fx-window-color: yellow;
   -fx-window-text: brown;
   -fx-button-color: pink;
}

type2.css:

.root{
   -fx-window-border: green;
   -fx-window-color: lightblue;
   -fx-window-text: white;
   -fx-button-color: grey;
}

type3.css:

.root{
   -fx-window-border: brown;
   -fx-window-color: lightgreen;
   -fx-window-text: blue;
   -fx-button-color: yellow;
}

enter image description here

Sai Dandem
  • 8,229
  • 11
  • 26
  • As I understand, that would be an effective solution if I were looking to make preset templates of specific color combinations. The catch is the colors aren't part of a matching set. If the text is red, the window could be a number of different colors, which is the crux of my issue. It would be a fool's errand to make a template for each combination. That being said, this may still be my solution if I can override a css file with multiple other css files at once. I just have to make a separate file for each window color, one for each text color, etc. Then determine what to override at runtime. – MagerBlutooth Sep 25 '19 at 13:29
  • 1
    @MagerBlutooth To allow aspects of the CSS to be defined at runtime, you could create the content of any needed CSS files dynamically (using fileIO or an [in memory url representation](https://stackoverflow.com/questions/17384783/is-it-possible-to-create-an-url-pointing-to-an-in-memory-object)), rather than using predefined css files on disk. – jewelsea Sep 25 '19 at 23:07
  • 1
    @MagerBlutooth I updated my answer to demonstrate what I mean by creating needed CSS files dynamically using fileIO. – jewelsea Sep 26 '19 at 00:16
  • JavaFX 17 added an ability to [load style sheets from data URIs](https://bugs.openjdk.java.net/browse/JDK-8267554), so that would be another alternative to creating CSS files dynamically using fileIO or using static, pre-created style sheets. – jewelsea Sep 09 '21 at 13:07
3

Here are a couple of concrete example of setting theme colors for given components dynamically using:

  1. fabian's suggestion of looked-up colors.
  2. programmatically created css stylesheets written to temporary files.

Sample using looked-up colors

What the sample is doing is setting some of the standard looked up colors which were found in modena.css to style the three things you want to style:

  1. A color for the background (-fx-background-color). This is the standard background used in classes which derive from the class Pane.
  2. A color for the text (-fx-text-background-color). Yeah, confusingly named I know, but that seems to be what it is, for whatever reason.
  3. A color for the buttons (-fx-base).

sample colors

In the sample application, the user can pick colors dynamically using JavaFX ColorPicker controls to modify the colors of items displayed in the preview pane.

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class ThemeMaker extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        Pane previewPane = createPreviewPane();
        Pane controlPane = createControlPane(previewPane);

        Pane layout = new VBox(
                20,
                controlPane,
                previewPane
        );
        layout.setPadding(new Insets(10));

        stage.setScene(new Scene(layout));
        stage.show();
    }

    private Pane createControlPane(Pane previewPane) {
        ColorPicker backgroundColorPicker = new ColorPicker(Color.web("#b3ccff"));
        ColorPicker textColorPicker = new ColorPicker(Color.web("#4d804d"));
        ColorPicker controlColorPicker = new ColorPicker(Color.web("#ffe6cc"));

        GridPane controlPane = new GridPane();
        controlPane.setHgap(5);
        controlPane.setVgap(5);
        controlPane.addRow(0, new Label("Background color:"), backgroundColorPicker);
        controlPane.addRow(1, new Label("Text color:"), textColorPicker);
        controlPane.addRow(2, new Label("Control color:"), controlColorPicker);

        backgroundColorPicker.valueProperty().addListener((observable, oldColor, newColor) ->
                setThemeColors(previewPane, backgroundColorPicker.getValue(), textColorPicker.getValue(), controlColorPicker.getValue())
        );
        textColorPicker.valueProperty().addListener((observable, oldColor, newColor) ->
                setThemeColors(previewPane, backgroundColorPicker.getValue(), textColorPicker.getValue(), controlColorPicker.getValue())
        );
        controlColorPicker.valueProperty().addListener((observable, oldColor, newColor) ->
                setThemeColors(previewPane, backgroundColorPicker.getValue(), textColorPicker.getValue(), controlColorPicker.getValue())
        );

        setThemeColors(previewPane, backgroundColorPicker.getValue(), textColorPicker.getValue(), controlColorPicker.getValue());

        return controlPane;
    }

    private void setThemeColors(Pane previewPane, Color backgroundColor, Color textColor, Color controlColor) {
        previewPane.setStyle(
                "-fx-background-color: " + toHexString(backgroundColor) + ";" +
                "-fx-text-background-color: " + toHexString(textColor) + ";" +
                "-fx-base: " + toHexString(controlColor) + ";"
        );
    }

    private Pane createPreviewPane() {
        Label label = new Label(
                "Lorem ipsum dolor sit amet, consectetur adipiscing elit, " +
                        "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.");
        label.setWrapText(true);
        Button btn = new Button("Sample Button");
        Pane previewPane = new VBox(10, label, btn);

        previewPane.setPadding(new Insets(5));
        previewPane.setPrefWidth(200);

        return previewPane;
    }

    // from https://stackoverflow.com/a/56733608/1155209 "How to get hex web String from JavaFX ColorPicker color?"
    private String toHexString(Color value) {
        return "#" + (format(value.getRed()) + format(value.getGreen()) + format(value.getBlue()) + format(value.getOpacity()))
                .toUpperCase();
    }

    private String format(double val) {
        String in = Integer.toHexString((int) Math.round(val * 255));
        return in.length() == 1 ? "0" + in : in;
    }

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

Sample using dynamic stylesheets


Update

JavaFX 17 added an ability to load style sheets from data URIs. That would be an alternative approach to that demonstrated in the example below, which creates CSS files dynamically using file IO.


So the lookup color solution is very powerful because you can dynamically style the color of all items in a scene. However, CSS in general is much more powerful than just color setting. If your dynamic styling requires more than just color setting or you want very specific rules for how to style a particular item in the scene, then you will need your own custom style sheet.

The stylesheets property of nodes and scenes is a dynamic observable list. So if you change the stylesheets, you will restyle the node or scene. Each stylesheet is referred to by a URL. So to dynamically create a style sheet, all you need to do is construct the stylesheet contents in code and write it out to a temporary file, grab a URL reference to the temporary file, and then set that as a stylesheet for the thing you wish to style.

To provide an example of this approach, just take the code from the previous example using looked-up colors and replace the setThemeColors method with the method below. This will then accomplish a dynamic styling of the preview pane using a dynamically created CSS file rather than looked-up colors.

Note: In creating the dynamic stylesheet, I tried to use the .root selector to define styles (similar to Sai's answer), but, for whatever reason, it did not work (perhaps it doesn't work with my version of JavaFX (v13)). So instead I used specific CSS selectors to style items (e.g. label{-fx-text-fill:<custom-color>}). That worked fine, and, as a bonus, it demonstrates the additional level of control you can get by defining your own stylesheets.

private void setThemeColors(Pane previewPane, Color backgroundColor, Color textColor, Color controlColor) {
    try {
        Path cssPath = Files.createTempFile("fx-theme-", ".css");
        Files.writeString(
                cssPath,
                ".themed{-fx-background-color:"+ toHexString(backgroundColor) +";}" +
                ".label{-fx-text-fill:"+ toHexString(textColor) +";}" +
                ".button{-fx-base:" + toHexString(controlColor) + ";}"
        );
        cssPath.toFile().deleteOnExit();

        System.out.println("Wrote " + cssPath);
        System.out.println("URL " + cssPath.toUri().toURL().toExternalForm());

        previewPane.getStyleClass().setAll("themed");
        previewPane.getStylesheets().setAll(
                cssPath.toUri().toURL().toExternalForm()
        );
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Background on looked-up colors

The following documentation is copied from the linked JavaFX CSS reference section on looked-up colors. This is a powerful technique to accomplish what you wish and the concept is (as far as I know) peculiar to JavaFX CSS processing and does not exist with standard HTML based CSS.

With looked-up colors you can refer to any other color property that is set on the current node or any of its parents. This is a very powerful feature, as it allows a generic palette of colors to be specified on the scene then used thoughout the application. If you want to change one of those palette colors you can do so at any level in the scene tree and it will affect that node and all its decendents. Looked-up colors are not looked up until they are applied, so they are live and react to any style changes that might occur, such as replacing a palette color at runtime with the "style" property on a node.

If you search inside the jar files which come with the JavaFX SDK you are using, you will find a file named modena.css. This file pre-defines many looked-up colors that you can override as you with to more easily theme your application. These are not documented anywhere, you need to look at the modena.css file to see what they are (the most useful ones are in the .root section of the file). The most important color is -fx-base which will set a base color for the entire JavaFX control system.

The looked-up colors are often combined with some other JavaFX CSS concepts such as derivation and laddering to create consistent themes that are still readable when the base lookup color changes. This allows you to change the base color for example from white to black and the text displayed in controls based on the base color will automatically change from back to white so that it is still readable.

What is -fx-base?

fx-base is the base color for all controls, so setting it will change the color of all controls in the scene, which is probably what you also want, but perhaps not.

If you only want to change the buttons and not everything in the scene, just set the -fx-base color directly on each of the buttons rather than on an enclosing pane. One tricky way to accomplish that is that you can define your own CSS style for a button and in that, set -fx-base: my-custom-color, then set the my-custom-color style to a value dynamically in your program as shown in fabian's answer.

Note that setting the base color is preferred to trying to set the actual button color. Because, the button itself, when you look at it closely, includes a variety of gradients and shading it derives from the base color, so it is in fact made up of multiple colors when it is rendered and not just a single color.

jewelsea
  • 150,031
  • 14
  • 366
  • 406