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

Aud*_*sis 5 javafx

我有可以移动的节点,这些节点放置在位于 ScrollPane 上的窗格上。当我将节点拖动到滚动窗格之外时viewportBoundsvvalue应该会发生变化,以便该节点再次位于这些边界内。为了解决这个问题,我尝试使用这个问题的答案。

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

如果您想测试代码,请记住,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;
    }
}
Run Code Online (Sandbox Code Playgroud)

Sai*_*dem 4

回答你的问题:

如何保持光标在节点上的位置?

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

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");
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

为了解决光标到达应用程序/屏幕边界的另一个问题,我实现了一种在拖动时自动启动滚动的方法:一旦光标超出视口边界。当光标回到视口边界时,它的行为就像正常的拖动操作。

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

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

在此输入图像描述

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • “_您需要使用 AWT Robot 类来移动光标_” – 请注意,从版本 11 开始,就有了 [`javafx.scene.robot.Robot`](https://openjfx.io/javadoc/17/javafx.graphics/ javafx/scene/robot/Robot.html) 类。 (3认同)