如何在移动节点时更改 viewportBounds 后使光标停留在节点的边界内

How to make the cursor stay within the bounds of a node after changing viewportBounds while moving that node

我有可以移动的节点,这些节点放置在 ScrollPane.When 上的窗格上 我将一个节点拖到滚动窗格的 viewportBounds 之外, vvalue 应该改变,以便节点再次在这些范围内。为了解决这个问题,我尝试使用 this question.

的答案

我的问题是当节点再次在viewportBounds的边界内后,光标相对于节点移动,如果我想继续将节点移动到视口之外,经过几次迭代光标会移动太多以至于它会超出整个应用程序 window 并且会靠在屏幕边界上。如何保持光标在节点上的位置?

如果您想测试代码,请记住 viewport 边界的重构仅在您沿 Y 轴移动节点时发生。

public class NewFXMain extends Application {

    @Override
    public void start(Stage primaryStage) {
        AnchorPane root = new AnchorPane();

        ScrollPane scrollPane = new ScrollPane(root);
        root.setPrefSize(5000,5000);

        Scene scene = new Scene(scrollPane, 800, 600, Color.rgb(160, 160, 160));

        final int numNodes = 6; // number of nodes to add
        final double spacing = 30; // spacing between nodes

        // add numNodes instances of DraggableNode to the root pane
        for (int i = 0; i < numNodes; i++) {
            DraggableNode node = new DraggableNode(scrollPane);
            node.setPrefSize(98, 80);
            // define the style via css
            node.setStyle(
                    "-fx-background-color: #334488; "
                    + "-fx-text-fill: black; "
                    + "-fx-border-color: black;");

            node.setLayoutX(spacing * (i + 1) + node.getPrefWidth() * i);// position the node
            node.setLayoutY(spacing);
            root.getChildren().add(node); // add the node to the root pane 
        }

        // finally, show the stage
        primaryStage.setTitle("Draggable Node 01");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

class DraggableNode extends Pane {

    private double x = 0;
    private double y = 0;
    private double mousex = 0;
    private double mousey = 0;
    private Node view;
    private boolean dragging = false;
    private boolean moveToFront = true;

    private void ensureVisible(ScrollPane scrollPane) {
        Bounds viewport = scrollPane.getViewportBounds();
        double contentHeight = scrollPane.getContent().localToScene(scrollPane.getContent().getBoundsInLocal()).getHeight();
        double nodeMinY = this.localToScene(this.getBoundsInLocal()).getMinY();
        double nodeMaxY = this.localToScene(this.getBoundsInLocal()).getCenterY();

        double vValueDelta = 0;
        double vValueCurrent = scrollPane.getVvalue();

        if (nodeMaxY < 0) {
            // currently located above (remember, top left is (0,0))
            vValueDelta = (nodeMinY) / contentHeight;
            System.out.println("FIRST CASE DELTA: " + vValueDelta);
        } else if (nodeMinY > viewport.getHeight()) {
            // currently located below
            vValueDelta = ((nodeMinY) / contentHeight) / 5;
            System.out.println("SECOND CASE DELTA: " + vValueDelta);
        }
        scrollPane.setVvalue(vValueCurrent + vValueDelta);
    }

    public DraggableNode(ScrollPane pane) {
        init(pane);
    }

    private void init(ScrollPane scroll) {

        onMousePressedProperty().set(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {

                // record the current mouse X and Y position on Node
                mousex = event.getSceneX();
                mousey = event.getSceneY();

                x = getLayoutX();
                y = getLayoutY();

                if (isMoveToFront()) {
                    toFront();
                }
            }
        });

        //Event Listener for MouseDragged
        onMouseDraggedProperty().set(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                // Get the exact moved X and Y
                double offsetX = event.getSceneX() - mousex;
                double offsetY = event.getSceneY() - mousey;

                x += offsetX;
                y += offsetY;

                double scaledX = x;
                double scaledY = y;

                setLayoutX(scaledX);
                setLayoutY(scaledY);

                ensureVisible(scroll);

                dragging = true;

                // again set current Mouse x AND y position
                mousex = event.getSceneX();
                mousey = event.getSceneY();

                event.consume();
            }
        });

