javafxfxml

FXML: An elegant way to bind a child width/height to the parent's width/height


I'm using FXML with JavaFX to develop graphical interfaces. I discovered this method for binding the width/height of a child node to the width/height of it's parent node:

<Node fx:id="parent">
    <children>
        <Node prefWidth=${parent.width}></Node>
    <children>
</Node>

But that feels inelegant. I'd rather not have to give an id to a node just so it's child can reference it's dimensions. Is there perahps some method for referenceing the parent of a node? Maybe something like prefWidth=${parent.width}?

NOTE: I know this can be done programmatically from Java, but that's not what I want here. I'm looking for a way to do this in the FXML.


Solution

  • Expression bindings work by referencing elements of the FXMLLoader's namespace. There is no concept of "this" to refer to "the current node" (that I am aware of). So the only way to access the parent of a node is for the node itself to be part of the namespace: you can achieve this by setting an fx:id on the node itself:

    <Pane>
        <children>
            <Node fx:id="node" prefWidth="${node.parent.width}"></Node>
        <children>
    </Pane>
    

    Here's a complete working example, using the default FX project created by IntelliJ. Note that IntelliJ (and probably other IDEs) doesn't really understand expression binding properly, so it flags width in the FXML binding as an error, but if you run it (I am using JavaFX 22.0.1 on JDK 22 on Mac OS X) and change the size of the window (forcing the VBox to change width) you will see the output indicating the preferred width is changing.

    hello-view.fxml

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.geometry.Insets?>
    <?import javafx.scene.control.Label?>
    <?import javafx.scene.layout.VBox?>
    
    <?import javafx.scene.control.Button?>
    <VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml"
          fx:controller="org.jamesd.examples.fxmlbind.HelloController">
        <padding>
            <Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
        </padding>
    
        <Label fx:id="welcomeText" prefWidth="${welcomeText.parent.width}"/>
        <Button text="Hello!" onAction="#onHelloButtonClick"/>
    </VBox>
    

    HelloController.java

    package org.jamesd.examples.fxmlbind;
    
    import javafx.fxml.FXML;
    import javafx.scene.control.Label;
    
    public class HelloController {
        @FXML
        private Label welcomeText;
    
        @FXML
        private void initialize() {
            welcomeText.prefWidthProperty().subscribe((oldW, newW) -> System.out.printf("%.1f -> %.1f%n", oldW, newW));
        }
    
        @FXML
        protected void onHelloButtonClick() {
            welcomeText.setText("Welcome to JavaFX Application!");
        }
    }
    

    HelloApplication.java

    package org.jamesd.examples.fxmlbind;
    
    import javafx.application.Application;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    
    import java.io.IOException;
    
    public class HelloApplication extends Application {
        @Override
        public void start(Stage stage) throws IOException {
            FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
            Scene scene = new Scene(fxmlLoader.load(), 320, 240);
            stage.setTitle("Hello!");
            stage.setScene(scene);
            stage.show();
        }
    
        public static void main(String[] args) {
            launch();
        }
    }
    

    Console output after changing window size:

    0.0 -> 320.0
    320.0 -> 390.0
    390.0 -> 454.0
    454.0 -> 498.0
    498.0 -> 511.0