TableView: adjust number of visible rows

Changing tableview's height and removing "empty" rows are two different things. Be specific.

For removing rows see this tutorial.

For changing height, first set the fixedCellSizeProperty of the table view then use it in binding:

table.setFixedCellSize(25);
table.prefHeightProperty().bind(Bindings.size(table.getItems()).multiply(table.getFixedCellSize()).add(30));

Adding 30px is for tableview's header.


Unfortunately, configuration of the visibleRowCount isn't supported in TableView (you might consider filing a feature request in fx' jira - no need, already done years ago). And it's not entirely straightforward to let the view return a prefHeight based on a such a preference: we'll need to measure the size requirements of the "real" cell and that's somehow buried inside the bowels.

Just for fun, experimented with extending the whole stack of collaborators:

  • a custom tableView that has a visibleRowCount property
  • a custom skin that listens to the property calculates its prefHeight depending on it
  • some way to access the height of the "real" cell - the only class with all info to measure it, is the VirtualFlow. As the relevant method is protected, this requires either a custom VirtualFlow that exposes that method or reflective access.

The code:

/**
 * TableView with visibleRowCountProperty.
 * 
 * @author Jeanette Winzenburg, Berlin
 */
public class TableViewWithVisibleRowCount<T> extends TableView<T> {

    private IntegerProperty visibleRowCount = new SimpleIntegerProperty(this, "visibleRowCount", 10);
    
    
    public IntegerProperty visibleRowCountProperty() {
        return visibleRowCount;
    }
    
    @Override
    protected Skin<?> createDefaultSkin() {
        return new TableViewSkinX<T>(this);
    }
    
    /**
     * Skin that respects table's visibleRowCount property.
     */
    public static class TableViewSkinX<T> extends TableViewSkin<T> {

        public TableViewSkinX(TableViewWithVisibleRowCount<T> tableView) {
            super(tableView);
            registerChangeListener(tableView.visibleRowCountProperty(), "VISIBLE_ROW_COUNT");
            handleControlPropertyChanged("VISIBLE_ROW_COUNT");
        }
        
        @Override
        protected void handleControlPropertyChanged(String p) {
            super.handleControlPropertyChanged(p);
            if ("VISIBLE_ROW_COUNT".equals(p)) {
                needCellsReconfigured = true;
                getSkinnable().requestFocus();
            }
        }

        /**
         * Returns the visibleRowCount value of the table.
         */
        private int getVisibleRowCount() {
            return ((TableViewWithVisibleRowCount<T>) getSkinnable()).visibleRowCountProperty().get();
        }
        
        /**
         * Calculates and returns the pref height of the 
         * for the given number of rows.
         * 
         * If flow is of type MyFlow, queries the flow directly
         * otherwise invokes the method.
         */
        protected double getFlowPrefHeight(int rows) {
            double height = 0;
            if (flow instanceof MyFlow) {
                height = ((MyFlow) flow).getPrefLength(rows);
            }
            else {
                for (int i = 0; i < rows && i < getItemCount(); i++) {
                    height += invokeFlowCellLength(i);
                }
            }    
            return height + snappedTopInset() + snappedBottomInset();

        }
        
        /**
         * Overridden to compute the sum of the flow height and header prefHeight.
         */
        @Override
        protected double computePrefHeight(double width, double topInset,
                double rightInset, double bottomInset, double leftInset) {
            // super hard-codes to 400 .. doooh
            double prefHeight = getFlowPrefHeight(getVisibleRowCount());
            return prefHeight + getTableHeaderRow().prefHeight(width);
        }
        
        /**
         * Reflectively invokes protected getCellLength(i) of flow.
         * @param index the index of the cell.
         * @return the cell height of the cell at index.
         */
        protected double invokeFlowCellLength(int index) {
            double height = 1.0;
            Class<?> clazz = VirtualFlow.class;
            try {
                Method method = clazz.getDeclaredMethod("getCellLength", Integer.TYPE);
                method.setAccessible(true);
                return ((double) method.invoke(flow, index));
            } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                e.printStackTrace();
            }
            return height;
        }

        /**
         * Overridden to return custom flow.
         */
        @Override
        protected VirtualFlow createVirtualFlow() {
            return new MyFlow();
        }
        
        /**
         * Extended to expose length calculation per a given # of rows.
         */
        public static class MyFlow extends VirtualFlow {

            protected double getPrefLength(int rowsPerPage) {
                double sum = 0.0;
                int rows = rowsPerPage; //Math.min(rowsPerPage, getCellCount());
                for (int i = 0; i < rows; i++) {
                    sum += getCellLength(i);
                }
                return sum;
            }

        }
        
    }

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger.getLogger(TableViewWithVisibleRowCount.class
            .getName());
}

