内部带有可拖动节点的 JavaFX 平移和缩放

JavaFX Pan and Zoom with Draggable Nodes Inside

我有一个简单的 JavaFX 平移和缩放应用程序,如下所示。平移和缩放功能效果很好,但我也希望能够拖放圆形节点。我遇到的问题是滚动窗格首先获取所有鼠标事件,因此我无法将拖放分配给圆圈。是否可以有一个 draggable/zoomable 滚动窗格并且还可以将一个节点拖到窗格内?

Screenshot

这是我正在使用的代码:

package sample;

import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;

public class Main extends Application {

    private ScrollPane scrollPane = new ScrollPane();
    private final DoubleProperty zoomProperty = new SimpleDoubleProperty(1.0d);
    private final DoubleProperty deltaY = new SimpleDoubleProperty(0.0d);
    private final Group group = new Group();
    ImageView bigImageView = null;
    PanAndZoomPane panAndZoomPane = null;
    Pane featuresPane = new Pane();

    @Override
    public void start(Stage primaryStage) throws Exception{

        bigImageView = new ImageView();

        StackPane bigStackpane = new StackPane();

        bigStackpane.getChildren().add(bigImageView);
        bigStackpane.getChildren().add(featuresPane);
        featuresPane.toFront();

        featuresPane.setOpacity(.8);
        Circle circle = new Circle();
        circle.setCenterX(200);
        circle.setCenterY(200);
        circle.setRadius(100);
        circle.setFill(Color.RED);
        circle.setOnMouseClicked(e -> {
            System.out.println("circle clicked");
        });
        featuresPane.getChildren().add(circle);

        scrollPane.setPannable(true);
        scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
        scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);

        group.getChildren().add(bigImageView);
        group.getChildren().add(bigStackpane);

        panAndZoomPane = new PanAndZoomPane();
        zoomProperty.bind(panAndZoomPane.myScale);
        deltaY.bind(panAndZoomPane.deltaY);
        panAndZoomPane.getChildren().add(group);

        SceneGestures sceneGestures = new SceneGestures(panAndZoomPane);

        scrollPane.setContent(panAndZoomPane);
        panAndZoomPane.toBack();

        scrollPane.addEventFilter( MouseEvent.MOUSE_CLICKED, sceneGestures.getOnMouseClickedEventHandler());
        scrollPane.addEventFilter( MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
        scrollPane.addEventFilter( MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
        scrollPane.addEventFilter( ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());

        AnchorPane bigImageAnchorPane = new AnchorPane();

        bigImageAnchorPane.getChildren().add(scrollPane);

        Image image = new Image("https://i.imgur.com/8p1XBag.jpg");
        bigImageView.setImage(image);

        bigImageAnchorPane.setTopAnchor(scrollPane, 1.0d);
        bigImageAnchorPane.setRightAnchor(scrollPane, 1.0d);
        bigImageAnchorPane.setBottomAnchor(scrollPane, 1.0d);
        bigImageAnchorPane.setLeftAnchor(scrollPane, 1.0d);

        BorderPane root = new BorderPane(bigImageAnchorPane);
        Label label = new Label("Pan and Zoom Test");
        root.setTop(label);

        Scene scene = new Scene(root, 1000, 1000);
        primaryStage.setScene(scene);
        primaryStage.show();
    }


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

    class PanAndZoomPane extends Pane {

        public static final double DEFAULT_DELTA = 1.5d; //1.3d
        DoubleProperty myScale = new SimpleDoubleProperty(1.0);
        public DoubleProperty deltaY = new SimpleDoubleProperty(0.0);
        private Timeline timeline;


        public PanAndZoomPane() {

            this.timeline = new Timeline(30);//60

            // add scale transform
            scaleXProperty().bind(myScale);
            scaleYProperty().bind(myScale);
        }


        public double getScale() {
            return myScale.get();
        }

        public void setScale( double scale) {
            myScale.set(scale);
        }

        public void setPivot( double x, double y, double scale) {
            // note: pivot value must be untransformed, i. e. without scaling
            // timeline that scales and moves the node
            timeline.getKeyFrames().clear();
            timeline.getKeyFrames().addAll(
                    new KeyFrame(Duration.millis(100), new KeyValue(translateXProperty(), getTranslateX() - x)), //200
                    new KeyFrame(Duration.millis(100), new KeyValue(translateYProperty(), getTranslateY() - y)), //200
                    new KeyFrame(Duration.millis(100), new KeyValue(myScale, scale)) //200
            );
            timeline.play();

        }

        public double getDeltaY() {
            return deltaY.get();
        }
        public void setDeltaY( double dY) {
            deltaY.set(dY);
        }
    }


    /**
     * Mouse drag context used for scene and nodes.
     */
    class DragContext {

        double mouseAnchorX;
        double mouseAnchorY;

        double translateAnchorX;
        double translateAnchorY;

    }

    /**
     * Listeners for making the scene's canvas draggable and zoomable
     */
    public class SceneGestures {

        private DragContext sceneDragContext = new DragContext();

        PanAndZoomPane panAndZoomPane;

        public SceneGestures( PanAndZoomPane canvas) {
            this.panAndZoomPane = canvas;
        }

        public EventHandler<MouseEvent> getOnMouseClickedEventHandler() {
            return onMouseClickedEventHandler;
        }

        public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
            return onMousePressedEventHandler;
        }

        public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
            return onMouseDraggedEventHandler;
        }

        public EventHandler<ScrollEvent> getOnScrollEventHandler() {
            return onScrollEventHandler;
        }

