Chris_D_Turk
Chris_D_Turk

Reputation: 452

JavaFX - Bind stage size to the root node's preferred size

I would like to automatically adjust the width/height of a javafx.stage.Stage whenever the preferred width/height of the Scene's root node changes.

It's a small utility window (not resizable by the user) that contains multiple javafx.scene.control.TitledPanes and the window's height should increase when one of these TitledPanes is expanded (otherwise the TitlePane's content may be out of bounds).

Unfortunately I could only find javafx.stage.Window.sizeToScene(), which initially sets the window's size to the root's preferred size.

Is there a way to permanently bind the Stage size to the root node size?

Upvotes: 1

Views: 10245

Answers (2)

DVarga
DVarga

Reputation: 21799

If you want/can set the preferred size of the root container of the Scene

Class Region has a prefHeightProperty and a prefWidthProperty, and class Stage has a setWidth method and a setHeight method.

You can listen to the property change of the root Region of the scene of your stage, and in the listener you can call the corresponding mutator method of your stage:

root.prefHeightProperty().addListener((obs, oldVal, newVal) -> primaryStage.setHeight(newVal.doubleValue()));
root.prefWidthProperty().addListener((obs, oldVal, newVal) -> primaryStage.setWidth(newVal.doubleValue()));

An example that I used for testing:

public class Main extends Application {
    @Override
    public void start(final Stage primaryStage) {
        try {
            // Add some controls to set the pref size of the root VBox
            final VBox par = new VBox();
            final TextField tf1 = new TextField();
            final TextField tf2 = new TextField();

            Button b1 = new Button();
            b1.setOnAction(new EventHandler<ActionEvent>() {

                @Override
                public void handle(ActionEvent arg0) {
                    // On button press set the pref size of the VBox to the values of the TextFields
                    par.setPrefSize(Double.parseDouble(tf1.getText()), Double.parseDouble(tf2.getText()));

                }
            });

            par.getChildren().addAll(tf1, tf2, b1);

            Scene scene = new Scene(par,400,400);

            // Attach the listeners
            par.prefHeightProperty().addListener((obs, oldVal, newVal) -> primaryStage.setHeight(newVal.doubleValue()));
            par.prefWidthProperty().addListener((obs, oldVal, newVal) -> primaryStage.setWidth(newVal.doubleValue()));


            primaryStage.setScene(scene);
            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

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

Update: If you do not want/cannot set the preferred size of the root

Layouts query the preferred size of their nodes by invoking the prefWidth(height) and prefHeight(width) methods. By default, UI controls compute default values for their preferred size that is based on the content of the control. For example, the computed size of a Button object is determined by the length of the text and the size of the font used for the label, plus the size of any image. Typically, the computed size is just big enough for the control and the label to be fully visible.

UI controls also provide default minimum and maximum sizes that are based on the typical usage of the control. For example, the maximum size of a Button object defaults to its preferred size because you don't usually want buttons to grow arbitrarily large. However, the maximum size of a ScrollPane object is unbounded because typically you do want them to grow to fill their spaces.

Based on this, the idea is to have a ScrollPane as root, with hidden scrollbars, as this control allows the content to "freely" grow.

As the content can grow, you can listen to the heightProperty and widthProperty of the content of the ScrollPane, and set the size of the Stage.

Example:

public class Main extends Application {
    @Override
    public void start(final Stage primaryStage) {
        try {
            // ScrollPane will be the root
            ScrollPane sp = new ScrollPane();

            // Don't show the ScrollBars
            sp.setHbarPolicy(ScrollBarPolicy.NEVER);
            sp.setVbarPolicy(ScrollBarPolicy.NEVER);

            // TitledPane will be the content of the root
            TitledPane tp = new TitledPane();
            sp.setContent(tp);

            // Set the layout bounds of the TitledPane
            tp.setMinHeight(400);
            tp.setMinWidth(400);
            tp.setMaxHeight(900);
            tp.setMaxWidth(900);

            // Fill the TitledPane with some Buttons and containers to test the growing and shrinking progress
            final VBox vboxTPContentVertical = new VBox();
            final VBox vboxTexts = new VBox();
            final HBox hboxTexts = new HBox();

            HBox hboxButtons = new HBox();
            vboxTPContentVertical.getChildren().addAll(hboxButtons, hboxTexts, vboxTexts);


            Button b1 = new Button("Add row");
            b1.setOnAction((event) -> vboxTexts.getChildren().addAll(new Text("Row1"), new Text("Row2")));

            Button b2 = new Button("Add column");
            b2.setOnAction((event) -> hboxTexts.getChildren().addAll(new Text("Col1"), new Text("Col2")));

            Button b3 = new Button("Remove row");
            b3.setOnAction((event) -> {
                vboxTexts.getChildren().remove(0);
                vboxTexts.getChildren().remove(0);
            });

            Button b4 = new Button("Remove column");
            b4.setOnAction((event) -> {
                hboxTexts.getChildren().remove(0);
                hboxTexts.getChildren().remove(0);
            });

            hboxButtons.getChildren().addAll(b1, b2, b3, b4);
            tp.setContent(vboxTPContentVertical);

            // Set the ScrollPane as root
            Scene scene = new Scene(sp, 400, 400);

            // Now just listen to the heightProperty and widthProperty of the TitledPane
            tp.heightProperty().addListener((obs, oldVal, newVal) -> primaryStage.setHeight(to.doubleValue()));
            tp.widthProperty().addListener((obs, oldVal, newVal) -> primaryStage.setWidth(to.doubleValue()));

            primaryStage.setScene(scene);
            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

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

Upvotes: 1

Chris_D_Turk
Chris_D_Turk

Reputation: 452

Based on DVarga's example I crafted the following "solution":
A InvalidationListener is installed on the heightProperty of every child node that may shrink/grow in height (two TitlePanes in this example).
When a heightProperty is invalidated, the height of the containing window gets recalculated (also honering window decorations).

Example:

public class SampleApp extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        final Label lbl1 = new Label("content");
        final TitledPane tp1 = new TitledPane("First TP", lbl1);

        final Label lbl2 = new Label("more content");
        final TitledPane tp2 = new TitledPane("Second TP", lbl2);

        final VBox rootPane = new VBox(tp1, tp2);

        tp1.heightProperty().addListener((InvalidationListener) observable -> {
            updateWindowHeight(rootPane);
        });

        tp2.heightProperty().addListener((InvalidationListener) observable -> {
            updateWindowHeight(rootPane);
        });

        final Scene scene = new Scene(rootPane);
        primaryStage.setScene(scene);
        primaryStage.sizeToScene();
        primaryStage.setResizable(false);
        primaryStage.show();
    }

    private void updateWindowHeight(final VBox rootPane) {
        final Scene scene = rootPane.getScene();
        if (scene == null) 
            return;
        final Window window = scene.getWindow();
        if (window == null)
            return;
        final double rootPrefHeight = rootPane.prefHeight(-1);
        final double decorationHeight = window.getHeight() - scene.getHeight(); // window decorations
        window.setHeight(rootPrefHeight + decorationHeight);
    }

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

Although it works as intended, there are some major drawbacks with this solution:

  • You have to install listeners on every node that might shrink/grow in size
  • From a CCD perspective this just wrong: Child-Nodes should not be responsible for sizing the stage they are contained by.

I couldn't get a cleaner solution to work. javafx.stage.Stage and javafx.scene.Scene are just too obstructed (possible extension points are final or package-private) to implement this feature where it belongs to.

Update
Using just window.sizeToScene() instead of

final double rootPrefHeight = rootNode.prefHeight(-1);
final double decorationHeight = window.getHeight() - scene.getHeight();
window.setHeight(rootPrefHeight + decorationHeight);

generates a lot less "stutter" when adjusting the window size!

Upvotes: 4

Related Questions