2

When I run the following code in the start method of my Main (JavaFX) class I get weird results. The window gets displayed but pane (with a green border) has a width of 0. It is supposed to have the same width as the container's height since I binded prefWidth to the height property. Then, when I resize the window, the binding comes into effect and the pane becomes a square. Notice that if I maximize the window it also doesn't apply the bindings.

Thank you!

//Create a pane with a min width of 10 and a green border to be able to see it
Pane pane = new Pane();
pane.setStyle("-fx-border-color: green; -fx-border-width: 2");

//Bind the pane's preferred width to the pane's height
pane.prefWidthProperty().bind(pane.heightProperty());

//Put the pane in a vbox that does not fill the stage's width and make the pane grow in the vbox
VBox container = new VBox(pane);
container.setFillWidth(false);
VBox.setVgrow(pane, Priority.SOMETIMES);

//Show the vbox
primaryStage.setScene(new Scene(container, 600, 400));
primaryStage.show();
staad
  • 796
  • 8
  • 20
  • Ah, finally. I've been suspecting things like this can happen if you use bindings to manage layout; I just haven't come across an example yet. Thanks :). – James_D May 23 '18 at 01:10
  • I think there are a few things that contributed to this behavior. Firstly, bindings are lazily evaluated. As long as the JavaFX scene graph render pulse does not call the getter of `prefWidthProperty` then `prefWidthProperty` will hold the old value with `invalid` set to `true`. So apparently, JavaFX does not query this value every pulse, that's why the binding does not seem to work. You can replace with a `ChangeListener` and it should work - but it's purely based on what I understood, though. – Jai May 23 '18 at 01:17
  • @Jai Using a change listener: `pane.heightProperty().addListener((obs, oldH, newH) -> pane.setPrefWidth(newH.doubleValue()));` produces the same behavior... I think this is just about order of operations within the layout code. – James_D May 23 '18 at 01:27
  • @James_D I see.. Well, I was playing around with layout several months ago, similar to what the OP did. Some layout behaviors were pretty weird, like this problem that OP mentioned. – Jai May 23 '18 at 01:32
  • @Jai Sure; using bindings or listeners just isn't reliable for performing layout. You can't control whether or not the properties are updated in time for the current layout pass. Theoretically, you could presumably create stack overflow exceptions (via infinite recursion) using these techniques, though I suspect the library layout code guards against this possibility. – James_D May 23 '18 at 01:38

1 Answers1

3

The problem you are running into here is that when the container is laid out, it has no reasonable information as to the order in which it should compute the width and the height of the pane. So essentially what happens is it computes the width, which (since it's empty), is zero; then computes the height (which fills the container, since you told the VBox to do that). After that, the prefWidth property is changed, but by then the actual width has already been set, so it's essentially too late. The next time a layout pass occurs, the new pref width is taken into account.

I haven't checked the actual layout code, but (since the default content bias is null) most likely the layout code for the vbox is going to do something equivalent to the following pseudocode:

protected void layoutChildren() {

    // content bias is null:
    double prefWidth = pane.prefWidth(-1);
    double prefHeight = pane.prefHeight(-1);

    // no fill width:
    double paneWidth = Math.max(this.getWidth(), prefWidth);
    // vgrow, so ignore preferred height and size to height of the vbox:
    double paneHeight = this.getHeight();
    pane.resizeRelocate(0, 0, paneWidth, paneHeight);

}

The last call actually causes the height of the pane to change, which then causes the prefWidth to change via the binding. Of course, that's too late for the current layout pass, which has already set the width based on the previous preferred width calculation.

Basically, relying on bindings to manage layout like this is not a reliable way of doing things, because you are changing properties (such as prefWidth in this example) during the layout pass, when it may be already too late to resize the component.

The reliable way to manage layout for a pane like this is to override the appropriate layout methods, which are invoked by the layout pass in order to size the component.

For this example, since the width depends on the height, you should return VERTICAL for the contentBias, and you should override computePrefWidth(double height) to return the height (so the width is set to the height):

@Override
public void start(Stage primaryStage) {
    Pane pane = new Pane() {
        @Override
        public Orientation getContentBias() {
            return Orientation.VERTICAL ;
        }
        @Override
        public double computePrefWidth(double height) {
            return height ;
        }
    };
    pane.setStyle("-fx-border-color: green; -fx-border-width: 2");


    //Bind the pane's preferred width to the pane's height
    //    pane.prefWidthProperty().bind(pane.heightProperty());

    //Put the pane in a vbox that does not fill the stage's width and make the pane grow in the vbox
    VBox container = new VBox(pane);
    container.setFillWidth(false);
    VBox.setVgrow(pane, Priority.SOMETIMES);

    //Show the vbox
    primaryStage.setScene(new Scene(container, 600, 400));
    primaryStage.show();
}
James_D
  • 201,275
  • 16
  • 291
  • 322