2

I have following problem: When i create a TableView and put data in the columns, the columns fit to content automatically. But if there many rows (more than 30) JavaFX optimize the columnwidth to the average length of all content.

In the first example i put the long strings first in the table and everything is fine.

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class Example extends Application {

    @Override
    public void start(Stage stage) {

        // Init data
        ObservableList<Person> persons = FXCollections.observableArrayList();
        persons.add(new Person("Maximus-Superman", "Power", "Maximus-Superman.Power@test.com"));
        persons.add(new Person("Max", "Powerstrongsupercool", "Max.Powerstrongsupercool@test.com"));
        persons.add(new Person("Maximus-Superman", "Powerstrongsupercool", "Maximus-Superman.Powerstrongsupercool@test.com"));
        for (int i = 0; i < 100; i++) {
            persons.add(new Person("Max", "Power", "Max.Power@test.com"));
        }
        // Init table
        TableView<Person> table = new TableView<Person>();
        table.setMaxWidth(Double.MAX_VALUE);
        table.setMaxHeight(Double.MAX_VALUE);
        table.setItems(persons);

        // Init columns
        TableColumn<Person, String> firstname = new TableColumn<Person, String>("Firstname");
        firstname.setCellValueFactory(new PropertyValueFactory<Person, String>("firstname"));
        table.getColumns().add(firstname);

        TableColumn<Person, String> lastname = new TableColumn<Person, String>("Lastname");
        lastname.setCellValueFactory(new PropertyValueFactory<Person, String>("lastname"));
        table.getColumns().add(lastname);

        TableColumn<Person, String> email = new TableColumn<Person, String>("E-Mail");
        email.setCellValueFactory(new PropertyValueFactory<Person, String>("email"));
        table.getColumns().add(email);

        // Init Stage
        Scene scene = new Scene(table, 400, 150);
        stage.setScene(scene);
        stage.show();
    }

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

    public class Person {

        private String firstname;
        private String lastname;
        private String email;

        public Person(String firstname, String lastname, String email) {
            this.firstname = firstname;
            this.lastname = lastname;
            this.email = email;
        }

        // Getters and Setters

    }
}

Looks good...

enter image description here

In the second example i put short strings first in the table and at least the long strings. In this example JavaFX choose columnwidth to small and the content gets cut.

public class Example extends Application {

