LoginSignup
1
2

More than 3 years have passed since last update.

JavaFXのTableViewで最後の列を自動制御にする

Last updated at Posted at 2020-05-28

概要

JavaFXのTableViewの列幅の制御で、困ったのでそれの対応方法のメモ。

最後の列を自動制御にして、テーブル幅いっぱいに列が埋まるようにしてみた。

ColumnResizePolicy

JavaFxのTableViewの列幅の制御については、ColumnResizePolicyという仕組みがある。

これは、初期描画時や列幅の調整時に、どのように列幅の制御をするかを決めている仕組みになるようだ。

デフォルトで用意されているのは2種類

定数 概要
UNCONSTRAINED_RESIZE_POLICY 特に制御は無く、ユーザーが動かした通りに列幅を処理する
CONSTRAINED_RESIZE_POLICY 初期描画時などは均等の列幅、列の伸縮時はそれに合わせて、他の列を均等に処理する 

これ以外で独自の処理が必要な場合は実装する必要があり、実態としてはCallbackクラスとなる。

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

この中で、各列の幅の変更を行い、成功した場合はtrueを返すことで実現しているようだ。

個人的に欲しかったのは、最後の列の幅の自動制御で、以下を実現したい

- 初期描画時、ウィンドウのリサイズ時は、最後の列をウィンドウ幅(ペイン)に合わせたい
- ダブルクリックのオートフィット時にも、最後の列をウィンドウ幅(ペイン)に合わせたい

JavaFxのTableView周りを覗いてどうにか出来ないか探したが、デフォルトではないようで、あきらめて実装。
何とか目的の動作になった。細かい制御や考慮はしてないけど、動作確認としては問題なさそう。

public class ResizePolicy {

    public static <S> void setLastColumnResizePolicy(TableView<S> tableView) {
        // リサイズポリシーをセットする
        tableView.setColumnResizePolicy(prop -> {
            if (prop.getTable().getWidth() < 0) {
                return false;
            }
            resizeTargetColumn(prop);
            resizeLastColumn(prop);
            return true;
        });

        // ダブルクリック時のオートフィット後のリサイズ処理を追加する
        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;

            // 最大幅、最小幅を考慮して、最終的な幅を設定する
            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.0 をしないとスクロールバーが出る。18.0は画面を見て決めただけ
        var tableContentWidth = table.getWidth() - 18.0;

        // 最後のカラム以外の合計値とテーブルの幅から、最後のカラムの幅を算出する
        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) {
        // 画面描画時にはskinがnullなので、イベントとしてやりたいことを登録しておく
        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 -> {
                    // カラム幅の変更については、四角形の線が実際のイベント対象になる
                    // かつダブルクリックのオートフィットを対象とする
                    if (isRectangle(mouseEvent) && isDoubleClick(mouseEvent)) {

                        // クリックされた四角形の線の位置と、カラム幅から対象となるカラムを探し出す
                        var rectangle = (Rectangle) mouseEvent.getTarget();
                        var columnPosition = 0.0;
                        for (var column : tableView.getColumns()) {
                            columnPosition += column.getWidth();
                            if (rectangle.getLayoutX() < columnPosition) {
                                // このタイミングでリサイズポリシーを実行すると、
                                // オートフィットによる幅の変更前なので、実際の実行は後回しにする
                                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();
    }

}

本当は、staticではなく、クラスとして作ろうとしたが、ジェネリクスの問題で警告が出て、SuppressWarningsで消すのもなんだかなと思って、staticなメソッドにしてしまった。。。

ResizeFeaturesはジェネリクスのクラスなのに、setColumnResizePolicyメソッドでは、生で扱ってるので、自分で型宣言をちゃんとやると、型エラーで受け入れてくれない。

型を付けないと警告が出る感じです。

なんでこうしたんだろうか。。。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2