当 TableView 中有 dragging/reordering 列时水平滚动

Scroll horizontally when dragging/reordering columns in the TableView

在 JavaFX 的 TableView(和 TreeTableView)中,当存在水平滚动条时,很难使用拖放对列重新排序,因为 table 不会自动滚动想要将列拖动到当前不可见的位置(离开滚动窗格视口)。

我注意到已经有关于此的错误(增强)报告:

...但由于它已经有一段时间没有得到解决,我想知道是否有任何其他方法可以使用当前的 API.

实现相同的行为

有SSCCE:

public class TableViewColumnReorderDragSSCCE extends Application {

    public static final int NUMBER_OF_COLUMNS = 30;
    public static final int MAX_WINDOW_WIDTH = 480;

    @Override
    public void start(Stage stage) {
        stage.setScene(new Scene(createTable()));
        stage.show();
        stage.setMaxWidth(MAX_WINDOW_WIDTH);
    }

    private TableView<List<String>> createTable() {
        final TableView<List<String>> tableView = new TableView<>();
        initColumns(tableView);
        return tableView;
    }

    private void initColumns(TableView<List<String>> tableView) {
        for (int i=0; i<NUMBER_OF_COLUMNS; i++) {
            tableView.getColumns().add(new TableColumn<>("Column " + i));
        }
        tableView.getItems().add(Collections.emptyList());
    }
}

重现步骤:

  1. 运行以上SSCCE
  2. 尝试将 Column 0 拖到 Column 29
  3. 之后

我正在寻找功能齐全的解决方案(如果有的话)。

变通并非不可能。你可以从这样的东西开始,虽然它是一个非常粗糙的实现,但我相信原则上它可以被改进为合理的东西:

    tableView.setOnMouseExited(me -> {
        if (me.isPrimaryButtonDown()) { // must be dragging
            Bounds tvBounds = tableView.getBoundsInLocal();
            double x = me.getX();
            if (x < tvBounds.getMinX()) {
                // Scroll to the left
                tableView.scrollToColumnIndex(0);
            } else if (x > tvBounds.getMaxX()) {
                // Scroll to the right
                tableView.scrollToColumnIndex(tableView.getColumns().size()-1);
            }
        }
    });

在正确的实现中,您可能必须绕过节点层次结构并找到 table 列的宽度并确定下一个视图外的列是什么,以便您可以滚动到确切的位置右栏。记住你这样做的时候,如果用户继续拖到 table 之外你可以再做一次,但不要太快。

编辑:根据您的自我回答,这是我的看法。我稍微压缩了您的代码并使其在 JavaFX 8.0 上运行:

static class TableHeaderScroller implements EventHandler<MouseEvent> {
    private TableView tv;
    private Pane header;
    private ScrollBar scrollBar;
    private double lastX;

    public static void install(TableView tv) {
        TableHeaderScroller ths = new TableHeaderScroller(tv);
        tv.skinProperty().addListener(ths::skinListener);
    }
    
    private TableHeaderScroller(TableView tv) {
        this.tv = tv;
    }

    private void skinListener(ObservableValue<? extends Skin<?>> observable, Skin<?> oldValue, Skin<?> newValue) {
        if (header != null) {
            header.removeEventFilter(MouseEvent.MOUSE_DRAGGED, this);
        }
        header = (Pane) tv.lookup("TableHeaderRow");
        if (header != null) {
            tv.lookupAll(".scroll-bar").stream().map(ScrollBar.class::cast)
                    .filter(sb -> sb.getOrientation() == Orientation.HORIZONTAL)
                    .findFirst().ifPresent( sb -> {
                        TableHeaderScroller.this.scrollBar = sb;
                        header.addEventFilter(MouseEvent.MOUSE_DRAGGED, TableHeaderScroller.this);
                    });
        }
    }

    @Override
    public void handle(MouseEvent event) {
        double x = event.getX();
        double sign = Math.signum(x - lastX);
        lastX = x;
        int dir = x < 0 ? -1 : x > header.getWidth() ? 1 : 0;
        if (dir != 0 && dir == sign) {
            if (dir < 0) {
                scrollBar.decrement();
            } else {
                scrollBar.increment();
            }
        }
    }
}

由于没有提供完整的解决方案,我想出了一个自己的解决方案。我引入了一个 (ColumnsOrderingEnhancer) 实现,它将通过自动滚动(需要时)增强 table 视图列的重新排序。

用法(使用上面 SSCCE 中定义的 table 视图):

// Enhance table view columns reordering
final ColumnsOrderingEnhancer<List<String>> columnsOrderingEnhancer = new ColumnsOrderingEnhancer<>(tableView);
columnsOrderingEnhancer.init();

ColumnsOrderingEnhancer 实施:

public class ColumnsOrderingEnhancer<T> {

    private final TableView<T> tableView;

    public ColumnsOrderingEnhancer(TableView<T> tableView) {
        this.tableView = tableView;
    }

    public void init() {
        tableView.skinProperty().addListener((observable, oldSkin, newSkin) -> {

            // This can be done only when skin is ready
            final TableHeaderRow header = (TableHeaderRow) tableView.lookup("TableHeaderRow");
            final MouseDraggingDirectionHelper mouseDraggingDirectionHelper = new MouseDraggingDirectionHelper(header);
            final ScrollBar horizontalScrollBar = getTableViewHorizontalScrollbar();

            // This is the most important part which is responsible for scrolling table during the column dragging out of the viewport.
            header.addEventFilter(MouseEvent.MOUSE_DRAGGED, event -> {
                final double totalHeaderWidth = header.getWidth();
                final double xMousePosition = event.getX();
                final MouseDraggingDirectionHelper.Direction direction = mouseDraggingDirectionHelper.getLastDirection();
                maybeChangeScrollBarPosition(horizontalScrollBar, totalHeaderWidth, xMousePosition, direction);
            });

        });
    }

    private void maybeChangeScrollBarPosition(ScrollBar horizontalScrollBar, double totalHeaderWidth, double xMousePosition, MouseDraggingDirectionHelper.Direction direction) {
        if (xMousePosition > totalHeaderWidth && direction == RIGHT) {
            horizontalScrollBar.increment();
        }
        else if (xMousePosition < 0 && direction == LEFT) {
            horizontalScrollBar.decrement();
        }
    }

    private ScrollBar getTableViewHorizontalScrollbar() {
        Set<Node> scrollBars = tableView.lookupAll(".scroll-bar");
        final Optional<Node> horizontalScrollBar =
                  scrollBars.stream().filter(node -> ((ScrollBar) node).getOrientation().equals(Orientation.HORIZONTAL)).findAny();
        try {
            return (ScrollBar) horizontalScrollBar.get();
        }
        catch (NoSuchElementException e) {
            return null;
        }
    }

    /**
     * A simple class responsible for determining horizontal direction of the mouse during dragging phase.
     */
    static class MouseDraggingDirectionHelper {

        private double xLastMousePosition = -1;
        private Direction direction = null;

        MouseDraggingDirectionHelper(Node node) {
            // Event filters that are determining when scrollbar needs to be incremented/decremented
            node.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> xLastMousePosition = event.getX());
            node.addEventFilter(MouseEvent.MOUSE_DRAGGED, event -> {
                direction = ((event.getX() - xLastMousePosition > 0) ? RIGHT : LEFT);
                xLastMousePosition = event.getX();
            });
        }

        enum Direction {
            LEFT,
            RIGHT
        }

        public Direction getLastDirection() {
            return direction;
        }
    }
    
}

最终结果(效果出奇的好):