JavaFX - Zoom relative to mouse position
Asked Answered
P

1

2

I can't get working zoom relative to mouse position in JavaFX. I've read and tried rewrite this and this, but it's not working. Can't scroll viewport using scrollPane.setViewportBounds(). I must use scrollPane.setHvalue() and scrollPane.setVvalue(), but can't get right recalculation to keep the same pixels under the mouse cursor. Current code keeps scrollbars positions:

ZoomHandler.java

import javafx.event.EventHandler;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Pane;

public class ZoomHandler implements EventHandler<ScrollEvent> {

    private final ScrollPane scrollPane;
    private final Pane canvas;    
    private final double minScale;
    private final double maxScale;

    public ZoomHandler(ScrollPane scrollPane, Pane canvas, double minScale, double maxScale) {
        this.scrollPane = scrollPane;
        this.canvas = canvas;
        this.minScale = minScale;
        this.maxScale = maxScale;
    }

    public ZoomHandler(ScrollPane scrollPane, Pane canvas) {
        this(scrollPane, canvas, 0.1, 10);
    }

    @Override
    public void handle(ScrollEvent e) {
        if (e.isControlDown()) {
            double actualScale = canvas.getScaleX();

            if (actualScale > maxScale || actualScale < minScale) {
                e.consume();
                return;
            }

            double hVal = scrollPane.getHvalue();
            double vVal = scrollPane.getVvalue();

            double scale, factor;           
            if (e.getDeltaY() > 0) {
                factor = 1.1;
            } else {
                factor = 0.9;
            }
            scale = actualScale * factor;

            scale = Math.min(scale, maxScale);
            scale = Math.max(scale, minScale);

            canvas.setScaleX(scale);
            canvas.setScaleY(scale);

            scrollPane.setHvalue(hVal);
            scrollPane.setVvalue(vVal);

            e.consume();
        }
    }

}

Main.java

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class Main extends Application  {     

    public static final int CANVAS_WIDTH = 1000;
    public static final int CANVAS_HEIGHT = 1000;

    private final Pane canvas = new Pane();
    private final ScrollPane scrollPane = new ScrollPane(); 
    private final ZoomHandler zoomHandler = new ZoomHandler(scrollPane, canvas);

    public static void main(String[] args) { launch(args); }
    @Override public void start(Stage primaryStage) {        
        canvas.setPrefWidth(CANVAS_WIDTH);
        canvas.setPrefHeight(CANVAS_HEIGHT);
        canvas.setOnScroll(zoomHandler);        

        scrollPane.setStyle("-fx-background-color: transparent; -fx-control-inner-background: transparent;");
        scrollPane.setContent(new Group(canvas));
        scrollPane.setPannable(true);
        scrollPane.setOnScroll(zoomHandler);

        addContent();

        Scene scene = new Scene(scrollPane, 800, 600);

        primaryStage.setTitle("Zoom");
        primaryStage.setScene(scene);
        primaryStage.show();

        scrollPane.setHvalue(0.5);  
        scrollPane.setVvalue(0.5);  
    }

    private void addContent() {
        Text hello = new Text(400, 500, "Hello");
        hello.setStyle("-fx-font-size: 20px; -fx-fill: red;");

        Text world = new Text(500, 600, "world!");
        world.setStyle("-fx-fill: blue;");

        Text text = new Text(200, 350, "How to zoom to this text when mouse is over?");
        text.setStyle("-fx-font-weight: bold;");

        Rectangle rect = new Rectangle(600, 450, 300, 200);
        rect.setFill(Color.GREEN);

        Circle circle = new Circle(250, 700, 100);
        circle.setFill(Color.YELLOW);

        canvas.getChildren().addAll(hello, world, text, rect, circle);
    }

}

Any ideas? Thanks in advance.

Pomerania answered 17/3, 2014 at 12:4 Comment(0)
T
1

I had the same problem and I did the following - I wouldn't call is a proper solution (rather a workaround), but it works mostly fine.

Basically, I do some calculations to retrieve the correct values of Hvalue and Vvalue. Moreover, I have to apply a compensation factor due to the fact that the ScrollPane will do not only the zooming but also a vertical translation of the image (due to standard behaviour of the ScrollPane).

Here you find the code.

/**
 * Pane containing an image that can be resized.
 */
public class SizeableImageView extends ScrollPane {
    /**
     * The zoom factor to be applied for each zoom event.
     *
     * (480th root of 2 means that 12 wheel turns of 40 will result in size factor 2.)
     */
    private static final double ZOOM_FACTOR = 1.0014450997779993488675056142818;