        onMouseClickedProperty().set(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {

                dragging = false;
            }
        });

    }

    public void setMoveToFront(boolean moveToFront) {
        this.moveToFront = moveToFront;
    }

    public boolean isMoveToFront() {
        return moveToFront;
    }
}

回答你的问题:

How do I maintain position of cursor on the node?

您需要使用 AWT Robot class 来移动光标。

private void movePointer(Node node){
    Bounds b = node.localToScreen(node.getLayoutBounds());
    try {
        final Robot robot = new Robot();
        robot.mouseMove((int) (b.getMinX()+(b.getWidth()/2)), (int) (b.getMinY()+(b.getHeight()/2)));
    } catch (final AWTException e) {
        System.out.println("Unable to poistion the pointer");
    }
}

上述方法会将光标定位在提供的节点的中心。设置vValue后即可调用上述方法。

话虽如此,从用户的角度来看,这是非常奇怪的行为

  • 让节点超出视口边界然后跳回 突然让它可见
  • 跳转光标

所以我尝试以一种方式实现,拖动的节点 永远不会 超出视口边界并且 vValue/hValue 根据拖动调整并维护节点在视口中可见。

为了解决光标到达 app/screen 边界的另一个问题,我实现了一种在拖动时自动开始滚动的方法:一旦光标超出视口 bounds.When,光标就会返回视口边界,它只是作为一个正常的拖动操作。

下面是具有所需更改的代码的快速演示。您可以根据自己的需要进行微调。

我在窗格中添加了背景以展示正在进行的拖动操作;)

import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Duration;
import javafx.util.Pair;

public class NewFXMainV1 extends Application {

    public static void main(final String[] args) {
        launch(args);
    }