    @Override
    public void start(Stage stage) {

        // Init data
        ObservableList<Person> persons = FXCollections.observableArrayList();
        for (int i = 0; i < 100; i++) {
            persons.add(new Person("Max", "Power", "Max.Power@test.com"));
        }
        persons.add(new Person("Maximus-Superman", "Power", "Maximus-Superman.Power@test.com"));
        persons.add(new Person("Max", "Powerstrongsupercool", "Max.Powerstrongsupercool@test.com"));
        persons.add(new Person("Maximus-Superman", "Powerstrongsupercool", "Maximus-Superman.Powerstrongsupercool@test.com"));
        [...]

Looks bad...

enter image description here

How can i avoid this?​

EDIT 19.05.2018

The links in the comments didn`t work.

So, i have found the problem in the source of JavaFx: In the updateScene() method the maxRows are set to 30. There is no way to change the value because every methods that are involved are protected or private.

The comment is right. It can be take much time to create a table if there many rows in the table. But sometimes a developer knows the much possible rows of a table or it´s okay to risk higher loadingtime.

One solution is to contact Oracle to create a setter() for the value of max. rows. So the developer can choose the max. rows for each column individually.

public class TableColumnHeader extends Region {

    [...]

    private void updateScene() {
        // RT-17684: If the TableColumn widths are all currently the default,
        // we attempt to 'auto-size' based on the preferred width of the first
        // n rows (we can't do all rows, as that could conceivably be an unlimited
        // number of rows retrieved from a very slow (e.g. remote) data source.
        // Obviously, the bigger the value of n, the more likely the default
        // width will be suitable for most values in the column
        final int n = 30; // ------------------------------------------> This is the problem!
        if (! autoSizeComplete) {
            if (getTableColumn() == null || getTableColumn().getWidth() != DEFAULT_COLUMN_WIDTH || getScene() == null) {
                return;
            }
            doColumnAutoSize(getTableColumn(), n);
            autoSizeComplete = true;
        }
    }

    [...]

    private void doColumnAutoSize(TableColumnBase<?,?> column, int cellsToMeasure) {
        double prefWidth = column.getPrefWidth();

        // if the prefWidth has been set, we do _not_ autosize columns
        if (prefWidth == DEFAULT_COLUMN_WIDTH) {
            getTableViewSkin().resizeColumnToFitContent(column, cellsToMeasure);
        }
    }

    [...]

}

public class TableViewSkin<T> extends TableViewSkinBase<T, T, TableView<T>, TableViewBehavior<T>, TableRow<T>, TableColumn<T, ?>> {

    [...]

    @Override protected void resizeColumnToFitContent(TableColumn<T, ?> tc, int maxRows) {
        if (!tc.isResizable()) return;

        // final TableColumn<T, ?> col = tc;
        List<?> items = itemsProperty().get();
        if (items == null || items.isEmpty()) return;

        Callback/*<TableColumn<T, ?>, TableCell<T,?>>*/ cellFactory = tc.getCellFactory();
        if (cellFactory == null) return;

        TableCell<T,?> cell = (TableCell<T, ?>) cellFactory.call(tc);
        if (cell == null) return;

        // set this property to tell the TableCell we want to know its actual
        // preferred width, not the width of the associated TableColumnBase
        cell.getProperties().put(TableCellSkin.DEFER_TO_PARENT_PREF_WIDTH, Boolean.TRUE);

        // determine cell padding
        double padding = 10;
        Node n = cell.getSkin() == null ? null : cell.getSkin().getNode();
        if (n instanceof Region) {
            Region r = (Region) n;
            padding = r.snappedLeftInset() + r.snappedRightInset();
        }

        int rows = maxRows == -1 ? items.size() : Math.min(items.size(), maxRows); // ------------------> if maxRows equals -1 every item will be checked
        double maxWidth = 0;
        for (int row = 0; row < rows; row++) {
            cell.updateTableColumn(tc);
            cell.updateTableView(tableView);
            cell.updateIndex(row);

            if ((cell.getText() != null && !cell.getText().isEmpty()) || cell.getGraphic() != null) {
                getChildren().add(cell);
                cell.applyCss();
                maxWidth = Math.max(maxWidth, cell.prefWidth(-1));
                getChildren().remove(cell);
            }
        }

        // dispose of the cell to prevent it retaining listeners (see RT-31015)
        cell.updateIndex(-1);

        // RT-36855 - take into account the column header text / graphic widths.
        // Magic 10 is to allow for sort arrow to appear without text truncation.
        TableColumnHeader header = getTableHeaderRow().getColumnHeaderFor(tc);
        double headerTextWidth = Utils.computeTextWidth(header.label.getFont(), tc.getText(), -1);
        Node graphic = header.label.getGraphic();
        double headerGraphicWidth = graphic == null ? 0 : graphic.prefWidth(-1) + header.label.getGraphicTextGap();
        double headerWidth = headerTextWidth + headerGraphicWidth + 10 + header.snappedLeftInset() + header.snappedRightInset();
        maxWidth = Math.max(maxWidth, headerWidth);

        // RT-23486
        maxWidth += padding;
        if(tableView.getColumnResizePolicy() == TableView.CONSTRAINED_RESIZE_POLICY) {
            maxWidth = Math.max(maxWidth, tc.getWidth());
        }

        tc.impl_setWidth(maxWidth);
    }

    [...]

}

Another solution is to fire a MouseEvent on the header of the rect of the TableColumn.

enter image description here

If there a MouseEvent with a ClickCount equals 2 and the PrimaryButton is down the resizeColumnToFitContent() method is called with a value for maxRows of -1.

int rows = maxRows == -1 ? items.size() : Math.min(items.size(), maxRows);

-1 means all rows that are in the TableView.

public class NestedTableColumnHeader extends TableColumnHeader {

    [...]

    private static final EventHandler<MouseEvent> rectMousePressed = new EventHandler<MouseEvent>() {
        @Override public void handle(MouseEvent me) {
            Rectangle rect = (Rectangle) me.getSource();
            TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
            NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);

            if (! header.isColumnResizingEnabled()) return;

            if (me.getClickCount() == 2 && me.isPrimaryButtonDown()) {
                // the user wants to resize the column such that its
                // width is equal to the widest element in the column
                header.getTableViewSkin().resizeColumnToFitContent(column, -1); // -----------------------> this method should be call and everything is fine
            } else {
                // rather than refer to the rect variable, we just grab
                // it from the source to prevent a small memory leak.
                Rectangle innerRect = (Rectangle) me.getSource();
                double startX = header.getTableHeaderRow().sceneToLocal(innerRect.localToScene(innerRect.getBoundsInLocal())).getMinX() + 2;
                header.dragAnchorX = me.getSceneX();
                header.columnResizingStarted(startX);
            }
            me.consume();
        }
    };

    [...]

}

So, is it possible to create a new MouseEvent with a ClickCount of 2 and the PrimaryButtonDown-boolean is true and fire this to the TableColumn?

And: How can i contact Oracle to please them to create a setter() for the maxRows in the next release?

user6266369
  • 183
  • 1
  • 15
  • https://stackoverflow.com/questions/14650787/javafx-column-in-tableview-auto-fit-size – SedJ601 May 18 '18 at 14:39
  • https://stackoverflow.com/questions/10152828/javafx-2-automatic-column-width/38918168 – SedJ601 May 18 '18 at 14:39
  • 1
    Virtualisation and what you're trying to do have opposite effects on the performance. I'm pretty sure you'd need to go through all items, determine the greatest width of the text required to display the text and set the size constraints of the columns accordingly... – fabian May 18 '18 at 15:05

0 Answers0