Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFX TreeView TreeColumn auto-size to fit content

I have been trying to figure out how to auto-size TreeView columns to fit their content for a while now. This has been asked before, but the response was minimal and not acceptable.

[javafx column in tableview auto fit size

Curiously, the behavior I'm looking for is exactly what happens when you double-click a column resize line in TableView. However, I have been unable to figure out how this occurs when looking through the JavaFX source code.

Does anyone know how/where the double-click behavior on a column resize line is handled in JavaFX TableView/TableColumn/TableColumnHeader?

If possible, I would simply like the columns to automatically do at first render what happens when you double-click the resize line. How this isn't already one of the available pre-created column size constraint policies is beyond me.

like image 659
robross0606 Avatar asked May 14 '14 13:05

robross0606


3 Answers

The solution that works for me on JavaFX8:

Set the columnResizePolciy to CONSTRAINED_RESIZE_POLICY

<TableView fx:id="table" >
    <columnResizePolicy>
        <TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
    </columnResizePolicy>
</TableView>
like image 168
James Wierzba Avatar answered Sep 18 '22 23:09

James Wierzba


There's a few missing pieces in your code like obj isn't declared. I also can't find the method you're using anywhere in that class. I'm using 1.8.0_20-ea-b05 ,or so -version tells me. I found a similar method in TableViewSkin.

public static void autoSizeTableViewColumns(final TableView<?> tableView) {
    TableViewSkin<?> skin = (TableViewSkin<?>) tableView.getSkin();
    TableHeaderRow headerRow = skin.getTableHeaderRow();
    NestedTableColumnHeader rootHeader = headerRow.getRootHeader();
    for (TableColumnHeader columnHeader : rootHeader.getColumnHeaders()) {
        try {
            TableColumn<?, ?> column = (TableColumn<?, ?>) columnHeader.getTableColumn();
            if (column != null) {
                Method method = skin.getClass().getDeclaredMethod("resizeColumnToFitContent", TableColumn.class, int.class);
                method.setAccessible(true);
                method.invoke(skin,column, 30);
            }
        } catch (Throwable e) {
            e = e.getCause();
            e.printStackTrace(System.err);
        }
    }
}
like image 24
brian Avatar answered Sep 20 '22 23:09

brian


Here is the FINAL solution I came up with for JavaFX 2.2 which now also accounts for the width of the column header:

/**
 * Auto-sizes table view columns to fit its contents.
 * 
 * @note This is not a column resize policy and does not prevent manual
 *       resizing after this method has been called.
 * @param tableView
 *            The table view in which to resize all columns.
 */
public static void autoSizeTableViewColumns(final TableView<?> tableView)
{
    autoSizeTableViewColumns(tableView, -1, -1);
}

/**
 * Auto-sizes table view columns to fit its contents.
 * 
 * @note This is not a column resize policy and does not prevent manual
 *       resizing after this method has been called.
 *       
 * @param tableView
 *            The table view in which to resize all columns.
 * @param minWidth
 *            Minimum desired width of text for all columns.
 * @param maxWidth
 *            Maximum desired width of text for all columns.
 */
public static void autoSizeTableViewColumns(final TableView<?> tableView, int minWidth, int maxWidth)
{
    TableViewSkin<?> skin = (TableViewSkin<?>) tableView.getSkin();

    if (skin == null)
    {
        AppLogger.getLogger().warning(tableView + " skin is null.");
        return;
    }

    TableHeaderRow headerRow = skin.getTableHeaderRow();
    NestedTableColumnHeader rootHeader = headerRow.getRootHeader();
    for (Node node : rootHeader.getChildren())
    {
        if (node instanceof TableColumnHeader)
        {
            TableColumnHeader columnHeader = (TableColumnHeader) node;
            try
            {
                autoSizeTableViewColumn(columnHeader, minWidth, maxWidth, -1);
            }
            catch (Throwable e)
            {
                e = e.getCause();
                AppLogger.getLogger().log(Level.WARNING, "Unable to automatically resize tableView column.", e);
            }
        }
    }
}

/**
 * Auto-sizes table view columns to fit its contents.
 * 
 * @note This is not a column resize policy and does not prevent manual
 *       resizing after this method has been called.
 *       
 * @param col
 *            The column to resize.
 * @param minWidth
 *            Minimum desired width of text for this column. Use -1 for no minimum
 *            width.
 * @param maxWidth
 *            Maximum desired width of text for this column. Use -1 for no maximum
 *            width.
 * @param maxRows
 *            Maximum number of rows to examine for auto-resizing. Use -1
 *            for all rows.
 */
public static void autoSizeTableViewColumn(TableColumn<?,?> column, int minWidth, int maxWidth, int maxRows)
{
    TableView<?> tableView = column.getTableView();
    TableViewSkin<?> skin = (TableViewSkin<?>) tableView.getSkin();

    if (skin == null)
    {
        AppLogger.getLogger().warning(tableView + " skin is null.");
        return;
    }

    TableHeaderRow headerRow = skin.getTableHeaderRow();
    NestedTableColumnHeader rootHeader = headerRow.getRootHeader();
    for (Node node : rootHeader.getChildren())
    {
        if (node instanceof TableColumnHeader)
        {
            TableColumnHeader columnHeader = (TableColumnHeader) node;
            if(columnHeader.getTableColumn().equals(column))
            {
                autoSizeTableViewColumn(columnHeader, minWidth, maxWidth, maxRows);
            }
        }
    }
}

/**
 * Auto-sizes a table view column to fit its contents.
 * 
 * @note This is not a column resize policy and does not prevent manual
 *       resizing after this method has been called.
 * 
 * @param col
 *            The column to resize.
 * @param minWidth
 *            Minimum desired width of text for this column. Use -1 for no minimum
 *            width.
 * @param maxWidth
 *            Maximum desired width of text for this column. Use -1 for no maximum
 *            width.
 * @param maxRows
 *            Maximum number of rows to examine for auto-resizing. Use -1
 *            for all rows.
 */
@SuppressWarnings({ "rawtypes", "unchecked" })
public static void autoSizeTableViewColumn(TableColumnHeader header, int minWidth, int maxWidth, int maxRows)
{
    TableColumn<?, ?> col = header.getTableColumn();
    if(col != null)
    {
        List<?> items = col.getTableView().getItems();
        if (items == null || items.isEmpty())
            return;

        Callback cellFactory = col.getCellFactory();
        if (cellFactory == null)
            return;

        TableCell cell = (TableCell) cellFactory.call(col);
        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 TableColumn
        cell.getProperties().put("deferToParentPrefWidth", 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.getInsets().getLeft() + r.getInsets().getRight();
        }

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

        double desiredWidth = 0;

        // Check header
        Label headerLabel = (Label) header.lookup(".label");
        String headerText = headerLabel.getText();
        if (!headerLabel.getContentDisplay().equals(ContentDisplay.GRAPHIC_ONLY) && headerText != null)
        {
            Text text = new Text(headerLabel.getText());
            text.setFont(headerLabel.getFont());
            desiredWidth += text.getLayoutBounds().getWidth() + headerLabel.getLabelPadding().getLeft() + headerLabel.getLabelPadding().getRight();
        }

        Node headerGraphic = headerLabel.getGraphic();
        if((headerLabel.getContentDisplay().equals(ContentDisplay.LEFT) || headerLabel.getContentDisplay().equals(ContentDisplay.RIGHT)) && headerGraphic != null)
        {
            desiredWidth += headerGraphic.getLayoutBounds().getWidth();
        }

        // Handle minimum width calculations
        // Use a "w" because it is typically the widest character
        Text minText = new Text(StringUtils.repeat("W", Math.min(0, minWidth)));
        minText.setFont(headerLabel.getFont());

        // Check rows
        double minPxWidth = 0;
        for (int row = 0; row < rows; row++)
        {
            cell.updateTableColumn(col);
            cell.updateTableView(col.getTableView());
            cell.updateIndex(row);

            // Handle minimum width calculations
            // Just do this once
            if(row == 0)
            {
                String oldText = cell.getText();
                // Use a "w" because it is typically the widest character
                cell.setText(StringUtils.repeat("W", Math.max(0, minWidth)));

                header.getChildren().add(cell);
                cell.impl_processCSS(false);
                minPxWidth = cell.prefWidth(-1);
                header.getChildren().remove(cell);

                cell.setText(oldText);
            }

            if ((cell.getText() != null && !cell.getText().isEmpty()) || cell.getGraphic() != null)
            {
                header.getChildren().add(cell);
                cell.impl_processCSS(false);
                desiredWidth = Math.max(desiredWidth, cell.prefWidth(-1));
                desiredWidth = Math.max(desiredWidth, minPxWidth);
                header.getChildren().remove(cell);
            }
        }

        desiredWidth = desiredWidth + padding;

        if(maxWidth > 0)
        {
            desiredWidth = Math.min(maxWidth, desiredWidth);
        }

        col.impl_setWidth(desiredWidth);
    }
}

Keep in mind that this is "hacking" protected methods which could change at any time with a JavaFX update and break the method. I believe this already changes in JavaFX 8. This could also break depending upon how your application is signed an deployed.

It isn't pretty, but JavaFX has hidden this functionality so unless you want to re-implement the entire thing, this is the best approach I've found.

like image 41
robross0606 Avatar answered Sep 19 '22 23:09

robross0606