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

1

6

What I am trying to do is create a label in fxml, using Scenebuilder, which updates its font size to always ensure that the content of the label is the same size.

Some background info is that I am using an AnchorPane, which is maximized and non-resizable.

I do not need the height of the text to be the same--just the width. Also, I would only like to resize if it is too big to fit. So if the label is only 1 letter, I do not want it to be a giant single letter. I only have rudimentary ideas, of which some pseudocode is below. Thanks!

lengthOfLabel = menuLabel.getText().length();

if(lengthOfLabel > numOfCharsThatCanFitInWidth){
    menuLabel.setStyle("-fx-font-size: " + (int) (someConstant/lengthOfLabel) + ";")
}
Outbalance answered 2/2, 2019 at 17:6 Comment(0)
M
8

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

Mattah answered 3/2, 2019 at 1:48 Comment(5)
This is exactly what I was looking for. Thank you! Followup question. If I want the vertical position of the text to stay the same, would I have to bind the y position of the label to the size, or is there an attribute that I can change to vertically center. (Seems like the y position refers to the top of the label, so the smaller text squishes up to the top) Regardless, thanks!Outbalance
You can use bindings, but it's easier to just set the height of the label and align the text inside however you want: set the height (choose preferred height yourself) lbl.setPrefHeight(50);; to center text vertically: lbl.setAlignment(Pos.CENTER_LEFT); or, to align it to bottom edge: lbl.setAlignment(Pos.BOTTOM_LEFT);. Of course, if the height of the label is already constrained you don't need to set it explicitly.Mattah
It doesnt work for me, because I set the default font in css. Every time I change the text value of the label, the ScenePulseListener overrides the font I have just set. Implies that the font is always the one specified in css. Any ideas to fix this?Gundry
@Gundry In that case, you should use inline styles (they take precedence over the styles defined in css files): lbl.setStyle("-fx-font-size: " + newFontSize + "px;");Mattah
@Guest21 thanks for the advice. I solved it by binding the fontProperty to the font of another label (also defined in css). And then unbinding the proprety when I change the font size again. Using the inline styles should work as well, and is probably a little neater.Gundry

© 2022 - 2024 — McMap. All rights reserved.