cssjavafxfxml

Bond Font Size and Image with Thresholds


I'd like to make my application resizable. I want to resize all child elements and its font size and images depending on the height and width. But I do not want that font size and image fall under or exceed a certain value.

I followed the instructions on the following page: Bind Font Size in JavaFX?

I am attaching a cut of my code:

FXFontBindApp.java:

package how.to.resize.in.java.fx;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class FXFontBindApp extends Application {
    private DoubleProperty fontSize = new SimpleDoubleProperty(10);

    @Override
    public void start(Stage primaryStage) throws Exception {
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/FontResizeExample.fxml"));
        StackPane pane = fxmlLoader.load();
        Scene scene = new Scene(pane);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Font Resize Example");
        fontSize.bind(scene.widthProperty().add(scene.heightProperty()).divide(50));
        pane.styleProperty().bind(Bindings.concat("-fx-font-size: ", fontSize.asString()));
        primaryStage.show();
    }
}

FXFontBindController.java:

package how.to.resize.in.java.fx;

import javafx.fxml.Initializable;

import java.net.URL;
import java.util.ResourceBundle;

public class FXFontBindController implements Initializable {
    @Override
    public void initialize(URL location, ResourceBundle resources) {

    }
}

FontResizeExample.fxml:

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

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.image.*?>
<?import javafx.scene.layout.*?>

<?import javafx.collections.FXCollections?>
<?import java.lang.String?>
<StackPane fx:id="stackPane" stylesheets="@root.css" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="how.to.resize.in.java.fx.FXFontBindController">
    <AnchorPane fx:id="anchorPane">
        <VBox fx:id="outerVBox" spacing="10.0" AnchorPane.bottomAnchor="10.0" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="10.0">
            <VBox fx:id="header" alignment="TOP_CENTER" VBox.vgrow="ALWAYS">
                <Label fx:id="headerTitle" styleClass="title" text="Please enter username and password"/>
            </VBox>
            <VBox fx:id="enterRootPasswords" alignment="CENTER" VBox.vgrow="ALWAYS">
                <VBox fx:id="rootPasswordFields" spacing="5.0" VBox.vgrow="ALWAYS">
                    <PasswordField fx:id="pin1" promptText="Pin" />
                    <HBox VBox.vgrow="ALWAYS">
                        <Region HBox.hgrow="ALWAYS" />
                        <Hyperlink fx:id="forgotPassword" alignment="CENTER" text="Forgot Password?" textAlignment="JUSTIFY" HBox.hgrow="ALWAYS" />
                    </HBox>
                </VBox>
            </VBox>
            <VBox alignment="CENTER" VBox.vgrow="ALWAYS">
                <VBox alignment="CENTER" VBox.vgrow="ALWAYS">
                    <Label fx:id="selectCardReaderLabel" text="Select Smart Card Reader" />
                    <ComboBox fx:id="selectCardReaderCombo" promptText="Select Smart Card Reader">
                        <items>
                            <FXCollections fx:factory="observableArrayList">
                                <String fx:value="Three"/>
                                <String fx:value="Two"/>
                                <String fx:value="One"/>
                            </FXCollections>
                        </items>
                    </ComboBox>
                </VBox>
            </VBox>
            <VBox fx:id="footer" alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
                <HBox fx:id="hBoxOkCancel" VBox.vgrow="ALWAYS">
                    <Region HBox.hgrow="ALWAYS" />
                    <Button fx:id="btnLogin" graphicTextGap="10.0" text="OK" HBox.hgrow="ALWAYS" VBox.vgrow="ALWAYS">
                        <graphic>
                            <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
                                <Image url="@hook.png" />
                            </ImageView>
                        </graphic>
                    </Button>
                </HBox>
            </VBox>
            <padding>
                <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
            </padding>
        </VBox>
    </AnchorPane>
</StackPane>

root.css:

.label {
    -fx-font-size: 0.9375em;
    -fx-font-family: "Arial Black";
}
.title{
    -fx-font-size: 1.25em;
    -fx-font-family: "Arial Black";
}
.password-field {
    -fx-font-size: 0.9375em;
    -fx-font-family: "Arial Black";
}
.hyperlink {
    -fx-font-size: 0.75em;
    -fx-font-family: "Arial Black";
    -fx-text-fill: #005f6a;
}
.button {
    -fx-background-color: green;
    -fx-text-fill: #ffffff;
    -fx-font-family: "Arial Black";
    -fx-font-size: 1em;
    -fx-cursor: hand;
}
.button:hover {
    -fx-background-color: darkgreen;
}
.combo-box-base {
    -fx-background-color: green;
    -fx-background-radius: 0;
    -fx-font-size: 0.875em;
}
.combo-box-base:hover {
    -fx-background-color: darkgreen;
}
.combo-box .list-cell {
    -fx-text-fill: white;
    -fx-font-size: 0.875em;
    -fx-font-family: "Arial Black";
}
.combo-box-base .arrow-button .arrow {
    -fx-background-color: white;
}
.combo-box-base:showing {
    -fx-background-color: darkgreen;
}
.combo-box-base:showing:hover {
    -fx-background-color: darkgreen;
}
.combo-box .combo-box-popup .list-view .list-cell {
    -fx-background-color: green;
}
.combo-box .combo-box-popup .list-view .list-cell:hover,
.combo-box .combo-box-popup .list-view .list-cell:selected {
    -fx-background-color: darkgreen;
}

