I've been having issues with some Textflow
s overflowing from their defined cell in a GridPane
. My guess would be that no prefHeight
is defined but I'm not managing to find how to calculate this height in order to set or bind it.
My intent is to have the TextFlow
takes as much size as it needs to display the text fully, as the whole GridPane
is in a ScrollPane
.
Or is it another issue?
Here's a screenshot (added borders for debugging):
Here's the fxml definition:
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.String?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<?import javafx.scene.text.Text?>
<?import javafx.scene.text.TextFlow?>
<fx:root stylesheets="@../styles/main.css" type="BorderPane" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1">
<styleClass>
<String fx:value="binocles-pane" />
<String fx:value="book-pane" />
</styleClass>
<center>
<VBox fx:id="bookZoneVBox" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<Text fx:id="bookTitle" strokeType="OUTSIDE" strokeWidth="0.0" styleClass="main-title" text="Book title">
<font>
<Font size="14.0" />
</font>
</Text>
<ScrollPane fx:id="bookZoneScroll" fitToHeight="true" fitToWidth="true" focusTraversable="false" hbarPolicy="NEVER" minHeight="224.0" minWidth="300.0" prefHeight="233.0" prefWidth="310.0" styleClass="edge-to-edge" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<content>
<GridPane fx:id="bookGrid" minHeight="150.0" minWidth="300.0" prefHeight="215.0" prefWidth="300.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" minWidth="100.0" prefWidth="100.0" />
<ColumnConstraints hgrow="ALWAYS" minWidth="200.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" prefHeight="22.0" vgrow="ALWAYS" />
<RowConstraints minHeight="10.0" prefHeight="21.0" vgrow="ALWAYS" />
<RowConstraints minHeight="10.0" prefHeight="24.0" vgrow="NEVER" />
<RowConstraints minHeight="10.0" prefHeight="138.0" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<Label text="Synopsis" GridPane.valignment="TOP" />
<Label text="Description" GridPane.rowIndex="1" GridPane.valignment="TOP" />
<Label text="Metadata" GridPane.rowIndex="2" GridPane.valignment="BOTTOM" />
<TableView fx:id="bookMetadataTable" focusTraversable="false" maxHeight="150.0" maxWidth="300.0" prefHeight="150.0" prefWidth="300.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" GridPane.columnSpan="2147483647" GridPane.hgrow="ALWAYS" GridPane.rowIndex="3">
<columns>
<TableColumn editable="false" prefWidth="75.0" sortable="false" text="Field" />
<TableColumn editable="false" prefWidth="215.0" sortable="false" text="Value" />
</columns>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</TableView>
<TextFlow fx:id="bookSynopsisField" minWidth="200.0" textAlignment="JUSTIFY" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" />
<TextFlow fx:id="bookDescriptionField" minWidth="200.0" textAlignment="JUSTIFY" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" GridPane.rowIndex="1" />
</children>
</GridPane>
</content>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</ScrollPane>
</children>
</VBox>
</center>
</fx:root>
Here's the java code:
package com.github.sylordis.binocles.ui.views;
import java.io.IOException;
import java.net.URL;
import java.util.Map;
import java.util.ResourceBundle;
import com.github.sylordis.binocles.model.text.Book;
import com.github.sylordis.binocles.ui.javafxutils.FXFormatUtils;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
/**
* Pane component for a book.
*/
public class BookView extends BorderPane implements Initializable, BinoclesTabPane {
private Book book;
@FXML
private Text bookTitle;
@FXML
private VBox bookZoneVBox;
@FXML
private TextFlow bookSynopsisField;
@FXML
private TextFlow bookDescriptionField;
@FXML
private GridPane bookGrid;
@FXML
private ScrollPane bookZoneScroll;
@FXML
private TableView<Map.Entry<String, String>> bookMetadataTable;
private ObservableList<Map.Entry<String, String>> bookMetadataTableData;
public BookView(Book book) {
super();
this.book = book;
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("book_view.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
@Override
public void initialize(URL location, ResourceBundle resources) {
// Set content
bookTitle.setText(book.getTitle());
bookSynopsisField.getChildren().add(new Text(book.getSynopsis()));
bookDescriptionField.getChildren().add(new Text(book.getDescription()));
bookMetadataTable.setPlaceholder(new Label("No metadata provided"));
bookMetadataTable.setItems(FXCollections.observableArrayList(book.getMetadata().entrySet()));
bookMetadataTable.refresh();
// Format
FXFormatUtils.bindToParent(bookGrid);
bookSynopsisField.setMaxWidth(Double.MAX_VALUE);
bookSynopsisField.setMaxHeight(Double.MAX_VALUE);
bookDescriptionField.setMaxWidth(Double.MAX_VALUE);
bookDescriptionField.setMaxHeight(Double.MAX_VALUE);
bookGrid.maxWidthProperty().bind(bookZoneScroll.widthProperty().subtract(5));
bookGrid.prefWidthProperty().bind(bookZoneScroll.widthProperty().subtract(5));
bookGrid.getColumnConstraints().forEach(c -> c.setMaxWidth(Double.MAX_VALUE));
// Debug
bookSynopsisField.setStyle("-fx-border-color: #FF0000");
bookDescriptionField.setStyle("-fx-border-color: #00FF00");
}
@Override
public Object getItem() {
return book;
}
}
I'm using JavaFX 21 and Java 21.
Cheers
Some issues with your approach:
You have a TableView (which is scrollable) inside a ScrollPane (which is scrollable). That usually isn't a good design. It results in one scrollable thing inside another, making for a difficult-to-use interface for your users.
You are setting many more sizing constraints than needed, both in the FXML and code. Any constraints you add make the layout less flexible. So it is best to keep the number of constraints set to a minimum.
AnchorPane is often not a good choice for a resizable layout, it is primarily designed to facilitate absolute layout positioning.
Binding is a layout mechanism of last resort.
It is usually better to choose appropriate layout panes and constraints and let the layout panes manage the layout, instead of using binding to control the layout.
If no appropriate layout pane (or combination of panes) exists then you can create a custom layout pane design, but that is usually quite difficult and advanced, and, thankfully, usually not necessary.
Here is an example that addresses some of the issues with your implementation.
The outer ScrollPane is removed
Minimal constraint information is provided in the FXML and code.
Unnecessary container elements such as AnchorPanes are removed from the implementation.
No binding is used to control layout.
book.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.TextFlow?>
<VBox xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.test.demo.books.BookController">
<children>
<Label fx:id="bookTitle" text="Book title" />
<GridPane hgap="5.0" vgap="3.0">
<children>
<Label text="Synopsis" GridPane.valignment="TOP" />
<Label minHeight="-Infinity" text="Description" GridPane.rowIndex="1" GridPane.valignment="TOP" />
<TextFlow fx:id="bookSynopsisField" textAlignment="JUSTIFY" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" />
<TextFlow fx:id="bookDescriptionField" textAlignment="JUSTIFY" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" GridPane.rowIndex="1" />
</children>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
<columnConstraints>
<ColumnConstraints minWidth="-Infinity" />
<ColumnConstraints />
</columnConstraints>
<rowConstraints>
<RowConstraints />
<RowConstraints />
</rowConstraints>
</GridPane>
<VBox VBox.vgrow="ALWAYS">
<children>
<Label text="Metadata" />
<TableView fx:id="bookMetadataTable" prefHeight="150.0" VBox.vgrow="ALWAYS">
<columns>
<TableColumn fx:id="fieldColumn" editable="false" prefWidth="75.0" sortable="false" text="Field" />
<TableColumn fx:id="valueColumn" editable="false" prefWidth="215.0" sortable="false" text="Value" />
</columns>
</TableView>
</children>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</VBox>
</children>
</VBox>
BookApp.java
package com.test.demo.books;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.Objects;
public class BookApp extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader loader = new FXMLLoader(
Objects.requireNonNull(
BookApp.class.getResource(
"book.fxml"
)
)
);
Pane root = loader.load();
root.setPrefWidth(500);
stage.setScene(new Scene(root));
stage.show();
}
}
BookController.java
package com.test.demo.books;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.fxml.FXML;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.Label;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import java.util.Arrays;
public class BookController {
public static final String SYNOPSIS = "I've been having issues with some Textflows overflowing from their defined cell in a GridPane. My guess would be that no prefHeight is defined but I'm not managing to find how to calculate this height in order to set or bind it.";
private record Metadata(String field, String value) {}
@FXML
private TextFlow bookDescriptionField;
@FXML
private TableView<Metadata> bookMetadataTable;
@FXML
private TextFlow bookSynopsisField;
@FXML
private Label bookTitle;
@FXML
private TableColumn<Metadata, String> fieldColumn;
@FXML
private TableColumn<Metadata, String> valueColumn;
@FXML
private void initialize() {
bookTitle.setText("Textflow overflowing and overlapping from GridPane cell");
bookDescriptionField.getChildren().add(new Text("My intent is to have the TextFlow takes as much size as it needs to display the text fully, as the whole GridPane is in a ScrollPane."));
bookSynopsisField.getChildren().add(new Text(SYNOPSIS));
fieldColumn.setCellValueFactory(p -> new ReadOnlyStringWrapper(p.getValue().field));
valueColumn.setCellValueFactory(p -> new ReadOnlyStringWrapper(p.getValue().value));
bookMetadataTable.getItems().setAll(
Arrays.stream(SYNOPSIS.split(" ")).map(s -> new Metadata(s, s)).toList()
);
}
}
If you wanted to keep the outer ScrollPane, then I would recommend designing a new component based on GridPane for displaying your book metadata in a tabular format, rather than using a TableView. That is outside the scope of what I would be prepared to do here in this answer, but if you have questions about that, then ask about it specifically in a new question. If you do so, for context, you can put a link to this question in your new question.