Note that you might get away with a plain override of table's prefHeight when having a fixed-cell-size, didn't try that - no risk no fun :-)


Update: the custom skin in javafx15 - basically the same, just some access details changed (in both directions ;)

/**
 * Skin that respects table's visibleRowCount property.
 */
public class TableViewSkinX<T> extends TableViewSkin<T> {

    public TableViewSkinX(TableViewWithVisibleRowCount<T> tableView) {
        super(tableView);
        registerChangeListener(tableView.visibleRowCountProperty(), e -> visibleRowCountChanged());
    }
    
    private void visibleRowCountChanged() {
        getSkinnable().requestLayout();
    }
    
    /**
     * Returns the visibleRowCount value of the table.
     */
    private int getVisibleRowCount() {
        return ((TableViewWithVisibleRowCount<T>) getSkinnable()).visibleRowCountProperty().get();
    }
    
    /**
     * Calculates and returns the pref height of the for the given number of
     * rows.
     */
    protected double getFlowPrefHeight(int rows) {
        double height = 0;
        for (int i = 0; i < rows && i < getItemCount(); i++) {
            height += invokeFlowCellLength(i);
        }
        return height + snappedTopInset() + snappedBottomInset();
    }
    
    /**
     * Overridden to compute the sum of the flow height and header prefHeight.
     */
    @Override
    protected double computePrefHeight(double width, double topInset,
            double rightInset, double bottomInset, double leftInset) {
        // super hard-codes to 400 .. doooh
        double prefHeight = getFlowPrefHeight(getVisibleRowCount());
        return prefHeight + getTableHeaderRow().prefHeight(width);
    }
    
    /**
     * Reflectively invokes protected getCellLength(i) of flow.
     * @param index the index of the cell.
     * @return the cell height of the cell at index.
     */
    protected double invokeFlowCellLength(int index) {
        // note: use your own utility method to reflectively access internal fields/methods
        return (double) FXUtils.invokeGetMethodValue(VirtualFlow.class, getVirtualFlow(), 
                "getCellLength", Integer.TYPE, index);
    }

}

Just change background color of empty rows using css

.table-row-cell:empty {
-fx-background-color: white;
-fx-border-color: white;
} 

and modify number of rows on the basis of combobox.


Here's my solution, in order not to be deeply dependant on table.getFixedCellSize (we yet depend on it during FX initialization, while CSS is not yet computed/applied).

Note that we also need to add some pixels (don't understand why).

public static <S> void ensureDisplayingRows(@NotNull TableView<S> table, @Null Integer rowCount) {
    DoubleProperty headerRowHeightProperty = new SimpleDoubleProperty();
    table.skinProperty().addListener((observable, oldValue, newValue) -> {
        if (!Objects.equals(oldValue, newValue)) {
            TableHeaderRow headerRow = headerRow(table);
            // TableHeaderRow not defined until CSS is applied.
            if (headerRow == null) {
                assert table.getFixedCellSize() > 0.0 : "TableView '" + table.getId() + "' is not 'fixedCellSize'."; // TODO Find a better way to control.
                headerRowHeightProperty.setValue(table.getFixedCellSize()); // Approximation. // TODO Find a better approximation.
            } else {
                headerRowHeightProperty.bind(headerRow.heightProperty());
            }
        }
    });

    IntegerBinding itemsCountBinding = Bindings.size(table.getItems()); // NB: table.getItems() may not (yet) contains all/"new" items, may contain the "old" items.
    IntegerBinding maxRowsCountBinding = (rowCount == null) ? itemsCountBinding :
            (IntegerBinding) Bindings.min(
                    rowCount,
                    itemsCountBinding
            );
    IntegerBinding rowCountBinding = (IntegerBinding) Bindings.max(
            1, // Ensure to display at least 1 row, for JavaFX "No contents" message when table.items.isEmpty.
            maxRowsCountBinding
    );

    DoubleBinding tableHeightBinding = headerRowHeightProperty
            .add(rowCountBinding.multiply(table.getFixedCellSize()))
            .add(10); // TODO Understand why we need to add a dozen of pixels.

    table.minHeightProperty().bind(tableHeightBinding);
    table.prefHeightProperty().bind(tableHeightBinding);
    table.maxHeightProperty().bind(tableHeightBinding);
}

@Null
public static TableHeaderRow headerRow(@NotNull TableView<?> table) {
    TableHeaderRow tableHeaderRow = (TableHeaderRow) table.lookup("TableHeaderRow");
    return tableHeaderRow;
}