    @Override
    public void start(final Stage primaryStage) {
        final AnchorPane root = new AnchorPane();
        // Adding a pattern to background to observe the automatic drag :)
        String bg1 = "linear-gradient(from 0% 0% to 1% 0% , repeat, #DDDDDD 50% , transparent 22% )";
        String bg2 = "linear-gradient(from 0% 0% to 0% 1% , repeat, transparent 50% , #DDDDDD 22% )";
        root.setStyle("-fx-background-color:" + bg1 + ", " + bg2 + ";");

        final ScrollPane scrollPane = new ScrollPane(root);
        root.setPrefSize(5000, 5000);

        final Scene scene = new Scene(scrollPane, 800, 600, Color.rgb(160, 160, 160));

        final int numNodes = 6; // number of nodes to add
        final double spacing = 30; // spacing between nodes

        // add numNodes instances of DraggableNode to the root pane
        for (int i = 0; i < numNodes; i++) {
            final DraggableNodeV1 node = new DraggableNodeV1(scrollPane);
            node.setPrefSize(98, 80);
            // define the style via css
            node
                    .setStyle(
                            "-fx-background-color: #334488; " + "-fx-text-fill: black; " + "-fx-border-color: black;");

            node.setLayoutX(spacing * (i + 1) + node.getPrefWidth() * i);// position the node
            node.setLayoutY(spacing);
            root.getChildren().add(node); // add the node to the root pane
        }

        // finally, show the stage
        primaryStage.setTitle("Draggable Node 01");
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

class DraggableNodeV1 extends Pane {

    private static Duration d = Duration.millis(26);
    private static double PX_JUMP = 10;
    private static double AUTOMATIC_OFFSET = 15;

    private double x = 0;
    private double y = 0;
    private double mouseX = 0;
    private double mouseY = 0;

    private Timeline xTimeline;
    private Timeline yTimeline;

    public DraggableNodeV1(final ScrollPane pane) {
        init(pane);
    }

    private void ensureVisible(final ScrollPane scrollPane) {
        final Pane dragNode = this;
        final Bounds viewport = scrollPane.getViewportBounds();
        double vpMinY = viewport.getMinY() * -1;
        double vpHeight = viewport.getHeight();
        double vpMinX = viewport.getMinX() * -1;
        double vpWidth = viewport.getWidth();
        final Bounds contentBounds = scrollPane.getContent().getLayoutBounds();
        final double layoutY = dragNode.getLayoutY();
        final double layoutX = dragNode.getLayoutX();

        final double visibleYInViewport = vpMinY + (vpHeight - dragNode.getHeight());
        double totalHeightToScroll = contentBounds.getHeight() - vpHeight;
        if (layoutY > visibleYInViewport) {
            double heightToScroll = vpMinY + (layoutY - visibleYInViewport);
            scrollPane.setVvalue(heightToScroll / totalHeightToScroll);
        } else if (layoutY < vpMinY) {
            double heightToScroll = layoutY;
            scrollPane.setVvalue(heightToScroll / totalHeightToScroll);
        }

        final double visibleXInViewport = vpMinX + (vpWidth - dragNode.getWidth());
        double totalWidthToScroll = contentBounds.getWidth() - vpWidth;
        if (layoutX > visibleXInViewport) {
            double widthToScroll = vpMinX + (layoutX - visibleXInViewport);
            scrollPane.setHvalue(widthToScroll / totalWidthToScroll);
        } else if (layoutX < vpMinX) {
            double widthToScroll = layoutX;
            scrollPane.setHvalue(widthToScroll / totalWidthToScroll);
        }
    }

    private void init(final ScrollPane scroll) {

        onMousePressedProperty().set(event -> {
            mouseX = event.getScreenX();
            mouseY = event.getScreenY();
            x = getLayoutX();
            y = getLayoutY();
        });

        // Event Listener for MouseDragged
        onMouseDraggedProperty().set(event -> {
            if (isMouseInDraggableViewPort(event, scroll)) {
                dragTheNode(event, scroll);
                clearTimelines();
            } else {
                Pair<Pair<Boolean, Boolean>, Pair<Boolean, Boolean>> auto = isMouseInAutomaticDrag(event, scroll);
                Pair<Boolean, Boolean> xAuto = auto.getKey();
                if (xAuto.getKey() || xAuto.getValue()) {
                    if (xAuto.getKey()) { // towards left
                        if (xTimeline == null) {
                            xTimeline = new Timeline(new KeyFrame(d, e -> {
                                if (getLayoutX() > 0) {
                                    setLayoutX(getLayoutX() - PX_JUMP);
                                    x = getLayoutX();
                                    mouseX = event.getScreenX();
                                    ensureVisible(scroll);
                                }
                            }));
                            xTimeline.setCycleCount(Animation.INDEFINITE);
                            xTimeline.play();
                        }
                    } else if (xAuto.getValue()) { // towards right
                        if (xTimeline == null) {
                            xTimeline = new Timeline(new KeyFrame(d, e -> {
                                final Bounds contentBounds = scroll.getContent().getLayoutBounds();
                                if (getLayoutX() < contentBounds.getWidth() - getWidth()) {
                                    setLayoutX(getLayoutX() + PX_JUMP);
                                    x = getLayoutX();
                                    mouseX = event.getScreenX();
                                    ensureVisible(scroll);
                                }
                            }));
                            xTimeline.setCycleCount(Animation.INDEFINITE);
                            xTimeline.play();
                        }
                    }
                } else {
                    stopXTimeline();
                }

                Pair<Boolean, Boolean> yAuto = auto.getValue();
                if (yAuto.getKey() || yAuto.getValue()) {
                    if (yAuto.getKey()) { // towards top
                        if (yTimeline == null) {
                            yTimeline = new Timeline(new KeyFrame(d, e -> {
                                if (getLayoutY() > 0) {
                                    setLayoutY(getLayoutY() - PX_JUMP);
                                    y = getLayoutY();
                                    mouseY = event.getScreenY();
                                    ensureVisible(scroll);
                                }
                            }));
                            yTimeline.setCycleCount(Animation.INDEFINITE);
                            yTimeline.play();
                        }
                    } else if (yAuto.getValue()) { // towards bottom
                        if (yTimeline == null) {
                            yTimeline = new Timeline(new KeyFrame(d, e -> {
                                final Bounds contentBounds = scroll.getContent().getLayoutBounds();
                                if (getLayoutY() < contentBounds.getHeight() - getHeight()) {
                                    setLayoutY(getLayoutY() + PX_JUMP);
                                    y = getLayoutY();
                                    mouseY = event.getScreenY();
                                    ensureVisible(scroll);
                                }
                            }));
                            yTimeline.setCycleCount(Animation.INDEFINITE);
                            yTimeline.play();
                        }
                    }
                } else {
                    stopYTimeline();
                }
            }
            event.consume();
        });

        onMouseReleasedProperty().set(event -> clearTimelines());
    }

    private void clearTimelines() {
        stopXTimeline();
        stopYTimeline();
    }

    private void stopXTimeline() {
        if (xTimeline != null) {
            xTimeline.stop();
        }
        xTimeline = null;
    }

    private void stopYTimeline() {
        if (yTimeline != null) {
            yTimeline.stop();
        }
        yTimeline = null;
    }

    private Pair<Pair<Boolean, Boolean>, Pair<Boolean, Boolean>> isMouseInAutomaticDrag(MouseEvent event, ScrollPane scrollPane) {
        Node viewport = scrollPane.lookup(".viewport");
        Bounds viewportSceneBounds = viewport.localToScene(viewport.getLayoutBounds());
        double eX = event.getSceneX();
        double eY = event.getSceneY();
        double vpMinX = viewportSceneBounds.getMinX();
        double vpMaxX = viewportSceneBounds.getMaxX();
        double vpMinY = viewportSceneBounds.getMinY();
        double vpMaxY = viewportSceneBounds.getMaxY();
        Pair<Boolean, Boolean> autoX = new Pair<>(eX <= vpMinX + AUTOMATIC_OFFSET, vpMaxX - AUTOMATIC_OFFSET <= eX);
        Pair<Boolean, Boolean> autoY = new Pair<>(eY <= vpMinY + AUTOMATIC_OFFSET, vpMaxY - AUTOMATIC_OFFSET <= eY);
        return new Pair<>(autoX, autoY);
    }

    private void dragTheNode(MouseEvent event, ScrollPane scroll) {
        final double offsetX = event.getScreenX() - mouseX;
        final double offsetY = event.getScreenY() - mouseY;
        final double tX = x + offsetX;
        final double tY = y + offsetY;

        Bounds contentBounds = scroll.getContent().getLayoutBounds();
        if (tX >= 0 && tX <= (contentBounds.getWidth() - getWidth())) {
            determineX(scroll, tX, event);
        } else if (tX < 0) {
            setLayoutX(0);
        } else {
            setLayoutX(contentBounds.getWidth() - getWidth());
        }

        if (tY >= 0 && tY <= (contentBounds.getHeight() - getHeight())) {
            determineY(scroll, tY, event);
        } else if (tY < 0) {
            setLayoutY(0);
        } else {
            setLayoutY(contentBounds.getHeight() - getHeight());
        }
        ensureVisible(scroll);
    }

    private boolean isMouseInDraggableViewPort(MouseEvent event, ScrollPane scrollPane) {
        Node viewport = scrollPane.lookup(".viewport");
        Bounds viewportSceneBounds = viewport.localToScene(viewport.getLayoutBounds());
        Bounds draggableBounds = new BoundingBox(viewportSceneBounds.getMinX() + AUTOMATIC_OFFSET,
                viewportSceneBounds.getMinY() + AUTOMATIC_OFFSET,
                viewportSceneBounds.getWidth() - (2 * AUTOMATIC_OFFSET),
                viewportSceneBounds.getHeight() - (2 * AUTOMATIC_OFFSET));
        return draggableBounds.contains(event.getSceneX(), event.getSceneY());
    }

    private void determineY(ScrollPane scrollPane, double tY, MouseEvent event) {
        final Bounds viewport = scrollPane.getViewportBounds();
        double vpMinY = viewport.getMinY() * -1;
        double vpHeight = viewport.getHeight();
        final double visibleYInViewport = vpMinY + (vpHeight - getHeight());
        if (tY >= vpMinY && tY <= visibleYInViewport) {
            setLayoutY(tY);
        } else if (tY < vpMinY) {
            setLayoutY(vpMinY);
        } else {
            setLayoutY(visibleYInViewport);
        }
    }

    private void determineX(ScrollPane scrollPane, double tX, MouseEvent event) {
        final Bounds viewport = scrollPane.getViewportBounds();
        double vpMinX = viewport.getMinX() * -1;
        double vpWidth = viewport.getWidth();
        final double visibleXInViewport = vpMinX + (vpWidth - getHeight());
        if (tX >= vpMinX && tX <= visibleXInViewport) {
            setLayoutX(tX);
        } else if (tX < vpMinX) {
            setLayoutX(vpMinX);
        } else {
            setLayoutX(visibleXInViewport);
        }
    }
}