5

I have two TableView in a VBox. I need the first one to grow and take all available space, while the second one should not expand. This is my current code:

public class TestFx extends Application { @Override public void start(Stage primaryStage) { ObservableList<TestItem> data1 = FXCollections.observableArrayList(); ObservableList<TestItem> data2 = FXCollections.observableArrayList(); for (int i = 1; i <= 30; i++) { data1.add(new TestItem("Item " + i, "Value " + i)); } for (int i = 1; i <= 5; i++) { data2.add(new TestItem("Item " + i, "Value " + i)); } TableView<TestItem> table1 = createTable("Type Table (30 rows)", data1); TableView<TestItem> table2 = createTable("Base Table (5 rows)", data2); VBox container = new VBox(); VBox.setVgrow(table1, Priority.ALWAYS); table1.setMaxHeight(Double.MAX_VALUE); VBox.setVgrow(table2, Priority.NEVER); table2.setMaxHeight(Region.USE_PREF_SIZE); container.getChildren().addAll(table1, table2); BorderPane root = new BorderPane(); root.setCenter(container); Scene scene = new Scene(root, 600, 400); primaryStage.setTitle("TableView Test"); primaryStage.setScene(scene); primaryStage.show(); } private TableView<TestItem> createTable(String title, ObservableList<TestItem> data) { TableView<TestItem> table = new TableView<>(); table.setItems(data); TableColumn<TestItem, String> col1 = new TableColumn<>(title); col1.setCellValueFactory(cellData -> cellData.getValue().nameProperty()); TableColumn<TestItem, String> col2 = new TableColumn<>("Value"); col2.setCellValueFactory(cellData -> cellData.getValue().valueProperty()); table.getColumns().addAll(col1, col2); table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); return table; } public static class TestItem { private final SimpleStringProperty name; private final SimpleStringProperty value; public TestItem(String name, String value) { this.name = new SimpleStringProperty(name); this.value = new SimpleStringProperty(value); } public String getName() { return name.get(); } public void setName(String name) { this.name.set(name); } public SimpleStringProperty nameProperty() { return name; } public String getValue() { return value.get(); } public void setValue(String value) { this.value.set(value); } public SimpleStringProperty valueProperty() { return value; } } public static void main(String[] args) { launch(args); } } 

And this is the result:

enter image description here

As you can see, the second table shows empty rows, so it takes more space than required. How to fix it?

6
  • 1
    The preferred height of a table view does not take into account the number of non-empty rows, so this is going to be difficult to do. (In fact, in the current implementation, the preferred height of a table is hard-coded to 400 pixels). You would probably need to either write a custom skin for the table that performed a more sophisticated calculation of the preferred height, or use a custom layout that inspected the tables and calculated the size for them there. Either way is quite a bit of a project. Commented Nov 10 at 15:39
  • I suggest putting both TableViews in a vertical SplitPane. Users benefit from being able to resize scrollable controls. And this will make it easier to control which one changes size when the container is resized, using the setResizableWithParent method. Commented Nov 10 at 16:30
  • @VGR The problem is that the height of the second table is not computed on the base of item count. Commented Nov 10 at 16:38
  • 1
    @VGR How well that would work depends quite a lot on the OP's actual use case. For example, if they need selection, that is quite a lot of functionality to have to reimplement. Commented Nov 10 at 19:19
  • 1
    This might be a duplicate. stackoverflow.com/questions/26298337/… -> stackoverflow.com/a/26367040/2423906 Commented Nov 13 at 15:36

1 Answer 1

4

There is no particularly easy way to do this. The (current) implementation of TableViewSkin just hard-codes the preferred height to 400. Consequently, the approach of limiting the max height of both tables to the preferred height just prevents both tables from growing more than 400 pixels tall. The VBox, even with vGrow set to PRIORITY.NEVER will still try to size the second table to its preferred size, so it will be 400 pixels tall, no matter how many items it contains.

The API-supported way to do this would be to create a skin subclass for the table which computed its size based on the number of items. Here is a very basic implementation which is intended to serve only as a proof of concept: it is not intended to be production quality (or anything close to it).

The basic idea here is to keep the VirtualFlow's cellCount set to the required value (the number of items in the table; it's also probably a good idea to impose a maximum which I just hard-code as 10 here), then compute the preferred height of the table by adding up the padding, preferred height of the header, and the preferred height of the VirtualFlow. For some reason, the scrolling ends up being off by one, so I have included a workaround for that in the layoutChildren() method to fix the scroll position. I don't know why this is necessary.

