Search code examples
javajavafxlayoutlabelgridpane

GridPane not giving label enough space when using columns with percentage widths


The code below generates this screen shot:

Label not fitting

The label below the 2nd column does not want to properly claim extra space when it needs to be wrapped. This only occurs when the columns use percentage widths -- the docs say it will ignore all other properties in that case, including hgrow etc, but it does not mention it would also affect how rows work.. but it looks like it does.

Anyone got a work-around or can tell me what I'm doing wrong? All I want is to display 3 images, with labels of unknown size below them that are nicely spaced and aligned and all the same size... Something like this:

Properly reflowed labels

The above is done with a custom control, tentatively named "BetterGridPane"...

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.RowConstraints;
import javafx.stage.Stage;

public class TestLabelWrap extends Application {

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

  @Override
  public void start(Stage primaryStage) throws Exception {
    GridPane root = new GridPane();

    root.add(new ListView(FXCollections.observableArrayList("1", "2", "3")), 0, 0);
    root.add(new ListView(FXCollections.observableArrayList("1", "2", "3")), 1, 0);
    root.add(new ListView(FXCollections.observableArrayList("1", "2", "3")), 2, 0);

    root.add(new Label("Value A"), 0, 1);
    root.add(new Label("The value for this porperty is so large it wraps, The value for this porperty is so large it wraps") {{
      setWrapText(true);
    }}, 1, 1);
    root.add(new Label("Value C"), 2, 1);

    root.getColumnConstraints().add(new ColumnConstraints() {{ setPercentWidth(33.33); }});
    root.getColumnConstraints().add(new ColumnConstraints() {{ setPercentWidth(33.33); }});
    root.getColumnConstraints().add(new ColumnConstraints() {{ setPercentWidth(33.33); }});

    root.getRowConstraints().add(new RowConstraints() {{ setVgrow(Priority.NEVER); }});
    root.getRowConstraints().add(new RowConstraints() {{ setVgrow(Priority.ALWAYS); }});

    primaryStage.setScene(new Scene(root));
    primaryStage.show();
  }
}

Solution

  • Setting wrapText to true actually seems to be enough. You can see this by simply increasing the height of your window and watch the text start to wrap (decreasing the width of the window as necessary). The problem seems to be GridPane and it's initial size calculations—it grows horizontally to accommodate the Label, but not vertically. One fix is to simply give the Scene, GridPane, or Label an explicit (preferred) width while leaving the height to be computed.

    • new Scene(root, someWidth, -1)
    • root.setPrefWidth(someWidth)
    • label.setPrefWidth(someWidth) + label.setMaxWidth(Double.MAX_VALUE)

    However, even though the Javadoc of ColumnConstraints.percentWidth says:

    If set to a value greater than 0, the column will be resized to this percentage of the gridpane's available width and the other size constraints (minWidth, prefWidth, maxWidth, hgrow) will be ignored.

    The [min|pref|max]Width does not seemed to be ignored during preferred size calculations. As you appear to have images of known and uniform width, you should be able to set the preferred width of each constraint to be slightly larger than the width of the image. Here's an example (using OpenJDK 12 and OpenJFX 12):

    import java.util.stream.Stream;
    import javafx.application.Application;
    import javafx.geometry.HPos;
    import javafx.geometry.Insets;
    import javafx.geometry.Pos;
    import javafx.scene.Scene;
    import javafx.scene.control.Label;
    import javafx.scene.layout.ColumnConstraints;
    import javafx.scene.layout.GridPane;
    import javafx.scene.shape.Rectangle;
    import javafx.scene.text.TextAlignment;
    import javafx.stage.Stage;
    
    public class Main extends Application {
    
        private static ColumnConstraints[] createColumnConstraints() {
            return Stream.generate(() -> {
                var constraint = new ColumnConstraints();
                constraint.setHalignment(HPos.CENTER);
                constraint.setPercentWidth(100.0 / 3.0);
                constraint.setPrefWidth(225.0); // SET PREF WIDTH
                return constraint;
            }).limit(3).toArray(ColumnConstraints[]::new);
        }
    
        private static Rectangle[] createRectangles() {
            return Stream.generate(() -> new Rectangle(100.0, 150.0)).limit(3).toArray(Rectangle[]::new);
        }
    
        private static Label[] createLabels(String... texts) {
            return Stream.of(texts).map(text -> {
                var label = new Label(text);
                label.setWrapText(true);
                label.setTextAlignment(TextAlignment.CENTER);
                return label;
            }).toArray(Label[]::new);
        }
    
        @Override
        public void start(Stage primaryStage) {
            var root = new GridPane();
            root.setGridLinesVisible(true); // FOR VISUAL CLARITY
            root.setHgap(10.0);
            root.setVgap(10.0);
            root.setAlignment(Pos.CENTER);
            root.setPadding(new Insets(20.0));
            root.getColumnConstraints().addAll(createColumnConstraints());
    
            root.addRow(0, createRectangles());
            root.addRow(1, createLabels("Label A", "This is long text. ".repeat(10), "Label C"));
    
            primaryStage.setScene(new Scene(root));
            primaryStage.show();
        }
    
    }
    

    Which results in:

    screenshot of UI created by example code


    Speculation:

    Since you use new Scene(root) that means the Scene will size itself to the preferred size of its content. This basically gives the root free reign to grow as large as it deems necessary. As documented by GridPane:

    By default, rows and columns will be sized to fit their content; a column will be wide enough to accommodate the widest child, a row tall enough to fit the tallest child.

    With the use of percentWidth, the width of each column effectively doesn't have an upper bound; they will grow to to fit the widest child. Also, because each column uses percentWidth, as one grows, they all grow.

    This means the Labels have virtually unlimited space to grow horizontally, which means the preferred height of the Label is calculated to be only one line of text; why wrap when you have infinite width? Because of this, the row the Labels belong to only grows vertically enough to accommodate one line of text. With no room to wrap the text overruns.