Automatically control the last column in JavaFX TableView

Overview

I was having trouble controlling the column width of JavaFX TableView, so a memo of how to deal with it.

I tried to set the last column to automatic control so that the column would be filled to the full width of the table.

ColumnResizePolicy There is a mechanism called ColumnResizePolicy for controlling the column width of JavaFx TableView. This seems to be a mechanism that determines how to control the column width at the time of initial drawing and adjustment of the column width.

Two types are prepared by default

constant Overview
UNCONSTRAINED_RESIZE_POLICY No particular control, process column widths exactly as the user moves
CONSTRAINED_RESIZE_POLICY Equal column width when initial drawing, and evenly process other columns when expanding and contracting columns

If you need your own processing other than this, you need to implement it, and in reality it is a Callback class.

public final void setColumnResizePolicy(Callback<ResizeFeatures, Boolean> callback)

In this, it seems that it is realized by changing the width of each column and returning true if it succeeds.

What I personally wanted was automatic control of the width of the last column, which I wanted to achieve: --When initial drawing, when resizing the window, I want to fit the last column to the window width (pane) --I want to fit the last column to the window width (pane) even when double-clicking autofit.

I looked around the JavaFx TableView and looked for something I could do, but it didn't seem to be the default, so I gave up and implemented it. I managed to get the desired behavior. I haven't considered it in detail, but it doesn't seem to be a problem for checking the operation.

public class ResizePolicy {

    public static <S> void setLastColumnResizePolicy(TableView<S> tableView) {
        //Set resizing policy
        tableView.setColumnResizePolicy(prop -> {
            if (prop.getTable().getWidth() < 0) {
                return false;
            }
            resizeTargetColumn(prop);
            resizeLastColumn(prop);
            return true;
        });

        //Add resizing process after autofit when double-clicking
        addResizeBorderDoubleClickedEvent(tableView);
    }

    private static void resizeTargetColumn(TableView.ResizeFeatures<?> prop) {
        var targetColumn = prop.getColumn();
        var delta = prop.getDelta();

        if (delta != 0 && targetColumn.isResizable()) {
            var newWidth = targetColumn.getWidth() + delta;

            //Set the final width considering the maximum width and minimum width
            if (newWidth > targetColumn.getMaxWidth()) {
                targetColumn.setPrefWidth(targetColumn.getMaxWidth());
            } else if (newWidth < targetColumn.getMinWidth()) {
                targetColumn.setPrefWidth(targetColumn.getMinWidth());
            } else {
                targetColumn.setPrefWidth(newWidth);
            }
        }
    }

    private static void resizeLastColumn(TableView.ResizeFeatures<?> prop) {
        var table = prop.getTable();
        // -18.If you do not set 0, a scroll bar will appear. 18.0 is just decided by looking at the screen
        var tableContentWidth = table.getWidth() - 18.0;

        //Calculate the width of the last column from the total value other than the last column and the width of the table
        var columns = table.getVisibleLeafColumns();
        var columnsOfExcludedLastColumn = columns.stream().limit(columns.size() - 1);
        var widthsOfExcludedLastColumn = columnsOfExcludedLastColumn.map(TableColumnBase::getWidth);
        var lastColumnWidth = widthsOfExcludedLastColumn
                .reduce(tableContentWidth, (remainingWidth, columnWidth) -> remainingWidth - columnWidth);

        if (lastColumnWidth > 0) {
            var lastColumn = columns.get(columns.size() - 1);
            lastColumn.setPrefWidth(lastColumnWidth);
        }
    }

    private static <S> void addResizeBorderDoubleClickedEvent(TableView<S> tableView) {
        //Since skin is null when drawing the screen, register what you want to do as an event
        tableView.skinProperty().addListener((observable, oldValue, newValue) -> {
            var tableViewSkin = (TableViewSkin<?>) newValue;
            var tableViewSkinNodes = tableViewSkin.getChildren().stream();
            var tableHeaderRowNode = tableViewSkinNodes.filter(node -> node instanceof TableHeaderRow).findFirst();

            tableHeaderRowNode.ifPresent((node) -> {
                var tableHeaderRow = (TableHeaderRow) node;
                var tableColumnHeader = (NestedTableColumnHeader) tableHeaderRow.getChildren().get(1);

                tableColumnHeader.addEventFilter(MouseEvent.MOUSE_PRESSED, mouseEvent -> {
                    //For changing the column width, the square line is the actual event target
                    //And for double-click autofit
                    if (isRectangle(mouseEvent) && isDoubleClick(mouseEvent)) {

                        //Find the target column from the position of the clicked square line and the column width
                        var rectangle = (Rectangle) mouseEvent.getTarget();
                        var columnPosition = 0.0;
                        for (var column : tableView.getColumns()) {
                            columnPosition += column.getWidth();
                            if (rectangle.getLayoutX() < columnPosition) {
                                //If you execute the resizing policy at this timing,
                                //Since the width has not been changed by autofit, the actual execution will be postponed.
                                Platform.runLater(() -> {
                                    var columnResizePolicy = tableView.getColumnResizePolicy();
                                    columnResizePolicy.call(new TableView.ResizeFeatures<S>(tableView, column, -0.1));
                                });
                                break;
                            }
                        }
                    }
                });
            });
        });
    }

    private static boolean isRectangle(MouseEvent mouseEvent) {
        return mouseEvent.getTarget() instanceof Rectangle;
    }

    private static boolean isDoubleClick(MouseEvent mouseEvent) {
        return mouseEvent.getClickCount() == 2 && mouseEvent.isPrimaryButtonDown();
    }

}

Actually, I tried to make it as a class instead of static, but I got a warning due to a generic problem and thought that it would be possible to delete it with SuppressWarnings, so I made it a static method. .. ..

ResizeFeatures is a generics class, but the setColumnResizePolicy method handles it raw, so if you do the type declaration yourself, it will not be accepted with a type error. I feel like I get a warning if I don't type.

Why is this happening? .. ..

Recommended Posts

Automatically control the last column in JavaFX TableView
Organized memo in the head (Java --Control syntax)