public static class ResizingTableSkin<T> extends TableViewSkin<T> { private static final int MAX_MEASURED_ROWS = 10; public ResizingTableSkin(TableView<T> tableView) { super(tableView); tableView.itemsProperty().addListener((Observable _) -> getVirtualFlow().setCellCount(Math.min(tableView.getItems().size(), MAX_MEASURED_ROWS))); getVirtualFlow().setCellCount(Math.min(tableView.getItems().size(), MAX_MEASURED_ROWS)); } @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { VirtualFlow<TableRow<T>> virtualFlow = getVirtualFlow(); double padding = topInset + bottomInset; double availableWidth = width - leftInset - rightInset; TableHeaderRow header = getTableHeaderRow(); return padding + header.prefHeight(availableWidth) + virtualFlow.prefHeight(availableWidth); } @Override protected void layoutChildren(double x, double y, double w, double h) { if (getSkinnable().getItems().size() <= MAX_MEASURED_ROWS) { getVirtualFlow().scrollTo(0); } super.layoutChildren(x, y, w, h); } } 

To use this, you should set the minimum height of the table to its preferred height. This will ensure all items (up to 10) are visible. Using a layout that doesn't allow it to grow beyond its preferred size (such as the VBox you have configured in the original post) will keep it at this preferred size.

The skin classes in JavaFX were originally not part of the public API. Since JavaFX 9 they were promoted to the public API, but the design of these classes is still very much as it was originally, and they are not designed for easy extension. To make this work correctly and robustly you would really need to read through a lot of the skin source code to understand how it works, and it would be a bit of a project. How much you want to invest in that depends on how important this layout is to you.

Here is a complete example, which works on my system. You can use this as a starting point for a more robust solution if you want to go that direction.

import javafx.application.Application; import javafx.beans.Observable; import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.scene.Scene; import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; import javafx.scene.control.TableView; import javafx.scene.control.skin.TableHeaderRow; import javafx.scene.control.skin.TableViewSkin; import javafx.scene.control.skin.VirtualFlow; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.stage.Stage; public class TestFx extends Application { @Override public void start(Stage primaryStage) { ObservableList<TestItem> data1 = FXCollections.observableArrayList(); ObservableList<TestItem> data2 = FXCollections.observableArrayList(); for (int i = 1; i <= 30; i++) { data1.add(new TestItem("Item " + i, "Value " + i)); } for (int i = 1; i <= 15; i++) { data2.add(new TestItem("Item " + i, "Value " + i)); } TableView<TestItem> table1 = createTable("Type Table (30 rows)", data1); TableView<TestItem> table2 = createTable("Base Table (5 rows)", data2); table2.setSkin(new ResizingTableSkin<>(table2)); VBox container = new VBox(); VBox.setVgrow(table1, Priority.ALWAYS); table1.setMaxHeight(Double.MAX_VALUE); VBox.setVgrow(table2, Priority.NEVER); table2.setMinHeight(Region.USE_PREF_SIZE); container.getChildren().addAll(table1, table2); BorderPane root = new BorderPane(); root.setCenter(container); Scene scene = new Scene(root, 600, 400); primaryStage.setTitle("TableView Test"); primaryStage.setScene(scene); primaryStage.show(); } private TableView<TestItem> createTable(String title, ObservableList<TestItem> data) { TableView<TestItem> table = new TableView<>(); table.setItems(data); TableColumn<TestItem, String> col1 = new TableColumn<>(title); col1.setCellValueFactory(cellData -> cellData.getValue().nameProperty()); TableColumn<TestItem, String> col2 = new TableColumn<>("Value"); col2.setCellValueFactory(cellData -> cellData.getValue().valueProperty()); table.getColumns().addAll(col1, col2); table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); return table; } public static class ResizingTableSkin<T> extends TableViewSkin<T> { private static final int MAX_MEASURED_ROWS = 10; public ResizingTableSkin(TableView<T> tableView) { super(tableView); tableView.itemsProperty().addListener((Observable _) -> getVirtualFlow().setCellCount(Math.min(tableView.getItems().size(), MAX_MEASURED_ROWS))); getVirtualFlow().setCellCount(Math.min(tableView.getItems().size(), MAX_MEASURED_ROWS)); } @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { VirtualFlow<TableRow<T>> virtualFlow = getVirtualFlow(); double padding = topInset + bottomInset; double availableWidth = width - leftInset - rightInset; TableHeaderRow header = getTableHeaderRow(); return padding + header.prefHeight(availableWidth) + virtualFlow.prefHeight(availableWidth); } @Override protected void layoutChildren(double x, double y, double w, double h) { if (getSkinnable().getItems().size() <= MAX_MEASURED_ROWS) { getVirtualFlow().scrollTo(0); } super.layoutChildren(x, y, w, h); } } public static class TestItem { private final SimpleStringProperty name; private final SimpleStringProperty value; public TestItem(String name, String value) { this.name = new SimpleStringProperty(name); this.value = new SimpleStringProperty(value); } public String getName() { return name.get(); } public void setName(String name) { this.name.set(name); } public SimpleStringProperty nameProperty() { return name; } public String getValue() { return value.get(); } public void setValue(String value) { this.value.set(value); } public SimpleStringProperty valueProperty() { return value; } } public static void main(String[] args) { launch(args); } } 
Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.