This solution only works in Java 9 and later.
The display of a row is controlled using TableRow , and the actual layout of this row is performed using its skin (a TableRowSkin ). Therefore, to manage this, you need a subclass of TableRow that sets a custom skin.
The implementation of the strings is quite simple: in this example, I added a property for the "additional content" that will be displayed when a row is selected. It also overrides the createDefaultSkin() method to specify a custom skin implementation.
import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.scene.Node; import javafx.scene.control.Skin; import javafx.scene.control.TableRow; public class ExpandingTableRow<T> extends TableRow<T> { private final ObjectProperty<Node> selectedRowContent = new SimpleObjectProperty<>(); public final ObjectProperty<Node> selectedRowContentProperty() { return this.selectedRowContent; } public final Node getSelectedRowContent() { return this.selectedRowContentProperty().get(); } public final void setSelectedRowContent(final Node selectedRowContent) { this.selectedRowContentProperty().set(selectedRowContent); } public ExpandingTableRow(Node selectedRowContent) { super(); setSelectedRowContent(selectedRowContent); } public ExpandingTableRow() { this(null); } @Override protected Skin<?> createDefaultSkin() { return new ExpandingTableRowSkin<T>(this); } }
The implementation of the skin should do the work with the layout. He must override the methods that calculate the height, taking into account the height of the additional content, if necessary, and he needs to redefine the layoutChildren() method to place additional content, if necessary. Finally, it must manage additional content, add or remove additional content if the selected state of the line changes (or if the additional content itself changes).
import javafx.scene.control.skin.TableRowSkin; public class ExpandingTableRowSkin<T> extends TableRowSkin<T> { private ExpandingTableRow<T> row; public ExpandingTableRowSkin(ExpandingTableRow<T> row) { super(row); this.row = row; row.selectedRowContentProperty().addListener((obs, oldContent, newContent) -> { if (oldContent != null) { getChildren().remove(oldContent); } if (newContent != null && row.isSelected()) { getChildren().add(newContent); } if (row.getTableView() != null) { row.getTableView().requestLayout(); } }); row.selectedProperty().addListener((obs, wasSelected, isNowSelected) -> { if (isNowSelected && row.getSelectedRowContent() != null && !getChildren().contains(row.getSelectedRowContent())) { getChildren().add(row.getSelectedRowContent()); } else { getChildren().remove(row.getSelectedRowContent()); } }); } @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { if (row.isSelected() && row.getSelectedRowContent() != null) { return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset) + row.getSelectedRowContent().maxHeight(width); } return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset); } @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { if (row.isSelected() && row.getSelectedRowContent() != null) { return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset) + row.getSelectedRowContent().minHeight(width); } return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset); } @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { if (row.isSelected() && row.getSelectedRowContent() != null) { return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset) + row.getSelectedRowContent().prefHeight(width); } return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset); } @Override protected void layoutChildren(double x, double y, double w, double h) { if (row.isSelected()) { double rowHeight = super.computePrefHeight(w, snappedTopInset(), snappedRightInset(), snappedBottomInset(), snappedLeftInset()); super.layoutChildren(x, y, w, rowHeight); row.getSelectedRowContent().resizeRelocate(x, y + rowHeight, w, h - rowHeight); } else { super.layoutChildren(x, y, w, h); } } }
Finally, a test (using a regular example from Oracle or its version):
import java.util.function.Function; import javafx.application.Application; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ObservableValue; import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; import javafx.scene.control.TableView; import javafx.scene.layout.FlowPane; import javafx.stage.Stage; public class ExpandingTableRowTest extends Application { @Override public void start(Stage primaryStage) { TableView<Person> table = new TableView<>(); table.getColumns().add(column("First Name", Person::firstNameProperty)); table.getColumns().add(column("Last Name", Person::lastNameProperty)); table.setRowFactory(tv -> { Label label = new Label(); FlowPane flowPane = new FlowPane(label); TableRow<Person> row = new ExpandingTableRow<>(flowPane) { @Override protected void updateItem(Person person, boolean empty) { super.updateItem(person, empty); if (empty) { label.setText(null); } else { label.setText(String.format("Some additional information about %s %s here", person.getFirstName(), person.getLastName())); } } }; return row; }); table.getItems().addAll( new Person("Jacob", "Smith"), new Person("Isabella", "Johnson"), new Person("Ethan", "Williams"), new Person("Emma", "Jones"), new Person("Michael", "Brown") ); Scene scene = new Scene(table); primaryStage.setScene(scene); primaryStage.show(); } private static <S, T> TableColumn<S, T> column(String title, Function<S, ObservableValue<T>> property) { TableColumn<S, T> col = new TableColumn<>(title); col.setCellValueFactory(cellData -> property.apply(cellData.getValue())); return col; } public static class Person { private final StringProperty firstName = new SimpleStringProperty(); private final StringProperty lastName = new SimpleStringProperty(); public Person(String firstName, String lastName) { setFirstName(firstName); setLastName(lastName); } public final StringProperty firstNameProperty() { return this.firstName; } public final String getFirstName() { return this.firstNameProperty().get(); } public final void setFirstName(final String firstName) { this.firstNameProperty().set(firstName); } public final StringProperty lastNameProperty() { return this.lastName; } public final String getLastName() { return this.lastNameProperty().get(); } public final void setLastName(final String lastName) { this.lastNameProperty().set(lastName); } } public static void main(String[] args) { launch(args); } }
As you can see, it may take a little refinement of style and size to prepare this finished product, but it shows an approach that will work.
