【问题标题】:How to select multiple rows in JavaFX ListView with mouse drag如何通过鼠标拖动在 JavaFX ListView 中选择多行
【发布时间】:2019-08-05 20:58:10
【问题描述】:

我是 JavaFX 的新手,但我似乎无法找到如何做到这一点。

我在 Vbox 中有一个 ListView,我用字符串的 ObservableList 填充。我已将 ListView 的 SelectionMode 设置为 MULTIPLE,这允许我在按住 Ctrl 或 Shift 键的同时选择多个项目。

我希望能够单击一行并向下拖动鼠标并选择多行,但我不知道该怎么做。我尝试了几次搜索,似乎只能找到拖放,这不是我需要的。

@FXML private ListView availableColumnList;

private ObservableList<String> availableColumns = FXCollections.<String>observableArrayList("One","Two","Three","Four");

availableColumnList.getItems().addAll(availableColumns);

availableColumnList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

【问题讨论】:

  • 我找不到任何允许这样做的内置 API。您可能需要结合使用listView.onMouseDragged()、MouseEvent 过滤器,并计算节点位置以确定要选择的项目。
  • 不支持(正如@Zephyr 已经提到的) - 可能需要大量工作来实现:鼠标事件对选择状态的控制在 CellBehaviorBase 中处理,行为是内部 api,存储在私有 final 字段中xxCellSkin - 更改此类行为的选项是调整行为的 inputMap(再次是内部 api,加上需要对该字段的反射访问)或完全编写自己的皮肤/行为堆栈。
  • 你可能不得不尝试 ListView 以外的东西; stackoverflow.com/questions/30481363/…
  • 感谢大家的帮助。如果我实现了这个,我会在这里发布我的解决方案。

标签: listview javafx


【解决方案1】:

如果您使用的是 JavaFX 10+,那么您可以扩展 ListViewSkin 并在那里添加功能。您需要 JavaFX 10 或更高版本的原因是因为那时 VirtualContainerBase 类(由 ListViewSkin 扩展)添加了 getVirtualFlow() 方法。然后,您可以使用动画 API,例如 AnimationTimer,通过 VirtualFlow#scrollPixels(double) 方法滚动 ListView。

下面是一个概念验证。它所做的只是在鼠标靠近ListView 的顶部(或左侧)或底部(或右侧)时自动滚动ListView。当鼠标进入一个单元格时,该项目被选中(粗略地)。如果您想在开始向相反方向拖动鼠标时取消选择项目,那么您需要自己实现。如果ListView 被隐藏或从场景中移除,您可能想要实现的另一件事是停止AnimationTimer。

注意:以下使用“完全按下-拖动-释放”手势。换句话说,MouseEvent 处理程序和MouseDragEvent 处理程序混合在一起。使用MouseDragEvents 的原因是因为它们可以传递到其他节点,而不仅仅是原始节点(与“简单的按下-拖动-释放”手势不同)。查看this documentation 了解更多信息。

Main.java

import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.stage.Stage;

public final class Main extends Application {

    @Override
    public void start(Stage primaryStage) {
        var listView = IntStream.range(0, 1000)
                .mapToObj(Integer::toString)
                .collect(Collectors.collectingAndThen(
                        Collectors.toCollection(FXCollections::observableArrayList),
                        ListView::new
                ));
        // Sets the custom skin. Can also be set via CSS.
        listView.setSkin(new CustomListViewSkin<>(listView));
        listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        primaryStage.setScene(new Scene(listView, 600, 400));
        primaryStage.show();
    }

}

CustomListViewSkin.java

import javafx.animation.AnimationTimer;
import javafx.geometry.Rectangle2D;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.skin.ListViewSkin;
import javafx.scene.input.MouseDragEvent;
import javafx.scene.input.MouseEvent;

public class CustomListViewSkin<T> extends ListViewSkin<T> {

    private static final double DISTANCE = 10;
    private static final double PERCENTAGE = 0.05;

    private AnimationTimer scrollAnimation = new AnimationTimer() {

        @Override
        public void handle(long now) {
            if (direction == -1) {
                getVirtualFlow().scrollPixels(-DISTANCE);
            } else if (direction == 1) {
                getVirtualFlow().scrollPixels(DISTANCE);
            }
        }

    };

    private Rectangle2D leftUpArea;
    private Rectangle2D rightDownArea;

    private int direction = 0;
    private int anchorIndex = -1;

