JavaFX Pan and Zoom с перетаскиваемыми узлами внутри - PullRequest
1 голос
/ 13 апреля 2020

У меня есть простое приложение JavaFX для панорамирования и масштабирования, как показано ниже. Функциональные возможности панорамирования и масштабирования работают отлично, но я также хотел бы иметь возможность перетаскивать круговой узел. У меня проблема в том, что сначала панель прокрутки получает все события мыши, поэтому я не могу назначить перетаскивание только на круг. Возможно ли иметь перетаскиваемую / масштабируемую полосу прокрутки, а также иметь возможность перетаскивать узел внутри панели?

Снимок экрана

Здесь используется код, который я использую :

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...");
                    }
                }
            }
        };
    }
}

1 Ответ

0 голосов
/ 14 апреля 2020

Ваш код добавляет поведение через событие filters . Эти фильтры вызываются во время фазы захвата , что означает, что они вызываются до того, как события достигнут вашего круга. Вы должны стремиться реализовать свое поведение с помощью события handlers , которое вызывается во время фазы bubbling . Затем вы можете использовать события, чтобы предотвратить их попадание к предкам, что позволяет перетаскивать ваш круг без прокрутки / панорамирования содержимого панели прокрутки. Для получения дополнительной информации об обработке и распространении событий, ознакомьтесь с этим руководством .

Вот доказательство концепции , которое добавляет обработку масштабирования к панели прокрутки содержание и все же давайте перетащим вокруг круга:

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();
  }
}

Обратите внимание, что это точно не повторяет поведение вашего примера. Он не использует анимации и не увеличивает масштаб точки поворота. Но, надеюсь, это поможет вам двигаться вперед в вашем приложении.

...