Furthermore, the two lines, see below, change the font size slightly. How can I fix this additionally?

        fontSize.bind(scene.widthProperty().add(scene.heightProperty()).divide(50));
        pane.styleProperty().bind(Bindings.concat("-fx-font-size: ", fontSize.asString()));

hook


Solution

  • Related question and answer

    If adopting the letterboxing scaling solution from this question, also see the related question and answer:

    Direct answer using font size binding

    You can bind the font size to the max of the calculated size and your desired minimum size. That way the size will never go below your minimum.

    fontSize.bind(
            Bindings.max(
                    scene.widthProperty().add(
                            scene.heightProperty()
                    ).divide(50),
                    MIN_FONT_SIZE
            )
    );
    

    FontSizingApp.java

    import javafx.application.Application;
    import javafx.beans.binding.Bindings;
    import javafx.beans.property.DoubleProperty;
    import javafx.beans.property.SimpleDoubleProperty;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    
    public class FontSizingApp extends Application {
        private static final double MIN_FONT_SIZE = 12;
    
        @Override
        public void start(Stage stage) throws Exception {
            // load the content
            FXMLLoader fxmlLoader = new FXMLLoader(
                    FontSizingApp.class.getResource(
                            "fontResizeSample.fxml"
                    )
            );
            Scene scene = new Scene(
                    fxmlLoader.load()
            );
    
            // adjust the font size of the scene root to scale the scene content based on scene size.
            DoubleProperty fontSize = new SimpleDoubleProperty(MIN_FONT_SIZE);
            fontSize.bind(
                    Bindings.max(
                            scene.widthProperty().add(
                                    scene.heightProperty()
                            ).divide(50),
                            MIN_FONT_SIZE
                    )
            );
    
            scene.getRoot().styleProperty().bind(
                    Bindings.concat(
                            "-fx-font-size: ",
                            fontSize.asString()
                    )
            );
    
            // show the stage.
            stage.setScene(scene);
            stage.setTitle("Font Sizing Example");
            stage.show();
        }
    }
    

    The rest of this answer uses a different approach, but the font size solution may still be a valid approach for you.

    Alternate answer using scaling

    An example based on DaveB's suggestion in comments to:

    scale the root element of your window using scaleX, and scaleY

    Uses an update of a solution from an old question:

    JavaFX fullscreen - resizing elements based upon screen size

    The concept behind the solution (letterboxing a UI resized using scale transforms) is provided in the above answer so I won't repeat it here.

    Notes on UI scaling

    There are advantages and disadvantages to both various UI scaling approaches, those are also discussed in some other answers I have provided on this site regarding interface scaling.

    The standard method will be to use neither font size-based scaling nor scaling-based scaling, but instead to use layout panes to adjust the interface as described in the original Oracle tutorial on JavaFX layouts.

    Scaling is usually best reserved for situations where one of the following applies:

    However, for windowed UIs, often it is best to keep the UI elements at the same scale. If the window is resized, adjust the UI to show more or less content rather than scaling a fixed amount of content up. And if the size changes radically (e.g. from a laptop screen size to a phone screen size), then a different UI design completely is often preferred.

    On scaling images

    Bitmapped formats like images don't scale up that well. If you want them to look good when enlarged, you need high-resolution images. An alternative is to use a vector-based image format (e.g. SVG). JavaFX 23 doesn't natively support the full SVG images except through the WebView control. However, regions in JavaFX can have an SVG path shape applied to them (this is how modena.css creates scalable UI widgets such as checkmarks in checkboxes). Additionally, you can directly draw SVGPath shapes in JavaFX.

    Sample solution screenshots

    These screenshots are based on the letterboxing solution, not the font size solution.

    UI at the default size (also configured as the minimum size).

    min size example

    UI displayed full screen.

    full screen sample

    Example code

    LetterBoxingApp.java

    import javafx.application.Application;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    
    public class LetterBoxingApp extends Application {
        // a MIN_SCALE >= 1 will ensure that the scene content is never sized below its initial size.
        private static final double MIN_SCALE = 1;
    
        @Override
        public void start(Stage stage) throws Exception {
            // load the content
            FXMLLoader fxmlLoader = new FXMLLoader(
                    LetterBoxingApp.class.getResource(
                            "letterboxingSample.fxml"
                    )
            );
            Parent root = fxmlLoader.load();
    
            // create a letterboxed scene.
            LetterBoxer letterBoxer = new LetterBoxer();
            Scene scene = letterBoxer.box(
                    root,
                    MIN_SCALE
            );
    
            // show the stage.
            stage.setScene(scene);
            stage.setTitle("Letterboxing Example");
            stage.show();
    
            // optional code to set the minimum stage size to the preferred size.
            stage.setMinWidth(stage.getWidth());
            stage.setMinHeight(stage.getHeight());
        }
    }
    

    LetterBoxer.java

    import javafx.beans.value.ChangeListener;
    import javafx.beans.value.ObservableValue;
    import javafx.geometry.Bounds;
    import javafx.scene.Group;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    import javafx.scene.layout.StackPane;
    import javafx.scene.transform.Scale;
    
    public class LetterBoxer {
        public Scene box(final Parent content) {
            return box(content, 1);
        }
    
        public Scene box(final Parent content, final double minScale) {
            // the content is wrapped in a Group so that the stack centers
            // the scaled node rather than the untransformed node.
            Group group = new Group(content);
            Scene scene = new Scene(group);
    
            // generate a layout pass.
            group.applyCss();
            group.layout();
    
            // determine the default ratio of laid out components.
            final Bounds layoutBounds = group.getLayoutBounds();
            final double initWidth = layoutBounds.getWidth();
            final double initHeight = layoutBounds.getHeight();
            final double ratio = initWidth / initHeight;
    
            // place the root in a stack to keep it centered.
            scene.setRoot(
                    new StackPane(
                            group
                    )
            );
    
            // configure a listener to adjust the size of the scene content (by scaling it).
            BoxParams boxParams = new BoxParams(
                    scene, minScale, ratio, initHeight, initWidth, content
            );
    
            // adjust the size of the scene content (by scaling it) if the size changes.
            BoxSizeAdjuster boxSizeAdjuster = new BoxSizeAdjuster(boxParams);
            scene.widthProperty().addListener(boxSizeAdjuster);
            scene.heightProperty().addListener(boxSizeAdjuster);
    
            return scene;
        }
    
        private record BoxParams(
                Scene scene,
                double minScale,
                double ratio,
                double initHeight, double initWidth,
                Parent content
        ) { }
    
        private static class BoxSizeAdjuster
                implements ChangeListener<Number> {
    
            private final BoxParams boxParams;
            private final Scale scale = new Scale();
    
            public BoxSizeAdjuster(BoxParams boxParams) {
                this.boxParams = boxParams;
    
                scale.setPivotX(0);
                scale.setPivotY(0);
    
                boxParams.content.getTransforms().setAll(scale);
            }
    
            @Override
            public void changed(
                    ObservableValue<? extends Number> observableValue,
                    Number oldValue,
                    Number newValue
            ) {
                final double newWidth = boxParams.scene.getWidth();
                final double newHeight = boxParams.scene.getHeight();
    
                final double scaleFactor =
                        Math.max(
                                newWidth / newHeight > boxParams.ratio
                                        ? newHeight / boxParams.initHeight
                                        : newWidth / boxParams.initWidth,
                                boxParams.minScale
                        );
    
                scale.setX(scaleFactor);
                scale.setY(scaleFactor);
            }
        }
    }
    

    letterBoxingSample.fxml

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import java.lang.String?>
    <?import javafx.collections.FXCollections?>
    <?import javafx.geometry.Insets?>
    <?import javafx.scene.control.*?>
    <?import javafx.scene.image.Image?>
    <?import javafx.scene.image.ImageView?>
    <?import javafx.scene.layout.BorderPane?>
    <?import javafx.scene.layout.VBox?>
    
    <BorderPane stylesheets="@root.css" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1">
       <center>
          <VBox fx:id="outerVBox" spacing="10.0">
             <VBox alignment="TOP_RIGHT" spacing="5.0">
                <children>
                      <PasswordField fx:id="pin1" promptText="Pin" />
                       <Hyperlink fx:id="forgotPassword" alignment="CENTER" text="Forgot Password?" textAlignment="JUSTIFY" />
                </children>
             </VBox>
               <VBox alignment="CENTER">
                   <Label fx:id="selectCardReaderLabel" text="Select Smart Card Reader" />
                   <ComboBox fx:id="selectCardReaderCombo" promptText="Select Smart Card Reader">
                       <items>
                           <FXCollections fx:factory="observableArrayList">
                               <String fx:value="Three" />
                               <String fx:value="Two" />
                               <String fx:value="One" />
                           </FXCollections>
                       </items>
                   </ComboBox>
               </VBox>
              <padding>
                  <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
              </padding>
          </VBox>
       </center>
       <top>
            <Label fx:id="headerTitle" styleClass="title" text="Please enter username and password" BorderPane.alignment="CENTER" />
       </top>
       <bottom>
             <Button fx:id="btnLogin" graphicTextGap="10.0" text="OK" BorderPane.alignment="CENTER_RIGHT">
                 <graphic>
                     <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
                         <Image url="@hook.png" />
                     </ImageView>
                 </graphic>
             </Button>
       </bottom>
       <padding>
          <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
       </padding>
    </BorderPane>
    

    hook.png

    The checkbox image from the question.

    root.css

    The example CSS from the question.