TableView: adjust number of visible rows
Asked Answered
S

6

25

I'm using this table to display data in Table View:

import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Pagination;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;

public class MainApp extends Application
{
    IntegerProperty intP = new SimpleIntegerProperty(5);
    AnchorPane anchor = new AnchorPane();
    Scene scene;
    ObservableList<Integer> options
        = FXCollections.observableArrayList(
            5,
            10,
            15,
            20);
    final ComboBox comboBox = new ComboBox(options);
    final ObservableList<Person> data = FXCollections.observableArrayList(
        new Person("1", "Joe", "Pesci"),
        new Person("2", "Audrey", "Hepburn"),
        new Person("3", "Gregory", "Peck"),
        new Person("4", "Cary", "Grant"),
        new Person("5", "De", "Niro"),
        new Person("6", "Katharine", "Hepburn"),
        new Person("7", "Jack", "Nicholson"),
        new Person("8", "Morgan", "Freeman"),
        new Person("9", "Elizabeth", "Taylor"),
        new Person("10", "Marcello", "Mastroianni"),
        new Person("11", "Innokenty", "Smoktunovsky"),
        new Person("12", "Sophia", "Loren"),
        new Person("13", "Alexander", "Kalyagin"),
        new Person("14", "Peter", "OToole"),
        new Person("15", "Gene", "Wilder"),
        new Person("16", "Evgeny", "Evstegneev"),
        new Person("17", "Michael", "Caine"),
        new Person("18", "Jean-Paul", "Belmondo"),
        new Person("19", " Julia", "Roberts"),
        new Person("20", "James", "Stewart"),
        new Person("21", "Sandra", "Bullock"),
        new Person("22", "Paul", "Newman"),
        new Person("23", "Oleg", "Tabakov"),
        new Person("24", "Mary", "Steenburgen"),
        new Person("25", "Jackie", "Chan"),
        new Person("26", "Rodney", "Dangerfield"),
        new Person("27", "Betty", "White"),
        new Person("28", "Eddie", "Murphy"),
        new Person("29", "Amitabh", "Bachchan"),
        new Person("30", "Nicole", "Kidman"),
        new Person("31", "Adriano", "Celentano"),
        new Person("32", "Rhonda", " Fleming's"),
        new Person("32", "Humphrey", "Bogart"));
    private Pagination pagination;

    public static void main(String[] args) throws Exception
    {
        launch(args);
    }

    public int itemsPerPage()
    {
        return 1;
    }

    public int rowsPerPage()
    {
        return intP.get();
    }

    public VBox createPage(int pageIndex)
    {
        int lastIndex = 0;
        int displace = data.size() % rowsPerPage();
        if (displace > 0)
        {
            lastIndex = data.size() / rowsPerPage();
        }
        else
        {
            lastIndex = data.size() / rowsPerPage() - 1;
        }
        VBox box = new VBox();
        int page = pageIndex * itemsPerPage();
        for (int i = page; i < page + itemsPerPage(); i++)
        {
            TableView<Person> table = new TableView<>();

            TableColumn numCol = new TableColumn("ID");
            numCol.setCellValueFactory(new PropertyValueFactory<>("num"));
            numCol.setMinWidth(20);
            TableColumn firstNameCol = new TableColumn("First Name");
            firstNameCol.setCellValueFactory(new PropertyValueFactory<>("firstName"));

            firstNameCol.setMinWidth(160);
            TableColumn lastNameCol = new TableColumn("Last Name");
            lastNameCol.setCellValueFactory(new PropertyValueFactory<>("lastName"));
            lastNameCol.setMinWidth(160);
            table.getColumns().addAll(numCol, firstNameCol, lastNameCol);
            if (lastIndex == pageIndex)
            {
                table.setItems(FXCollections.observableArrayList(data.subList(pageIndex * rowsPerPage(), pageIndex * rowsPerPage() + displace)));
            }
            else
            {
                table.setItems(FXCollections.observableArrayList(data.subList(pageIndex * rowsPerPage(), pageIndex * rowsPerPage() + rowsPerPage())));
            }

            box.getChildren().addAll(table);

        }
        return box;
    }