    /**
     * The zoom factor.
     */
    private final DoubleProperty zoomProperty = new SimpleDoubleProperty(1000);

    /**
     * The mouse X position.
     */
    private final DoubleProperty mouseXProperty = new SimpleDoubleProperty();

    /**
     * The mouse Y position.
     */
    private final DoubleProperty mouseYProperty = new SimpleDoubleProperty();

    /**
     * Constructor without initialization of image.
     */
    public SizeableImageView() {
        this(new ImageView());
    }

    /**
     * Constructor, initializing with an image view.
     *
     * @param imageView
     *            The ImageView to be displayed.
     */
    public SizeableImageView(final ImageView imageView) {
        setContent(imageView);

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

        setOnMouseMoved(new EventHandler<MouseEvent>() {
            @Override
            public void handle(final MouseEvent event) {
                mouseXProperty.set(event.getX());
                mouseYProperty.set(event.getY());
            }
        });

        addEventFilter(ScrollEvent.ANY, new EventHandler<ScrollEvent>() {
            @Override
            public void handle(final ScrollEvent event) {
                ImageView image = (ImageView) getContent();

                // Original size of the image.
                double sourceWidth = zoomProperty.get() * image.getImage().getWidth();
                double sourceHeight = zoomProperty.get() * image.getImage().getHeight();

                zoomProperty.set(zoomProperty.get() * Math.pow(ZOOM_FACTOR, event.getDeltaY()));

                // Old values of the scrollbars.
                double oldHvalue = getHvalue();
                double oldVvalue = getVvalue();

                // Image pixels outside the visible area which need to be scrolled.
                double preScrollXFactor = Math.max(0, sourceWidth - getWidth());
                double preScrollYFactor = Math.max(0, sourceHeight - getHeight());

                // Relative position of the mouse in the image.
                double mouseXPosition = (mouseXProperty.get() + preScrollXFactor * oldHvalue) / sourceWidth;
                double mouseYPosition = (mouseYProperty.get() + preScrollYFactor * oldVvalue) / sourceHeight;

                // Target size of the image.
                double targetWidth = zoomProperty.get() * image.getImage().getWidth();
                double targetHeight = zoomProperty.get() * image.getImage().getHeight();

                // Image pixels outside the visible area which need to be scrolled.
                double postScrollXFactor = Math.max(0, targetWidth - getWidth());
                double postScrollYFactor = Math.max(0, targetHeight - getHeight());

                // Correction applied to compensate the vertical scrolling done by ScrollPane
                double verticalCorrection = (postScrollYFactor / sourceHeight) * event.getDeltaY();

                // New scrollbar positions keeping the mouse position.
                double newHvalue = ((mouseXPosition * targetWidth) - mouseXProperty.get()) / postScrollXFactor;
                double newVvalue =
                        ((mouseYPosition * targetHeight) - mouseYProperty.get() + verticalCorrection)
                                / postScrollYFactor;

                image.setFitWidth(targetWidth);
                image.setFitHeight(targetHeight);

                setHvalue(newHvalue);
                setVvalue(newVvalue);
            }
        });

        addEventFilter(ZoomEvent.ANY, new EventHandler<ZoomEvent>() {
            @Override
            public void handle(final ZoomEvent event) {
                zoomProperty.set(zoomProperty.get() * event.getZoomFactor());

                ImageView image = (ImageView) getContent();
                image.setFitWidth(zoomProperty.get() * image.getImage().getWidth());
                image.setFitHeight(zoomProperty.get() * image.getImage().getHeight());
            }
        });

    }

    /**
     * Set the image view displayed by this class.
     *
     * @param imageView
     *            The ImageView.
     */
    public final void setImageView(final ImageView imageView) {
        setContent(imageView);
        zoomProperty.set(Math.min(imageView.getFitWidth() / imageView.getImage().getWidth(), imageView.getFitHeight()
                / imageView.getImage().getHeight()));
    }
}
Teeter answered 3/1, 2015 at 21:6 Comment(1)
Sorry to resurrect an old question, but can you explain the reasoning behind the calculation of the relative position of the mouse in the image? So mouseYProperty.get() gets the position of the mouse in the vewport? And then you offset that by the amount of offscreen space proportional to the Vvalue position? Why do you divide it?Bargeman

© 2022 - 2024 — McMap. All rights reserved.