Auto fit size column in table view
Asked Answered
N

14

47

The TableView in JavaFX have 2 column resize policies:

CONSTRAINED_RESIZE_POLICY and UNCONSTRAINED_RESIZE_POLICY

But I want columns that are resized to fit the content of theirs cells.

How do I do this?

Nevlin answered 1/2, 2013 at 16:31 Comment(1)
there seem to be bug for this problem: bugs.openjdk.java.net/browse/…Doc
N
44

After 3 years I come back to this problem again, some suggestions are calculating the size of text of data in each cell (it's complicated depending on font size, font family, padding...)

But I realize that when I click on the divider on table header, it's resized fit to content as I want. So I dig into JavaFX source code I finally found resizeColumnToFitContent method in TableViewSkin, but it is protected method, we can resolve by reflection:

import com.sun.javafx.scene.control.skin.TableViewSkin;
import javafx.scene.control.Skin;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class GUIUtils {
    private static Method columnToFitMethod;

    static {
        try {
            columnToFitMethod = TableViewSkin.class.getDeclaredMethod("resizeColumnToFitContent", TableColumn.class, int.class);
            columnToFitMethod.setAccessible(true);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    public static void autoFitTable(TableView tableView) {
        tableView.getItems().addListener(new ListChangeListener<Object>() {
            @Override
            public void onChanged(Change<?> c) {
                for (Object column : tableView.getColumns()) {
                    try {
                        columnToFitMethod.invoke(tableView.getSkin(), column, -1);
                    } catch (IllegalAccessException | InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
    }
}

Note that we call "tableView.getItems()" so we have to call this function after setItems()

Nevlin answered 31/7, 2016 at 17:2 Comment(5)
I can't find or import the class TableViewSkin. I'm using Java 8 and this class doesn't seem to exist.Doubles
@Spen: I use java 8 and I can see it, can you check is the jfxrt.jar imported properly?Nevlin
For me this doesn't resize the columns, it just makes them impossible to resize for the userMccleary
The method 'resizeColumnToFitContent' has been moved to javafx.scene.control.skin.TableSkinUtils.Orthoptic
In JavaFx 16 the method resizeColumnToFitContent is now in javafx.scene.control.skin.TableColumnHeaderAngus
C
26

After testing the previous solutions I finally found one that worked for me. So here is mine (call the method after inserting the data into table):

public static void autoResizeColumns( TableView<?> table )
{
    //Set the right policy
    table.setColumnResizePolicy( TableView.UNCONSTRAINED_RESIZE_POLICY);
    table.getColumns().stream().forEach( (column) ->
    {
        //Minimal width = columnheader
        Text t = new Text( column.getText() );
        double max = t.getLayoutBounds().getWidth();
        for ( int i = 0; i < table.getItems().size(); i++ )
        {
            //cell must not be empty
            if ( column.getCellData( i ) != null )
            {
                t = new Text( column.getCellData( i ).toString() );
                double calcwidth = t.getLayoutBounds().getWidth();
                //remember new max-width
                if ( calcwidth > max )
                {
                    max = calcwidth;
                }
            }
        }
        //set the new max-widht with some extra space
        column.setPrefWidth( max + 10.0d );
    } );
}
Chrischrism answered 6/3, 2018 at 15:13 Comment(1)
This works great in my testing, is simpler than the other suggestions (that work, for instance the answers that involve reflection won't work when using JavaFX and linking via java modules).Pollie
B
8

I think just by overriding a call back function that returns true will solve your problem it will disable the re-sizing of columns and all columns will be re-sized to fit the content of their cells.

Example:

TableView<String[]> table = new TableView<>();
table.setColumnResizePolicy(new Callback<TableView.ResizeFeatures, Boolean>() {
  @Override
  public Boolean call(ResizeFeatures p) {
     return true;
  }
});
Braided answered 8/5, 2013 at 10:23 Comment(2)
It is worth noting that it prevents user from resizing the column.Laudanum
I'm using javafx 11.0.2 and this solution doesn't work for me, it just prevents resizing, but the the columns aren't resized to fit the content..Automate
A
5

If you want that only one column fills the remaining width of a table, I have found a pretty straight forward solution, which is short and does not require the hacky reflection solution described above:

DoubleBinding usedWidth = columnA.widthProperty().add(columnB.widthProperty()).add(columnC.widthProperty());

fillingColumn.prefWidthProperty().bind(tableView.widthProperty().subtract(usedWidth));
Angara answered 14/6, 2017 at 17:28 Comment(0)
M
3

Or to make it short:

// automatically adjust width of columns depending on their content
configAttributeTreeTable.setColumnResizePolicy((param) -> true );
Millstream answered 18/3, 2014 at 18:54 Comment(0)
H
3

I have used the other solutions on this question, and it works pretty good. However, the downside of this is when the width of the TableView is greater than the required width of the TableColumns together. I have created a hack to solve this problem, and it works OK:

orderOverview.setColumnResizePolicy((param) -> true );
Platform.runLater(() -> FXUtils.customResize(orderOverview));

where FXUtils.customResize() is created as follows:

public static void customResize(TableView<?> view) {

    AtomicDouble width = new AtomicDouble();
    view.getColumns().forEach(col -> {
        width.addAndGet(col.getWidth());
    });
    double tableWidth = view.getWidth();

    if (tableWidth > width.get()) {
        TableColumn<?, ?> col = view.getColumns().get(view.getColumns().size()-1);
        col.setPrefWidth(col.getWidth()+(tableWidth-width.get()));
    }

}

I hope this could be helpful for other people as well!

Horseplay answered 12/11, 2015 at 10:13 Comment(1)
No such thing as AtomicDouble; use DoubleAdderCatafalque
S
2

This is the way I found :

tableview.setColumnResizePolicy( TableView.CONSTRAINED_RESIZE_POLICY );
idCol.setMaxWidth( 1f * Integer.MAX_VALUE * 50 ); // 50% width
nameCol.setMaxWidth( 1f * Integer.MAX_VALUE * 30 ); // 30% width
ageCol.setMaxWidth( 1f * Integer.MAX_VALUE * 20 ); // 20% width
Sextant answered 28/6, 2021 at 10:27 Comment(0)
P
1

This code autoresizes all column widths in relational proportions to the table width,
while it can fix the first column width to a given value when table width is lower than x

        // To generalize the columns width proportions in relation to the table width,
        // you do not need to put pixel related values, you can use small float numbers if you wish,
        // because it's the relative proportion of each columns width what matters here:

        final float[] widths = { 1.2f, 2f, 0.8f };// define the relational width of each column 

        // whether the first column should be fixed
        final boolean fixFirstColumm = true; 

        // fix the first column width when table width is lower than:
        final float fixOnTableWidth = 360; //pixels 

        // calulates sum of widths
        float sum = 0;
        for (double i : widths) {
            sum += i;
        }

        // calculates the fraction of the first column proportion in relation to the sum of all column proportions
        float firstColumnProportion = widths[0] / sum;

        // calculate the fitting fix width for the first column, you can change it by your needs, but it jumps to this width
        final float firstColumnFixSize = fixOnTableWidth * firstColumnProportion;

        // set the width to the columns
        for (int i = 0; i < widths.length; i++) {
            table.getColumns().get(i).prefWidthProperty().bind(table.widthProperty().multiply((widths[i] / sum)));
            // ---------The exact width-------------^-------------^
    if (fixFirstColumm)
            if (i == 0) {
                table.widthProperty().addListener(new ChangeListener<Number>() {
                    @Override
                    public void changed(ObservableValue<? extends Number> arg0, Number oldTableWidth, Number newTableWidth) {

                        if (newTableWidth.intValue() <= fixOnTableWidth) {

                            // before you can set new value to column width property, need to unbind the autoresize binding
                            table.getColumns().get(0).prefWidthProperty().unbind();
                            table.getColumns().get(0).prefWidthProperty().setValue(firstColumnFixSize);

                        } else if (!table.getColumns().get(0).prefWidthProperty().isBound()) {

                            // than readd the autoresize binding if condition table.width > x
                            table.getColumns().get(0).prefWidthProperty()
                                    .bind(table.widthProperty().multiply(firstColumnProportion));
                        }

                    }
                });
            }
        }

advice to put the code in an separated TableAutoresizeModel class, there you can handle further calculations, for example on hiding columns add listener...

Pirn answered 4/9, 2017 at 5:27 Comment(0)
S
1

@HarleyDavidson 's answer in kotlin

val String.fxWidth: Double
    get() = Text(this).layoutBounds.width

//  call the method after inserting the data into table
fun <T> TableView<T>.autoResizeColumns() {
    columnResizePolicy = TableView.UNCONSTRAINED_RESIZE_POLICY
    columns.forEach { column ->
        column.setPrefWidth(
            (((0 until items.size).mapNotNull {
                column.getCellData(it)
            }.map {
                it.toString().fxWidth
            }.toMutableList() + listOf(
                column.text.fxWidth
            )).maxOrNull() ?: 0.0) + 10.0
        )
    }
}
Scourings answered 6/4, 2021 at 11:2 Comment(0)
D
1

This will set the minimum width of columns based on the font and the text, so that the column names wont be cropped.

public static void setDataTableMinColumnWidth(TableView<?> dataTable)
    {
        for (Node columnHeader : dataTable.lookupAll(".column-header"))
        {
            var columnString = columnHeader.getId();
            if (columnString != null)
            {
                for (Node columnHeaderLabel : columnHeader.lookupAll(".label"))
                {
                    var tableColumn = dataTable.getColumns()
                                               .stream()
                                               .filter(x -> x.getId()
                                                             .equals(columnString))
                                               .findFirst();
                    if (columnHeaderLabel instanceof Label && tableColumn.isPresent())
                    {
                        var label = (Label) columnHeaderLabel;
                        /* calc text width based on font */
                        var theText = new Text(label.getText());
                        theText.setFont(label.getFont());
                        var width = theText.getBoundsInLocal()
                                           .getWidth();
                        /*
                         * add 10px because of paddings/margins for the button
                         */
                        tableColumn.get()
                                   .setMinWidth(width + 10);
                    }
                }
            }
        }
    }

How to use:

dataTable.needsLayoutProperty()
             .addListener((obs, o, n) -> setDataTableMinColumnWidth(dataTable));

For the Columns, the id property needs to be set first:

    TableColumn<BundImportTask, String> columnTask = new TableColumn<>("My Column");
    columnTask.setId("MyColumnId");
    columnTask.setCellValueFactory(data -> new SimpleStringProperty(data.getValue()
                                                                        .fileName()));
Duckbill answered 24/8, 2021 at 11:51 Comment(0)
U
0

I implemented a solution that it's fairly more complicated than the ones that I found here, but that allows a specific column to be resized by double clicking on the header, while still letting the user resize columns manually.

This is achieved by listening to click events on the header of the table (TableHeaderRow). When a double click occurs, the specific column header is found by matching the mouse event X and Y.

Note: to make this work it's necessary that each column has an ID set.

// when skin is loaded (hence css), setup click listener on header to make column fit to content max width on double click
tableView.skinProperty().addListener((a, b, newSkin) -> {
    TableHeaderRow headerRow = (TableHeaderRow) tableView.lookup("TableHeaderRow");
    NestedTableColumnHeader headers = (NestedTableColumnHeader) (headerRow.getChildren().get(1));

    headerRow.setOnMouseClicked(evt -> {
        if (evt.getClickCount() != 2 || evt.getButton() != MouseButton.PRIMARY) return;

        // find the header column that contains the click
        for (TableColumnHeader header : headers.getColumnHeaders()) {
            if (header.contains(header.parentToLocal(evt.getX(), evt.getY()))) {
                fitColumnWidthToContent(header.getId());
            }
        }
        evt.consume();
    });
});

The method that takes care of the resizing is the following:

 private void fitColumnWidthToContent (String colId) {
    // find column matching id
    TableColumn column = null;

    for (TableColumn tempCol : tableView.getColumns()) {
        if (tempCol.getId().equals(colId)) {
            column = tempCol;
            break;
        }
    }

    if (column == null) {
        throw new IllegalStateException("Column ID doesn't match any actual column");
    }

    // set default width to column header width
    Text text = new Text(column.getText());
    double max = text.getLayoutBounds().getWidth();

    for (int i = 0; i < tableView.getItems().size(); i++ ) {
        if (column.getCellData(i) == null) continue;

        text = new Text(column.getCellData(i).toString());
        double textWidth = text.getLayoutBounds().getWidth();

        if (textWidth > max) {
            max = textWidth;
        }
    }

    column.setPrefWidth(max + 12);
}

I hope this can be useful to anyone.

In order to allow also manual resizing, it's necessary to add a bit more code on table initalization:

// listen to width changes in columns and set to pref width (otherwise if for example width changes because of
// user resizing the column, applying the old pref width won't work because it stayed the same)
for (TableColumn col : tableView.getColumns()) {
    col.widthProperty().addListener((obs, oldVal, newVal) -> {
        col.setPrefWidth(newVal.doubleValue());
    });
}
Unnatural answered 21/5, 2020 at 13:47 Comment(0)
B
0

I have implemented a solution for TreeTableView. It is still in evolution but it manifests now promising results. Hereafter a description of the solution.

In the control skin class, I added to the control children the TreeTableView and an invisible VBox. A cell factory provide derived cells to the target TreeTableColumn. The derived cells wrap a Label node which is added or removed to the invisible VBox according to the empty property, and which its prefWidth is set according to the cell width. The cells make use of:

getProperties().put(Properties.DEFER_TO_PARENT_PREF_WIDTH, Boolean.TRUE)

I override the cell's computePrefWidth() method as follow:

@Override
protected double computePrefWidth(double height) {

    return Double.max(_box.prefWidth(-1.0), super.computePrefWidth(height) + 24.0);

}

The Vbox width property is bind to the TreeTableColumn's prefWidth. This is required to resize as well the header of the column.

Is worth to note, that at the time being, to simplify the development of a solution, this approach works well with built in sort, order, and resize feature disabled. Ie.

_nameColumn = new TreeTableColumn<>("Name");
_nameColumn.setResizable(false);
_nameColumn.setReorderable(false);
_nameColumn.setSortable(false);

Happy coding

Brigidabrigit answered 13/2, 2021 at 11:14 Comment(0)
H
-1

After long research. Best Solution is..

tblPlan.setColumnResizePolicy((param) -> true );
Platform.runLater(() -> customResize(tblPlan));

"Custom Resize"

public void customResize(TableView<?> view) {

        AtomicLong width = new AtomicLong();
        view.getColumns().forEach(col -> {
            width.addAndGet((long) col.getWidth());
        });
        double tableWidth = view.getWidth();

        if (tableWidth > width.get()) {
            view.getColumns().forEach(col -> {
                col.setPrefWidth(col.getWidth()+((tableWidth-width.get())/view.getColumns().size()));
            });
        }
    }
Hogan answered 26/10, 2016 at 5:12 Comment(0)
C
-1
<TableView fx:id="datalist" layoutX="30.0" layoutY="65.0" prefHeight="400.0" AnchorPane.bottomAnchor="100.0" AnchorPane.leftAnchor="30.0" AnchorPane.rightAnchor="30.0" AnchorPane.topAnchor="100.0">
        <columns>
            <TableColumn fx:id="number" minWidth="-1.0" prefWidth="-1.0" style="width: auto;" text="number" />
            <TableColumn fx:id="id" minWidth="-1.0" prefWidth="-1.0" text="id" />
            <TableColumn fx:id="name" minWidth="-1.0" prefWidth="-1.0" text="name" />
            <TableColumn fx:id="action" minWidth="-1.0" prefWidth="-1.0" text="todo" />
        </columns>
         **<columnResizePolicy>
            <TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
         </columnResizePolicy>**
      </TableView>
Chabazite answered 10/4, 2019 at 7:2 Comment(2)
CONSTRAINED_RESIZE_POLICYChabazite
Please put your answer always in context instead of just pasting code. See here for more details.Sculpin

© 2022 - 2024 — McMap. All rights reserved.