    @Override
    public void start(final Stage stage) throws Exception
    {
        scene = new Scene(anchor, 450, 450);
        comboBox.valueProperty().addListener(new ChangeListener<Number>()
        {
            @Override
            public void changed(ObservableValue o, Number oldVal, Number newVal)
            {
                //System.out.println(newVal.intValue());
                intP.set(newVal.intValue());
                paginate();
            }
        });
        paginate();
        stage.setScene(scene);
        stage.setTitle("Table pager");
        stage.show();
    }

    public void paginate()
    {
        pagination = new Pagination((data.size() / rowsPerPage() + 1), 0);
        //   pagination = new Pagination(20 , 0);
//        pagination.setStyle("-fx-border-color:red;");
        pagination.setPageFactory(new Callback<Integer, Node>()
        {
            @Override
            public Node call(Integer pageIndex)
            {
                if (pageIndex > data.size() / rowsPerPage() + 1)
                {
                    return null;
                }
                else
                {
                    return createPage(pageIndex);
                }
            }
        });

        AnchorPane.setTopAnchor(pagination, 10.0);
        AnchorPane.setRightAnchor(pagination, 10.0);
        AnchorPane.setBottomAnchor(pagination, 10.0);
        AnchorPane.setLeftAnchor(pagination, 10.0);

        AnchorPane.setBottomAnchor(comboBox, 40.0);
        AnchorPane.setLeftAnchor(comboBox, 12.0);
        anchor.getChildren().clear();
        anchor.getChildren().addAll(pagination, comboBox);
    }

    public static class Person
    {
        private final SimpleStringProperty num;
        private final SimpleStringProperty firstName;
        private final SimpleStringProperty lastName;

        private Person(String id, String fName, String lName)
        {
            this.firstName = new SimpleStringProperty(fName);
            this.lastName = new SimpleStringProperty(lName);
            this.num = new SimpleStringProperty(id);
        }

        public String getFirstName()
        {
            return firstName.get();
        }

        public void setFirstName(String fName)
        {
            firstName.set(fName);
        }

        public String getLastName()
        {
            return lastName.get();
        }

        public void setLastName(String fName)
        {
            lastName.set(fName);
        }

        public String getNum()
        {
            return num.get();
        }

        public void setNum(String id)
        {
            num.set(id);
        }
    }
}

When I change the number of rows using Combo box only data in table rows is changed. Table height is not changed.

enter image description here

Is there a way to remove empty rows?

Snowwhite answered 10/10, 2014 at 11:26 Comment(1)
Changing tableview height and removing empty rows are two different things. Be specific. For removing rows see fxexperience.com/2011/11/…Always
A
37

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.

Always answered 14/10, 2014 at 17:46 Comment(1)
@PeterPenzov. Put that 2 line of code after the line TableView<Person> table = new TableView<>(); in your code above in the question.Always
H
19

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);
    }

}
Halona answered 14/10, 2014 at 15:8 Comment(10)
This works beautifully, many thanks for posting it. If anybody else is trying it - here are the imports it needs: import com.sun.javafx.scene.control.skin.*; import javafx.scene.control.*; import javafx.scene.control.cell.PropertyValueFactory; import javafx.beans.property.*; import java.lang.reflect.*; Casket
Note these imports are different in Java 9, with com.sun.javafx.scene.control.skin being replaced by javafx.scene.control.skin. (This means they are public API as of Java 9.) I haven't tested this solution in that version.Rhinoscopy
Excellent! I just made a slight change in your code : requestLayout instead of RequestFocus so that tableView is updated when visibleRow property is changed.Spitsbergen
@FlatEric hmm .. what's the problem? Should be the same except the skin adding listener (the regisgterXX now takes consumer which calls a method updating stuff, just follow the lead in similar property listening). Anyway, will try to add an update for current fx soon :)Halona
Thx for the quick response. One problem is that needCellsReconfigured field is no longer accessable.Danielldaniella
@FlatEric ahh .. yeah, faintly remember that the update process was changed (not necessarily for the better ;) If it's still there, you can go dirty with reflection .. don't have any more other then such concededly very vague hints at this time, sryHalona
@FlatEric looks like it's no longer needed (but flow.getCellLength moved to package private) - quick update to fx15 .. hih but no guarantee :)Halona
Thanks again, I'll test it tomorrowDanielldaniella
Ok, I tested and it works with some changes to the reflection-part (FXUtils.invokeGetMethodValue is no framework method)Danielldaniella
@FlatEric hach, forgot my usual disclaimer ("use your own favorite reflection utility code" :) Thanks for the heads-up!Halona
J
4

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.

