Search code examples
javajavafxproperty-bindinghbox

How do I properly set bindings in JavaFX so that two rectangles retain their relative width and space between each other when the window is resized?


I'm trying to create a bar chart as seen in exercise 14.12 in Introduction to Java Programming and Data Structures. I am so lost and incompetent when it comes to using JavaFX, that I currently I'm just trying to create two rectangles, that when you resize the window, their relative widths and distance from each other stay the same.

I tried this:

 HBox hBox = new HBox();
      
 Group group = new Group();
      
      
 Rectangle rectangle1 = new Rectangle();
 rectangle1.xProperty().bind(hBox.widthProperty().multiply((double)10/100));
 rectangle1.setY(100);
 rectangle1.widthProperty().bind(hBox.widthProperty().multiply((double)20/100));
 rectangle1.setHeight(400);
      
 Rectangle rectangle2 = new Rectangle();
 rectangle2.xProperty().bind(hBox.widthProperty().multiply((double)30/100));
 rectangle2.setY(100);
 rectangle2.widthProperty().bind(hBox.widthProperty().multiply((double)40/100));
 rectangle2.setHeight(400);

 group.getChildren().addAll(rectangle1, rectangle2);
            
 hBox.setPadding(new Insets(20,20,20,20));
 hBox.getChildren().add(group);
      
 Scene scene = new Scene(hBox);
      
 primaryStage.setScene(scene);
 primaryStage.setTitle("Bar Chart");
 primaryStage.show();

I would have expected that with the widthProperty of rectangle1 set to 20% of the HBox size, and the xProperty of rectangle2 set to 30% of the HBox size, that there would be a noticeable distance between the rectangles, but they are basically merged together.


Solution

  • Your rectangles are behaving more or less as expected. The x property of the first rectangle is set to 10% of the width of the HBox and its width is set to 20% of the width of the HBox, so it extends to 30% of the width of the HBox. The x property of the second rectangle is set to 30% of the width of the HBox, so it starts where the first rectangle ends.

    Binding properties like this, however, is not the right way to lay things out in JavaFX. The HBox will try to resize if its child nodes resize, so that it can comfortably hold its children. If the size and position of the child nodes are bound to the size of the HBox, then they will resize if the HBox resizes. The library code is written to prevent any infinite recursion, but under some conditions it could be impossible to both satisfy the implicit dependency of the size of the container (the HBox) on the size of its children and the explicit dependency of the size of the children on the size of the container that the bindings create. You may notice at startup things don't quite look right (there is no gap to the right of the rectangles).

    A better approach is to subclass Region and override the layoutChildren() method. You can then query the width and height of the region and set the size and position of the rectangles accordingly.

    If you want to allow for padding in the region, use snappedXXXInset() (where XXX is top/right/bottom/left). For simple use cases you could omit this.

    Here's a demo:

    import javafx.application.Application;
    import javafx.geometry.Insets;
    import javafx.scene.Scene;
    import javafx.scene.layout.HBox;
    import javafx.scene.layout.Priority;
    import javafx.scene.layout.Region;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Rectangle;
    import javafx.stage.Stage;
    
    public class BarChartTest extends Application {
        @Override
        public void start(Stage primaryStage) throws Exception {
            HBox hbox = new HBox();
    
            Rectangle rectangle1 = new Rectangle();
    
            Rectangle rectangle2 = new Rectangle();
            rectangle2.setFill(Color.SKYBLUE);
    
    
            Region region = new Region() {
    
                // in reality these can be properties
                private final double prefBarWidth = 40;
                private final double prefBarHeight = 400;
                private final double verticalBarPadding = 100;
                {
                    getChildren().addAll(rectangle1, rectangle2);
                }
    
                @Override
                protected void layoutChildren() {
                    double x = snappedLeftInset();
                    double w = getWidth() - x - snappedRightInset();
                    rectangle1.setX(snapPositionX(x + w * 0.1));
                    rectangle1.setWidth(snapPositionX(w * 0.2));
                    rectangle2.setX(snapPositionX(x + w * 0.4));
                    rectangle2.setWidth(snapPositionX(w * 0.2));
    
                    double y = snappedTopInset();
                    double h = getHeight() - y - snappedBottomInset();
                    rectangle1.setY(snapPositionY(y + verticalBarPadding));
                    rectangle1.setHeight(snapPositionY(h - 2 * verticalBarPadding));
                    rectangle2.setY(snapPositionY(y + verticalBarPadding));
                    rectangle2.setHeight(snapPositionY(h - 2 * verticalBarPadding));
                }
    
            };
    
            hbox.setPadding(new Insets(20,20,20,20));
            hbox.getChildren().add(region);
            // For demo, make region grow when window size increases:
            HBox.setHgrow(region, Priority.ALWAYS);
    
            Scene scene = new Scene(hbox, 440, 640);
    
            primaryStage.setScene(scene);
            primaryStage.setTitle("Bar Chart");
            primaryStage.show();
        }
    }
    

    Note I explicitly set the size of the scene here: that's because the region doesn't know how to calculate its preferred size (so by default it is zero). A slightly better solution would include that calculation:

    import javafx.application.Application;
    import javafx.geometry.Insets;
    import javafx.scene.Scene;
    import javafx.scene.layout.HBox;
    import javafx.scene.layout.Priority;
    import javafx.scene.layout.Region;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Rectangle;
    import javafx.stage.Stage;
    
    public class BarChartTest extends Application {
        @Override
        public void start(Stage primaryStage) throws Exception {
            HBox hbox = new HBox();
    
            Rectangle rectangle1 = new Rectangle();
    
            Rectangle rectangle2 = new Rectangle();
            rectangle2.setFill(Color.SKYBLUE);
    
    
            Region region = new Region() {
    
                // in reality these can be properties
                private final double prefBarWidth = 40;
                private final double prefBarHeight = 400;
                private final double verticalBarPadding = 100;
                {
                    getChildren().addAll(rectangle1, rectangle2);
                }
    
                @Override
                protected void layoutChildren() {
                    double x = snappedLeftInset();
                    double w = getWidth() - x - snappedRightInset();
                    rectangle1.setX(snapPositionX(x + w * 0.1));
                    rectangle1.setWidth(snapPositionX(w * 0.2));
                    rectangle2.setX(snapPositionX(x + w * 0.4));
                    rectangle2.setWidth(snapPositionX(w * 0.2));
    
                    double y = snappedTopInset();
                    double h = getHeight() - y - snappedBottomInset();
                    rectangle1.setY(snapPositionY(y + verticalBarPadding));
                    rectangle1.setHeight(snapPositionY(h - 2 * verticalBarPadding));
                    rectangle2.setY(snapPositionY(y + verticalBarPadding));
                    rectangle2.setHeight(snapPositionY(h - 2 * verticalBarPadding));
                }
    
                @Override
                public double computePrefHeight(double width) {
                    return 2*verticalBarPadding + prefBarHeight + snappedTopInset() + snappedBottomInset();
                }
    
                @Override
                public double computePrefWidth(double height) {
                    return prefBarWidth*5 + snappedLeftInset()+snappedRightInset();
                }
    
            };
    
            hbox.setPadding(new Insets(20,20,20,20));
            hbox.getChildren().add(region);
            // For demo, make region grow when window size increases:
            HBox.setHgrow(region, Priority.ALWAYS);
    
            Scene scene = new Scene(hbox);
    
            primaryStage.setScene(scene);
            primaryStage.setTitle("Bar Chart");
            primaryStage.show();
        }
    }