    public CustomListViewSkin(final ListView<T> control) {
        super(control);
        final var flow = getVirtualFlow();
        final var factory = flow.getCellFactory();

        // decorate the actual cell factory
        flow.setCellFactory(vf -> {
            final var cell = factory.call(flow);

            // handle drag start
            cell.addEventHandler(MouseEvent.DRAG_DETECTED, event -> {
                if (control.getSelectionModel().getSelectionMode() == SelectionMode.MULTIPLE) {
                    event.consume();
                    cell.startFullDrag();
                    anchorIndex = cell.getIndex();
                }
            });

            // handle selecting items when the mouse-drag enters the cell
            cell.addEventHandler(MouseDragEvent.MOUSE_DRAG_ENTERED, event -> {
                event.consume();
                if (event.getGestureSource() != cell) {
                    final var model = control.getSelectionModel();
                    if (anchorIndex < cell.getIndex()) {
                        model.selectRange(anchorIndex, cell.getIndex() + 1);
                    } else {
                        model.selectRange(cell.getIndex(), anchorIndex + 1);
                    }
                }
            });

            return cell;
        });

        // handle the auto-scroll functionality
        flow.addEventHandler(MouseDragEvent.MOUSE_DRAG_OVER, event -> {
            event.consume();
            if (leftUpArea.contains(event.getX(), event.getY())) {
                direction = -1;
                scrollAnimation.start();
            } else if (rightDownArea.contains(event.getX(), event.getY())) {
                direction = 1;
                scrollAnimation.start();
            } else {
                direction = 0;
                scrollAnimation.stop();
            }
        });

        // stop the animation when the mouse exits the flow/list (desired?)
        flow.addEventHandler(MouseDragEvent.MOUSE_DRAG_EXITED, event -> {
            event.consume();
            scrollAnimation.stop();
        });

        // handle stopping the animation and reset the state when the mouse
        // is released. Added to VirtualFlow because it doesn't matter
        // which cell receives the event.
        flow.addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
            if (anchorIndex != -1) {
                event.consume();
                anchorIndex = -1;
                scrollAnimation.stop();
            }
        });

        updateAutoScrollAreas();
        registerChangeListener(control.orientationProperty(), obs -> updateAutoScrollAreas());
        registerChangeListener(flow.widthProperty(), obs -> updateAutoScrollAreas());
        registerChangeListener(flow.heightProperty(), obs -> updateAutoScrollAreas());
    }

    // computes the regions where the mouse needs to be
    // in order to start auto-scrolling. The regions depend
    // on the orientation of the ListView.
    private void updateAutoScrollAreas() {
        final var flow = getVirtualFlow();
        switch (getSkinnable().getOrientation()) {
            case HORIZONTAL:
                final double width = flow.getWidth() * PERCENTAGE;
                leftUpArea = new Rectangle2D(0, 0, width, flow.getHeight());
                rightDownArea = new Rectangle2D(flow.getWidth() - width, 0, width, flow.getHeight());
                break;
            case VERTICAL:
                final double height = flow.getHeight() * PERCENTAGE;
                leftUpArea = new Rectangle2D(0, 0, flow.getWidth(), height);
                rightDownArea = new Rectangle2D(0, flow.getHeight() - height, flow.getWidth(), height);
                break;
            default:
                throw new AssertionError();
        }
    }

    @Override
    public void dispose() {
        unregisterChangeListeners(getSkinnable().orientationProperty());
        unregisterChangeListeners(getVirtualFlow().widthProperty());
        unregisterChangeListeners(getVirtualFlow().heightProperty());
        super.dispose();

        scrollAnimation.stop();
        scrollAnimation = null;
    }
}

注意:作为mentioned by kleopatra,至少其中一些功能更适合行为类。然而,为了简单起见,我决定只使用现有的公共皮肤类(通过扩展它)。同样,以上只是一个概念验证。

【讨论】:

  • 非常感谢@Slaw。我们目前使用的是 JavaFX 8,但我会在我们最终升级时将其归档。
  • 我相信你可以在 JavaFX 8 中做类似的事情。由于该版本是预模块,内部代码很容易访问(不需要像 --add-exports 这样的东西)。通过查看源代码,您只需要扩展具有受保护的VirtualFlow 字段的com.sun.javafx.scene.control.skin.ListViewSkin(来自其超类)。但是,它不是VirtualFlow#scrollPixels(double),而是VirtualFlow#adjustPixels(double)。请注意,与 FX 9+ 相比,在 FX 8 中的皮肤类中有些事情是不同的。
  • 另一个区别是 FX 8 使用 VirtualFlow#setCreateCell(和相应的 getter)——没有属性。没有深入研究,但您可能需要更改“装饰”电池工厂的方式。
猜你喜欢
  • 2017-12-27
  • 2014-01-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-10-06
  • 1970-01-01
  • 2013-05-31
  • 1970-01-01
相关资源
最近更新 更多