Jesu answered 16/10, 2014 at 11:8 Comment(1)
Brilliant and simple!Loge
B
3

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;
}
Basanite answered 15/10, 2017 at 17:51 Comment(6)
The extra 10 pixels are from the table insets.Twocycle
Sure, but for what graphics? Lines? Spaces? And why 10 pixels? Will it always be 10 pixels? Does this value depend on something that may vary other time?Basanite
It depends on styling, so you're better off using the top/bottom values from table.getInsets() rather than a fixed value.Twocycle
(See my answer below for an example.)Twocycle
Works fine, thanks !... ... except when the horizontal scrollbar is displayed :-( Is there a way to dynamically resize the table when the scrollbar is shown?Basanite
I haven't tried, but this answer shows how to add a listener to detect when a scrollbar becomes visible/invisible (the vertical one in that instance but I assume horizontal is similar).Twocycle
T
2

If you're not wedded to bindings, a simple way to do this is to calculate the desired height based on the fixed cell size (cf. Fred Danna's answer) and update it with a listener on the table data.

static void setTableHeightByRowCount(TableView table, ObservableList data) {
  int rowCount = data.size();
  TableHeaderRow headerRow = (TableHeaderRow) table.lookup("TableHeaderRow");
  double tableHeight = (rowCount * table.getFixedCellSize())
    // add the insets or we'll be short by a few pixels
    + table.getInsets().getTop() + table.getInsets().getBottom()
    // header row has its own (different) height
    + (headerRow == null ? 0 : headerRow.getHeight())
    ;

  table.setMinHeight(tableHeight);
  table.setMaxHeight(tableHeight);
  table.setPrefHeight(tableHeight);
}

In start(Stage), we create the table and add a ListChangeListener:

TableView<String> table = new TableView<>();
table.setFixedCellSize(24);
table.getItems().addListener((ListChangeListener<String>) c ->
  setTableHeightByRowCount(table, c.getList()));

// init scene etc...

stage.show();
table.getItems().addAll("Stacey", "Kristy", "Mary Anne", "Claudia");

Note that the table header row doesn't exist till after stage.show(), so the simplest thing to do is to wait to set the table data till then. Alternatively, we could set the data at table construction time, and then call setTableHeightByRowCount() explicitly:

TableView<String> table = new TableView<>(
  FXCollections.observableArrayList("Stacey", "Kristy", "Mary Anne", "Claudia")
);

// add listener, init scene etc...

stage.show();
setTableHeightByRowCount(table, table.getItems());
Twocycle answered 19/12, 2017 at 18:53 Comment(0)
K
0

Is there a way to do it... yes, what you need to do is when you create the table (since you re-create it every time you select a new number) you need to compute what the height of the table is with the current number of entries, and then use the setPrefHeight() property of TableView to make the table smaller to account for only those rows.

I toyed with it a little bit, and I didn't find any quick solutions to this to calculate the size of the table properly, so I don't have any code for you, but that is what you need to do. You could also 'style' the table to not have the alternating color scheme, which would make the rows below the ones that have data look 'empty' even though there would be some white space.

Good luck!

Khalif answered 10/10, 2014 at 13:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.