        private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {

            public void handle(MouseEvent event) {

                sceneDragContext.mouseAnchorX = event.getX();
                sceneDragContext.mouseAnchorY = event.getY();

                sceneDragContext.translateAnchorX = panAndZoomPane.getTranslateX();
                sceneDragContext.translateAnchorY = panAndZoomPane.getTranslateY();

            }

        };

        private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
            public void handle(MouseEvent event) {

                panAndZoomPane.setTranslateX(sceneDragContext.translateAnchorX + event.getX() - sceneDragContext.mouseAnchorX);
                panAndZoomPane.setTranslateY(sceneDragContext.translateAnchorY + event.getY() - sceneDragContext.mouseAnchorY);

                event.consume();
            }
        };

        /**
         * Mouse wheel handler: zoom to pivot point
         */
        private EventHandler<ScrollEvent> onScrollEventHandler = new EventHandler<ScrollEvent>() {

            @Override
            public void handle(ScrollEvent event) {

                double delta = PanAndZoomPane.DEFAULT_DELTA;

                double scale = panAndZoomPane.getScale(); // currently we only use Y, same value is used for X
                double oldScale = scale;

                panAndZoomPane.setDeltaY(event.getDeltaY());
                if (panAndZoomPane.deltaY.get() < 0) {
                    scale /= delta;
                } else {
                    scale *= delta;
                }

                double f = (scale / oldScale)-1;

                double dx = (event.getX() - (panAndZoomPane.getBoundsInParent().getWidth()/2 + panAndZoomPane.getBoundsInParent().getMinX()));
                double dy = (event.getY() - (panAndZoomPane.getBoundsInParent().getHeight()/2 + panAndZoomPane.getBoundsInParent().getMinY()));

                panAndZoomPane.setPivot(f*dx, f*dy, scale);

                event.consume();

            }
        };

        /**
         * Mouse click handler
         */
        private EventHandler<MouseEvent> onMouseClickedEventHandler = new EventHandler<MouseEvent>() {

            @Override
            public void handle(MouseEvent event) {
                if (event.getButton().equals(MouseButton.PRIMARY)) {
                    if (event.getClickCount() == 2) {
                        System.out.println("Image Layer Double Clicked...");
                    }else{
                        System.out.println("Image Layer Clicked...");
                    }
                }
            }
        };
    }
}

您的代码正在通过事件 过滤器 添加行为。这些过滤器在事件 捕获 阶段被调用,这意味着它们在事件到达您的圈子之前被调用。您应该努力通过在事件 冒泡 阶段调用的事件 处理程序 来实现您的行为。然后您可以使用事件来防止它们到达祖先,允许您在没有 scrolling/panning 滚动窗格内容的情况下拖动您的圆圈。有关事件处理和传播的更多信息,请查看 this tutorial

这是一个概念验证,它将缩放处理添加到滚动窗格的内容中,仍然让您绕着一个圆圈拖动:

import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

public class Main extends Application {

  @Override
  public void start(Stage primaryStage) {
    // using your example image
    ImageView imageView = new ImageView("https://i.imgur.com/8p1XBag.jpg");

    Circle circle = new Circle(100, 100, 25, Color.FIREBRICK);
    circle.setOnMousePressed(
        e -> {
          // prevent pannable ScrollPane from changing cursor on drag-detected (implementation
          // detail)
          e.setDragDetect(false);
          Point2D offset =
              new Point2D(e.getX() - circle.getCenterX(), e.getY() - circle.getCenterY());
          circle.setUserData(offset);
          e.consume(); // prevents MouseEvent from reaching ScrollPane
        });
    circle.setOnMouseDragged(
        e -> {
          // prevent pannable ScrollPane from changing cursor on drag-detected (implementation
          // detail)
          e.setDragDetect(false);
          Point2D offset = (Point2D) circle.getUserData();
          circle.setCenterX(e.getX() - offset.getX());
          circle.setCenterY(e.getY() - offset.getY());
          e.consume(); // prevents MouseEvent from reaching ScrollPane
        });

    // the zoom-able content of the ScrollPane
    Group group = new Group(imageView, circle);

    // wrap Group in another Group since it's the former that's scaled and
    // Groups only take transformations of their **children** into account (not themselves)
    StackPane content = new StackPane(new Group(group));
    content.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
    // due to later configuration, the StackPane will always cover the entire viewport
    content.setOnScroll(
        e -> {
          if (e.isShortcutDown() && e.getDeltaY() != 0) {
            if (e.getDeltaY() < 0) {
              group.setScaleX(Math.max(group.getScaleX() - 0.1, 0.5));
            } else {
              group.setScaleX(Math.min(group.getScaleX() + 0.1, 5.0));
            }
            group.setScaleY(group.getScaleX());
            e.consume(); // prevents ScrollEvent from reaching ScrollPane
          }
        });

    // use StackPane (or some other resizable node) as content since Group is not 
    // resizable. Note StackPane will center content if smaller than viewport.
    ScrollPane scrollPane = new ScrollPane(content);
    scrollPane.setVbarPolicy(ScrollBarPolicy.NEVER);
    scrollPane.setHbarPolicy(ScrollBarPolicy.NEVER);
    scrollPane.setPannable(true);
    // ensure StackPane content always has at least the same dimensions as the viewport
    scrollPane.setFitToWidth(true);
    scrollPane.setFitToHeight(true);

    primaryStage.setScene(new Scene(scrollPane, 1000, 650));
    primaryStage.show();
  }
}

请注意,这并不完全复制您示例的行为。它不使用动画,也不缩放轴心点。但希望它能帮助你在申请中取得进展。