Javafx Text Styling for Dynamic Objects

JavaFX Text styling for dynamic objects

There are three ways to apply CSS styling to a JavaFX Node (or a combination of them):

  1. As Tom mentioned, a Nodes css style classes are accessed via Node.getStyleClass(). This returns an ObservableList where you can add and remove your style classes.
  2. If you change a Nodes style rather often, it is better to use pseudo selectors, like :hover or :active with Node.pseudoClassStateChanged().
  3. The third option you already discovered (setStyle()), but I dislike this option because moving the CSS parts into a seperate file offers many advantages.

For #1 and #2 you need to attach a Stylesheet to the Scene/Node, with Scene.getStylesheets() or Parent.getStylesheets(), where your CSS definitions are stored. For example:

listView.getStylesheets().add(getClass().getResource("myStyles.css").toExternalForm());

Addtional info for #1:

If you want to add the style-class my-node to a JavaFX Node:

node.getStyleClass().add("my-node");

now you can style that node in your CSS file:

.my-node {}

Addtional info for #2:

You can declare and use your own pseudo-class with

PseudoClass comment = PseudoClass.getPseudoClass("comment");
node.pseudoClassStateChanged(comment, true);
node.pseudoClassStateChanged(comment, false);

And in your css file:

.my-node:comment {}

Adjust CSS font size rule dynamically in JavaFX

If everything with text displayed under the nodes with "my-view" style class is going to use the same font, then you can just do

.my-view {
-fx-font-size: 12 px ;
}

in the css file.

To update, choose some node that's an ancestor of all your nodes which have the "my-view" style class (it sounds like your tab pane will work, but in general you could just use the root node of the scene) and do

for (Node n: tabPane.lookupAll(".my-view")) {
n.setStyle("-fx-font-size: "+fontSize+"px;");
}

You obviously need to keep track of the font size in the Java code, which is simple enough (just do it from the outset).

If only a subset of the text-displaying nodes are going to have their font sized changed, you will likely have to give them a style class of their own, then do a lookup for that specific style class, instead of "my-view".

JavaFX CSS Dynamic Styling

There are a handful of colors on which everything in modena is based. I had an example somewhere, which I can't find right now, but basically

  • -fx-base
  • -fx-accent
  • -fx-default-button
  • -fx-focus-color
  • -fx-faint-focus-color (same as -fx-focus-color but with opacity of 0x22)

So setting these on a root node will basically theme the entire root and its descendants.

In the end, there's no way around the fact that you will have to somehow update these when the user changes them on each root node, and you need to provide the wiring to do that. Using a CSS file is probably not a way to go, as it's going to be difficult to ensure that an updated file is reloaded as needed. I would probably wire things up so that the styleProperty() of the root node changes to define these colors when the user changes them.

You could consider creating a Theme class that encapsulates these:

public class Theme {

private final ObjectProperty<Color> base = new SimpleObjectProperty<>(Color.web("#ececec"));
private final ObjectProperty<Color> accent = new SimpleObjectProperty<>(Color.web("#0096c9"));
private final ObjectProperty<Color> defaultButton = new SimpleObjectProperty<>(Color.web("#abd8ed"));
private final ObjectProperty<Color> focusColor = new SimpleObjectProperty<>(Color.web("#039ed3"));
private final ObjectProperty<Color> faintFocusColor = new SimpleObjectProperty<>(Color.web("039ed322"));

public ObjectProperty<Color> baseProperty() {
return base ;
}

public final Color getBase() {
return baseProperty().get();
}

public final void setBase(Color base) {
baseProperty().set(base);
}

// etc etc

private final ReadOnlyStringWrapper css = new ReadOnlyStringWrapper() ;

public Theme() {
css.bind(Bindings.createStringBinding(() -> String.format(
"-fx-base: %s; "
+"-fx-accent: %s; "
+"-fx-default-button: %s; "
+"-fx-focus-color: %s ; "
+"-fx-faint-focus-color: %s ;",
toRgba(getBase()),
toRgba(getAccent()),
toRgba(getDefaultButton()),
toRgba(getFocusColor()),
toRgba(getFaintFocusColor())),
base, accent, defaultButton, focusColor, faintFocusColor));
}

private String toRgba(Color color) {
int r = (int) (255 * color.getRed());
int g = (int) (255 * color.getGreen());
int b = (int) (255 * color.getBlue());
int a = (int) (255 * color.getOpacity());
return String.format("#%02x%02x%02x%02x", r, g, b, a);
}

public ReadOnlyStringProperty cssProperty() {
return css.getReadOnlyProperty();
}

}

Then you can create a single Theme instance that you make available to you app, and bind all the root node's styleProperty to the cssProperty. Alternatively you could add a factory method to Theme for generating root nodes:

public <T extends Parent> T createThemedNode(Supplier<T> factory) {
T node = factory.get();
node.styleProperty().bind(cssProperty());
return node ;
}

which you use as, for example,

BorderPane root = theme.createThemedNode(BorderPane::new);

If you use FXML you could create similar types of factory methods for loading a FXML document and binding the style of the resulting node.

Finally, of course, you would just do something like

ColorPicker baseColorPicker = new ColorPicker();
baseColorPicker.valueProperty().bindBidirectional(theme.baseProperty());

etc, and when the user chooses a new color, everything is updated.

How to dynamically change font size in UI to always be the same width in JavaFX?

You can use temp Text object to measure text size, and scale the font if it doesn't fit. Something like this:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class Main extends Application {

//maximum width of the text/label
private final double MAX_TEXT_WIDTH = 400;
//default (nonscaled) font size of the text/label
private final double defaultFontSize = 32;
private final Font defaultFont = Font.font(defaultFontSize);

@Override
public void start(Stage primaryStage) {

final TextField tf = new TextField("Label text goes here");

final Label lbl = new Label();
lbl.setFont(defaultFont);
lbl.textProperty().addListener((observable, oldValue, newValue) -> {
//create temp Text object with the same text as the label
//and measure its width using default label font size
Text tmpText = new Text(newValue);
tmpText.setFont(defaultFont);

double textWidth = tmpText.getLayoutBounds().getWidth();

//check if text width is smaller than maximum width allowed
if (textWidth <= MAX_TEXT_WIDTH) {
lbl.setFont(defaultFont);
} else {
//and if it isn't, calculate new font size,
// so that label text width matches MAX_TEXT_WIDTH
double newFontSize = defaultFontSize * MAX_TEXT_WIDTH / textWidth;
lbl.setFont(Font.font(defaultFont.getFamily(), newFontSize));
}

});
lbl.textProperty().bind(tf.textProperty());

final AnchorPane root = new AnchorPane(lbl, tf);
AnchorPane.setLeftAnchor(tf, 0d);
AnchorPane.setRightAnchor(tf, 0d);
AnchorPane.setBottomAnchor(tf, 0d);

primaryStage.setScene(new Scene(root, MAX_TEXT_WIDTH, 200));
primaryStage.show();
}
}

Note that tmpText.getLayoutBounds() returns the bounds that do not include any transformations/effects (if these are needed, you'll have to add text object to temp scene and calculate its bounds in parent).

Text fits
Text fits, again
Text scaled down

JavaFX: Dynamically Colored Window with CSS

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

Sample Image



Related Topics



Leave a reply



Submit