当 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());
}
}
重现步骤:
- 运行以上SSCCE
- 尝试将
Column 0
拖到 Column 29
之后
我正在寻找功能齐全的解决方案(如果有的话)。
变通并非不可能。你可以从这样的东西开始,虽然它是一个非常粗糙的实现,但我相信原则上它可以被改进为合理的东西:
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;
}
}
}
最终结果(效果出奇的好):
在 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());
}
}
重现步骤:
- 运行以上SSCCE
- 尝试将
Column 0
拖到Column 29
之后
我正在寻找功能齐全的解决方案(如果有的话)。
变通并非不可能。你可以从这样的东西开始,虽然它是一个非常粗糙的实现,但我相信原则上它可以被改进为合理的东西:
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;
}
}
}
最终结果(效果出奇的好):