JavaFX drag 2 nodes simultaneously - javafx

I have 2 Nodes, 1 Circle and 1 rectangle. Both of them are drag and droppable.
Is there a way to combine them, so that I can drag and drop one of them and both are moved?
I already tried to create a group and add both nodes as children (this didnt work for me). Here is an example code, the interesting part is the class ZoomAndScrollApplication at the bottom. It creates a circle and a rectangle (and 3 labels) all of them are draggable but i want to connect the rectangle and the circle (that i can drag them both by only clicking one of them):
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.Node;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
class PannableCanvas extends Pane {
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public PannableCanvas() {
setPrefSize(600, 600);
setStyle("-fx-background-color: lightgrey; -fx-border-color: blue;");
// add scale transform
scaleXProperty().bind(myScale);
scaleYProperty().bind(myScale);
}
/**
* Add a grid to the canvas, send it to back
*/
public void addGrid() {
double w = getBoundsInLocal().getWidth();
double h = getBoundsInLocal().getHeight();
// add grid
Canvas grid = new Canvas(w, h);
// don't catch mouse events
grid.setMouseTransparent(true);
GraphicsContext gc = grid.getGraphicsContext2D();
gc.setStroke(Color.GRAY);
gc.setLineWidth(1);
// draw grid lines
double offset = 50;
for( double i=offset; i < w; i+=offset) {
gc.strokeLine( i, 0, i, h);
gc.strokeLine( 0, i, w, i);
}
getChildren().add( grid);
grid.toBack();
}
public double getScale() {
return myScale.get();
}
public void setScale( double scale) {
myScale.set(scale);
}
public void setPivot( double x, double y) {
setTranslateX(getTranslateX()-x);
setTranslateY(getTranslateY()-y);
}
}
/**
* Mouse drag context used for scene and nodes.
*/
class DragContext {
double mouseAnchorX;
double mouseAnchorY;
double translateAnchorX;
double translateAnchorY;
}
/**
* Listeners for making the nodes draggable via left mouse button. Considers if parent is zoomed.
*/
class NodeGestures {
private DragContext nodeDragContext = new DragContext();
PannableCanvas canvas;
public NodeGestures( PannableCanvas canvas) {
this.canvas = canvas;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// left mouse button => dragging
if( !event.isPrimaryButtonDown())
return;
nodeDragContext.mouseAnchorX = event.getSceneX();
nodeDragContext.mouseAnchorY = event.getSceneY();
Node node = (Node) event.getSource();
nodeDragContext.translateAnchorX = node.getTranslateX();
nodeDragContext.translateAnchorY = node.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// left mouse button => dragging
if( !event.isPrimaryButtonDown())
return;
double scale = canvas.getScale();
Node node = (Node) event.getSource();
node.setTranslateX(nodeDragContext.translateAnchorX + (( event.getSceneX() - nodeDragContext.mouseAnchorX) / scale));
node.setTranslateY(nodeDragContext.translateAnchorY + (( event.getSceneY() - nodeDragContext.mouseAnchorY) / scale));
event.consume();
}
};
}
/**
* Listeners for making the scene's canvas draggable and zoomable
*/
class SceneGestures {
private static final double MAX_SCALE = 10.0d;
private static final double MIN_SCALE = .1d;
private DragContext sceneDragContext = new DragContext();
PannableCanvas canvas;
public SceneGestures( PannableCanvas canvas) {
this.canvas = canvas;
}
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) {
// right mouse button => panning
if( !event.isSecondaryButtonDown())
return;
sceneDragContext.mouseAnchorX = event.getSceneX();
sceneDragContext.mouseAnchorY = event.getSceneY();
sceneDragContext.translateAnchorX = canvas.getTranslateX();
sceneDragContext.translateAnchorY = canvas.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// right mouse button => panning
if( !event.isSecondaryButtonDown())
return;
canvas.setTranslateX(sceneDragContext.translateAnchorX + event.getSceneX() - sceneDragContext.mouseAnchorX);
canvas.setTranslateY(sceneDragContext.translateAnchorY + event.getSceneY() - 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 = 1.2;
double scale = canvas.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
if (event.getDeltaY() < 0)
scale /= delta;
else
scale *= delta;
scale = clamp( scale, MIN_SCALE, MAX_SCALE);
double f = (scale / oldScale)-1;
double dx = (event.getSceneX() - (canvas.getBoundsInParent().getWidth()/2 + canvas.getBoundsInParent().getMinX()));
double dy = (event.getSceneY() - (canvas.getBoundsInParent().getHeight()/2 + canvas.getBoundsInParent().getMinY()));
canvas.setScale( scale);
// note: pivot value must be untransformed, i. e. without scaling
canvas.setPivot(f*dx, f*dy);
event.consume();
}
};
public static double clamp( double value, double min, double max) {
if( Double.compare(value, min) < 0)
return min;
if( Double.compare(value, max) > 0)
return max;
return value;
}
}
/**
* An application with a zoomable and pannable canvas.
*/
public class ZoomAndScrollApplication extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Group group = new Group();
// create canvas
PannableCanvas canvas = new PannableCanvas();
// we don't want the canvas on the top/left in this example => just
// translate it a bit
canvas.setTranslateX(100);
canvas.setTranslateY(100);
// create sample nodes which can be dragged
NodeGestures nodeGestures = new NodeGestures( canvas);
Label label1 = new Label("Draggable node 1");
label1.setTranslateX(10);
label1.setTranslateY(10);
label1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Label label2 = new Label("Draggable node 2");
label2.setTranslateX(100);
label2.setTranslateY(100);
label2.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label2.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Label label3 = new Label("Draggable node 3");
label3.setTranslateX(200);
label3.setTranslateY(200);
label3.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label3.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Circle circle1 = new Circle( 300, 300, 50);
circle1.setStroke(Color.ORANGE);
circle1.setFill(Color.ORANGE.deriveColor(1, 1, 1, 0.5));
circle1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
circle1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Rectangle rect1 = new Rectangle(100,100);
rect1.setTranslateX(450);
rect1.setTranslateY(450);
rect1.setStroke(Color.BLUE);
rect1.setFill(Color.BLUE.deriveColor(1, 1, 1, 0.5));
rect1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
rect1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
canvas.getChildren().addAll(label1, label2, label3, circle1, rect1);
group.getChildren().add(canvas);
// create scene which can be dragged and zoomed
Scene scene = new Scene(group, 1024, 768);
SceneGestures sceneGestures = new SceneGestures(canvas);
scene.addEventFilter( MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scene.addEventFilter( MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scene.addEventFilter( ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
stage.setScene(scene);
stage.show();
canvas.addGrid();
}
}

You told us using a Group does not work. This simply isn't true and only a small change in code is required to move Circle and Rectangle at the same time using a Group:
Circle circle1 = new Circle(300, 300, 50);
circle1.setStroke(Color.ORANGE);
circle1.setFill(Color.ORANGE.deriveColor(1, 1, 1, 0.5));
// circle1.addEventFilter(MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
// circle1.addEventFilter(MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Rectangle rect1 = new Rectangle(100, 100);
rect1.setTranslateX(450);
rect1.setTranslateY(450);
rect1.setStroke(Color.BLUE);
rect1.setFill(Color.BLUE.deriveColor(1, 1, 1, 0.5));
// rect1.addEventFilter(MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
// rect1.addEventFilter(MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Group g = new Group(rect1, circle1);
g.addEventFilter(MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
g.addEventFilter(MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
canvas.getChildren().addAll(label1, label2, label3, g);

Related

Zooming and panning image in scrollpane

I have a problem with zooming and panning image in ScrollPane. So far I have code like this:
Image image = imageView.getImage();
scrollPane.setPrefViewportWidth(0d);
scrollPane.setPrefViewportHeight(0d);
imageView.setFitWidth(0d);
imageView.setFitHeight(0d);
Bounds viewportBounds = scrollPane.getViewportBounds();
boolean vertical = image.getWidth() > image.getHeight();
if (imageView.getRotate() == 90 || imageView.getRotate() == 270d) {
vertical = !vertical;
}
imageView.setPreserveRatio(true);
double propX = viewportBounds.getWidth() / image.getWidth();
double propY = viewportBounds.getHeight() / image.getHeight();
boolean xLead = !(propX > propY);
imageView.setScaleX((xLead) ? propX : propY);
imageView.setScaleY((xLead) ? propX : propY);
scrollPane.setContent(imageView);
scrollPane.setPannable(true);
scrollPane.setHvalue(scrollPane.getHmin() + (scrollPane.getHmax() - scrollPane.getHmin()) / 2); // center the scroll contents.
scrollPane.setVvalue(scrollPane.getVmin() + (scrollPane.getVmax() - scrollPane.getVmin()) / 2);
zoom(imageView);
private void zoom(ImageView imagePannable) {
imagePannable.setOnScroll(
new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double zoomFactor = 1.20;
double deltaY = event.getDeltaY();
if (deltaY < 0) {
zoomFactor = 0.80;
}
imagePannable.setScaleX(imagePannable.getScaleX() * zoomFactor);
imagePannable.setScaleY(imagePannable.getScaleY() * zoomFactor);
event.consume();
}
});
}
What I want to do is align image relative to mouse pointer not to center of image everytime.
I also have a problem with large images (like maps which are 8*A4 size for example). When I zooming this maps pannable function stop working. What is wrong with this code? Thanks for helps!
Several people (including me) have had this same question. I got my answer here.
In the interest of clarity, here is a working example of a panning & zooming pane using a rectangle as the zoomed node. I have implemented this in a slightly more complex way with an ImageView.
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.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ZoomAndPanExample 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();
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage primaryStage) {
scrollPane.setPannable(true);
scrollPane.setHbarPolicy(ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollBarPolicy.NEVER);
AnchorPane.setTopAnchor(scrollPane, 10.0d);
AnchorPane.setRightAnchor(scrollPane, 10.0d);
AnchorPane.setBottomAnchor(scrollPane, 10.0d);
AnchorPane.setLeftAnchor(scrollPane, 10.0d);
AnchorPane root = new AnchorPane();
Rectangle rect = new Rectangle(80, 60);
rect.setStroke(Color.NAVY);
rect.setFill(Color.NAVY);
rect.setStrokeType(StrokeType.INSIDE);
group.getChildren().add(rect);
// create canvas
PanAndZoomPane 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());
root.getChildren().add(scrollPane);
Scene scene = new Scene(root, 600, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
class PanAndZoomPane extends Pane {
public static final double DEFAULT_DELTA = 1.3d;
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public DoubleProperty deltaY = new SimpleDoubleProperty(0.0);
private Timeline timeline;
public PanAndZoomPane() {
this.timeline = new Timeline(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(200), new KeyValue(translateXProperty(), getTranslateX() - x)),
new KeyFrame(Duration.millis(200), new KeyValue(translateYProperty(), getTranslateY() - y)),
new KeyFrame(Duration.millis(200), new KeyValue(myScale, scale))
);
timeline.play();
}
public void fitWidth () {
double scale = getParent().getLayoutBounds().getMaxX()/getLayoutBounds().getMaxX();
double oldScale = getScale();
double f = scale - oldScale;
double dx = getTranslateX() - getBoundsInParent().getMinX() - getBoundsInParent().getWidth()/2;
double dy = getTranslateY() - getBoundsInParent().getMinY() - getBoundsInParent().getHeight()/2;
double newX = f*dx + getBoundsInParent().getMinX();
double newY = f*dy + getBoundsInParent().getMinY();
setPivot(newX, newY, scale);
}
public void resetZoom () {
double scale = 1.0d;
double x = getTranslateX();
double y = getTranslateY();
setPivot(x, y, scale);
}
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) {
panAndZoomPane.resetZoom();
}
}
if (event.getButton().equals(MouseButton.SECONDARY)) {
if (event.getClickCount() == 2) {
panAndZoomPane.fitWidth();
}
}
}
};
}
}

JavaFX8 - drag a pane within a ScrollPane

Base on this comment https://stackoverflow.com/a/29530135/1387524 I added a ScrollPane to use scrolling when the canvas is moved outside of the visible area.
When I drag the canvas to the right or the bottom the scrollbars are displayed. But when I drag to the left or to the top nothing happens.
What am I missing?
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.Node;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
class PannableCanvas extends Pane {
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public PannableCanvas() {
setPrefSize(600, 600);
setStyle("-fx-background-color: lightgrey; -fx-border-color: blue;");
// add scale transform
scaleXProperty().bind(myScale);
scaleYProperty().bind(myScale);
}
/**
* Add a grid to the canvas, send it to back
*/
public void addGrid() {
double w = getBoundsInLocal().getWidth();
double h = getBoundsInLocal().getHeight();
// add grid
Canvas grid = new Canvas(w, h);
// don't catch mouse events
grid.setMouseTransparent(true);
GraphicsContext gc = grid.getGraphicsContext2D();
gc.setStroke(Color.GRAY);
gc.setLineWidth(1);
// draw grid lines
double offset = 50;
for( double i=offset; i < w; i+=offset) {
gc.strokeLine( i, 0, i, h);
gc.strokeLine( 0, i, w, i);
}
getChildren().add( grid);
grid.toBack();
}
public double getScale() {
return myScale.get();
}
public void setScale( double scale) {
myScale.set(scale);
}
public void setPivot( double x, double y) {
setTranslateX(getTranslateX()-x);
setTranslateY(getTranslateY()-y);
}
}
/**
* Mouse drag context used for scene and nodes.
*/
class DragContext {
double mouseAnchorX;
double mouseAnchorY;
double translateAnchorX;
double translateAnchorY;
}
/**
* Listeners for making the nodes draggable via left mouse button. Considers if parent is zoomed.
*/
class NodeGestures {
private DragContext nodeDragContext = new DragContext();
PannableCanvas canvas;
public NodeGestures( PannableCanvas canvas) {
this.canvas = canvas;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// left mouse button => dragging
if( !event.isPrimaryButtonDown())
return;
nodeDragContext.mouseAnchorX = event.getSceneX();
nodeDragContext.mouseAnchorY = event.getSceneY();
Node node = (Node) event.getSource();
nodeDragContext.translateAnchorX = node.getTranslateX();
nodeDragContext.translateAnchorY = node.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// left mouse button => dragging
if( !event.isPrimaryButtonDown())
return;
double scale = canvas.getScale();
Node node = (Node) event.getSource();
node.setTranslateX(nodeDragContext.translateAnchorX + (( event.getSceneX() - nodeDragContext.mouseAnchorX) / scale));
node.setTranslateY(nodeDragContext.translateAnchorY + (( event.getSceneY() - nodeDragContext.mouseAnchorY) / scale));
event.consume();
}
};
}
/**
* Listeners for making the scene's canvas draggable and zoomable
*/
class SceneGestures {
private static final double MAX_SCALE = 10.0d;
private static final double MIN_SCALE = .1d;
private DragContext sceneDragContext = new DragContext();
PannableCanvas canvas;
public SceneGestures( PannableCanvas canvas) {
this.canvas = canvas;
}
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) {
// right mouse button => panning
if( !event.isSecondaryButtonDown())
return;
sceneDragContext.mouseAnchorX = event.getSceneX();
sceneDragContext.mouseAnchorY = event.getSceneY();
sceneDragContext.translateAnchorX = canvas.getTranslateX();
sceneDragContext.translateAnchorY = canvas.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// right mouse button => panning
if( !event.isSecondaryButtonDown())
return;
canvas.setTranslateX(sceneDragContext.translateAnchorX + event.getSceneX() - sceneDragContext.mouseAnchorX);
canvas.setTranslateY(sceneDragContext.translateAnchorY + event.getSceneY() - 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 = 1.2;
double scale = canvas.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
if (event.getDeltaY() < 0)
scale /= delta;
else
scale *= delta;
scale = clamp( scale, MIN_SCALE, MAX_SCALE);
double f = (scale / oldScale)-1;
double dx = (event.getSceneX() - (canvas.getBoundsInParent().getWidth()/2 + canvas.getBoundsInParent().getMinX()));
double dy = (event.getSceneY() - (canvas.getBoundsInParent().getHeight()/2 + canvas.getBoundsInParent().getMinY()));
canvas.setScale( scale);
// note: pivot value must be untransformed, i. e. without scaling
canvas.setPivot(f*dx, f*dy);
event.consume();
}
};
public static double clamp( double value, double min, double max) {
if( Double.compare(value, min) < 0)
return min;
if( Double.compare(value, max) > 0)
return max;
return value;
}
}
/**
* An application with a zoomable and pannable canvas.
*/
public class ZoomAndScrollApplication extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Group group = new Group();
// create canvas
PannableCanvas canvas = new PannableCanvas();
// we don't want the canvas on the top/left in this example => just
// translate it a bit
canvas.setTranslateX(100);
canvas.setTranslateY(100);
// create sample nodes which can be dragged
NodeGestures nodeGestures = new NodeGestures( canvas);
Label label1 = new Label("Draggable node 1");
label1.setTranslateX(10);
label1.setTranslateY(10);
label1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Label label2 = new Label("Draggable node 2");
label2.setTranslateX(100);
label2.setTranslateY(100);
label2.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label2.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Label label3 = new Label("Draggable node 3");
label3.setTranslateX(200);
label3.setTranslateY(200);
label3.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label3.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Circle circle1 = new Circle( 300, 300, 50);
circle1.setStroke(Color.ORANGE);
circle1.setFill(Color.ORANGE.deriveColor(1, 1, 1, 0.5));
circle1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
circle1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Rectangle rect1 = new Rectangle(100,100);
rect1.setTranslateX(450);
rect1.setTranslateY(450);
rect1.setStroke(Color.BLUE);
rect1.setFill(Color.BLUE.deriveColor(1, 1, 1, 0.5));
rect1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
rect1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
canvas.getChildren().addAll(label1, label2, label3, circle1, rect1);
group.getChildren().add(canvas);
Pane pane = new Pane(group);
ScrollPane scrollPane = new ScrollPane(pane);
// create scene which can be dragged and zoomed
Scene scene = new Scene(scrollPane, 1024, 768);
SceneGestures sceneGestures = new SceneGestures(canvas);
scene.addEventFilter( MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scene.addEventFilter( MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scene.addEventFilter( ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
stage.setScene(scene);
stage.show();
canvas.addGrid();
}
}
Create a class ZoomableScrollPane and pass the pane to ZoomableScrollPane(pane) in ZoomAndScrollApplication.
Image : Zoom feature along with scroll bar on the side
Pane pane = new Pane(group);
ZoomableScrollPane zs = new ZoomableScrollPane(pane);
// create scene which can be dragged and zoomed
Scene scene = new Scene(zs, 1024, 768);
/
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
public class ZoomableScrollPane extends ScrollPane {
private double scaleValue = 0.7;
private double zoomIntensity = 0.04;
private Node target;
private Node zoomNode;
public ZoomableScrollPane(Node target) {
super();
this.target = target;
this.zoomNode = new Group(target);
setContent(outerNode(zoomNode));
setPannable(true);
setHbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
setFitToHeight(true); //center
setFitToWidth(true); //center
updateScale();
}
private Node outerNode(Node node) {
Node outerNode = centeredNode(node);
outerNode.setOnScroll(e -> {
e.consume();
onScroll(e.getTextDeltaY(), new Point2D(e.getX(), e.getY()));
});
return outerNode;
}
private Node centeredNode(Node node) {
VBox vBox = new VBox(node);
vBox.setAlignment(Pos.CENTER);
return vBox;
}
private void updateScale() {
target.setScaleX(scaleValue);
target.setScaleY(scaleValue);
}
private void onScroll(double wheelDelta, Point2D mousePoint) {
double zoomFactor = Math.exp(wheelDelta * zoomIntensity);
Bounds innerBounds = zoomNode.getLayoutBounds();
Bounds viewportBounds = getViewportBounds();
// calculate pixel offsets from [0, 1] range
double valX = this.getHvalue() * (innerBounds.getWidth() - viewportBounds.getWidth());
double valY = this.getVvalue() * (innerBounds.getHeight() - viewportBounds.getHeight());
scaleValue = scaleValue * zoomFactor;
updateScale();
this.layout(); // refresh ScrollPane scroll positions & target bounds
// convert target coordinates to zoomTarget coordinates
Point2D posInZoomTarget = target.parentToLocal(zoomNode.parentToLocal(mousePoint));
// calculate adjustment of scroll position (pixels)
Point2D adjustment = target.getLocalToParentTransform().deltaTransform(posInZoomTarget.multiply(zoomFactor - 1));
// convert back to [0, 1] range
// (too large/small values are automatically corrected by ScrollPane)
Bounds updatedInnerBounds = zoomNode.getBoundsInLocal();
this.setHvalue((valX + adjustment.getX()) / (updatedInnerBounds.getWidth() - viewportBounds.getWidth()));
this.setVvalue((valY + adjustment.getY()) / (updatedInnerBounds.getHeight() - viewportBounds.getHeight()));
}
}

Implement dragging multiple selected nodes

I have a class which extends javafx.scene.Node, say DraggableNode.
I have written the event-handlers for dragging for any such DraggableNode .
Class DraggableNode extends Node
{
...
onMouseDraggedProperty().set(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
offsetX = dragStartPoitionX - event.getSceneX();
offsetY = dragStartPoitionY - event.getSceneY();
setLayoutX (event.getSceneX());
setLayoutY (event.getSceneY());
...
}
}
}
This event-handler works fine for dragging this node individually.
Next, I require to select multiple such "nodes" and, dragging of one of the selected node should change the (x,y) co-ordinates of all the selected nodes by "offsetX" & "offsetY".
Selection algorithm is also implemented(in a Class extending Pane in which these nodes are added as children). But, what I need is to somehow trigger the drag event handlers of the other selected nodes so that the final output would look like a multi-drag.
First you create a selection model, say a Set<Node>. Whenever you add a Node to your selection, you add it to the selection model.
When you drag a node, you simply change the position of all of the other nodes of the selection model in your event handler as well.
As simple as that.
Here's code which also supports rubberband selection, shift & ctrl keypress during selection, etc:
NodeSelection.java
import java.util.Arrays;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeLineCap;
import javafx.stage.Stage;
public class NodeSelection extends Application {
public static Image image = new Image("http://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Siberischer_tiger_de_edit02.jpg/320px-Siberischer_tiger_de_edit02.jpg");
// public Image image = new Image( getClass().getResource( "tiger.jpg").toExternalForm());
SelectionModel selectionModel = new SelectionModel();
DragMouseGestures dragMouseGestures = new DragMouseGestures();
static Random rnd = new Random();
#Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
pane.setStyle("-fx-background-color:white");
new RubberBandSelection( pane);
double width = 200;
double height = 160;
double padding = 20;
for( int row=0; row < 4; row++) {
for( int col=0; col < 4; col++) {
Selectable selectable = new Selectable( width, height);
selectable.relocate( padding * (col+1) + width * col, padding * (row + 1) + height * row);
pane.getChildren().add(selectable);
dragMouseGestures.makeDraggable(selectable);
}
}
Label infoLabel = new Label( "Drag on scene for Rubberband Selection. Shift+Click to add to selection, CTRL+Click to toggle selection. Drag selected nodes for multi-dragging.");
pane.getChildren().add( infoLabel);
Scene scene = new Scene( pane, 1600, 900);
scene.getStylesheets().add( getClass().getResource("application.css").toExternalForm());
primaryStage.setScene( scene);
primaryStage.show();
}
private class Selectable extends Region {
ImageView view;
public Selectable( double width, double height) {
view = new ImageView( image);
view.setFitWidth(width);
view.setFitHeight(height);
getChildren().add( view);
this.setPrefSize(width, height);
}
}
private class SelectionModel {
Set<Node> selection = new HashSet<>();
public void add( Node node) {
if( !node.getStyleClass().contains("highlight")) {
node.getStyleClass().add( "highlight");
}
selection.add( node);
}
public void remove( Node node) {
node.getStyleClass().remove( "highlight");
selection.remove( node);
}
public void clear() {
while( !selection.isEmpty()) {
remove( selection.iterator().next());
}
}
public boolean contains( Node node) {
return selection.contains(node);
}
public int size() {
return selection.size();
}
public void log() {
System.out.println( "Items in model: " + Arrays.asList( selection.toArray()));
}
}
private class DragMouseGestures {
final DragContext dragContext = new DragContext();
private boolean enabled = false;
public void makeDraggable(final Node node) {
node.setOnMousePressed(onMousePressedEventHandler);
node.setOnMouseDragged(onMouseDraggedEventHandler);
node.setOnMouseReleased(onMouseReleasedEventHandler);
}
EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
// don't do anything if the user is in the process of adding to the selection model
if( event.isControlDown() || event.isShiftDown())
return;
Node node = (Node) event.getSource();
dragContext.x = node.getTranslateX() - event.getSceneX();
dragContext.y = node.getTranslateY() - event.getSceneY();
// clear the model if the current node isn't in the selection => new selection
if( !selectionModel.contains(node)) {
selectionModel.clear();
selectionModel.add( node);
}
// flag that the mouse released handler should consume the event, so it won't bubble up to the pane which has a rubberband selection mouse released handler
enabled = true;
// prevent rubberband selection handler
event.consume();
}
};
EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if( !enabled)
return;
// all in selection
for( Node node: selectionModel.selection) {
node.setTranslateX( dragContext.x + event.getSceneX());
node.setTranslateY( dragContext.y + event.getSceneY());
}
}
};
EventHandler<MouseEvent> onMouseReleasedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
// prevent rubberband selection handler
if( enabled) {
// set node's layout position to current position,remove translate coordinates
for( Node node: selectionModel.selection) {
fixPosition(node);
}
enabled = false;
event.consume();
}
}
};
/**
* Set node's layout position to current position, remove translate coordinates.
* #param node
*/
private void fixPosition( Node node) {
double x = node.getTranslateX();
double y = node.getTranslateY();
node.relocate(node.getLayoutX() + x, node.getLayoutY() + y);
node.setTranslateX(0);
node.setTranslateY(0);
}
class DragContext {
double x;
double y;
}
}
private class RubberBandSelection {
final DragContext dragContext = new DragContext();
Rectangle rect;
Pane group;
boolean enabled = false;
public RubberBandSelection( Pane group) {
this.group = group;
rect = new Rectangle( 0,0,0,0);
rect.setStroke(Color.BLUE);
rect.setStrokeWidth(1);
rect.setStrokeLineCap(StrokeLineCap.ROUND);
rect.setFill(Color.LIGHTBLUE.deriveColor(0, 1.2, 1, 0.6));
group.addEventHandler(MouseEvent.MOUSE_PRESSED, onMousePressedEventHandler);
group.addEventHandler(MouseEvent.MOUSE_DRAGGED, onMouseDraggedEventHandler);
group.addEventHandler(MouseEvent.MOUSE_RELEASED, onMouseReleasedEventHandler);
}
EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
// simple flag to prevent multiple handling of this event or we'd get an exception because rect is already on the scene
// eg if you drag with left mouse button and while doing that click the right mouse button
if( enabled)
return;
dragContext.mouseAnchorX = event.getSceneX();
dragContext.mouseAnchorY = event.getSceneY();
rect.setX(dragContext.mouseAnchorX);
rect.setY(dragContext.mouseAnchorY);
rect.setWidth(0);
rect.setHeight(0);
group.getChildren().add( rect);
event.consume();
enabled = true;
}
};
EventHandler<MouseEvent> onMouseReleasedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if( !event.isShiftDown() && !event.isControlDown()) {
selectionModel.clear();
}
for( Node node: group.getChildren()) {
if( node instanceof Selectable) {
if( node.getBoundsInParent().intersects( rect.getBoundsInParent())) {
if( event.isShiftDown()) {
selectionModel.add( node);
} else if( event.isControlDown()) {
if( selectionModel.contains( node)) {
selectionModel.remove( node);
} else {
selectionModel.add( node);
}
} else {
selectionModel.add( node);
}
}
}
}
selectionModel.log();
rect.setX(0);
rect.setY(0);
rect.setWidth(0);
rect.setHeight(0);
group.getChildren().remove( rect);
event.consume();
enabled = false;
}
};
EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
double offsetX = event.getSceneX() - dragContext.mouseAnchorX;
double offsetY = event.getSceneY() - dragContext.mouseAnchorY;
if( offsetX > 0)
rect.setWidth( offsetX);
else {
rect.setX(event.getSceneX());
rect.setWidth(dragContext.mouseAnchorX - rect.getX());
}
if( offsetY > 0) {
rect.setHeight( offsetY);
} else {
rect.setY(event.getSceneY());
rect.setHeight(dragContext.mouseAnchorY - rect.getY());
}
event.consume();
}
};
private final class DragContext {
public double mouseAnchorX;
public double mouseAnchorY;
}
}
public static void main(String[] args) {
launch(args);
}
}
application.css
.highlight {
-fx-effect: dropshadow(three-pass-box, red, 4, 4, 0, 0);
}
Screenshot:

JavaFX 8 - Zooming Relative to Mouse Pointer

I have been trying to use this example to learn about zooming and scaling. It does a great job of demonstrating scaling.
I've been trying to figure out how to change it so that rather than simply zooming on the current center of the view, it will zoom in on the position of the mouse pointer. I have failed. I'm not sure which values to use to do the math. I've found some examples in other languages, but none for JavaFX.
I had the same question here on StackOverflow. The following code is a modified version of the question/answer there.
You can use
right mouse button = panning
left mouse button = dragging of the nodes
scroll wheel = zoom in on mouse cursor location
The relevant part for you is the onScrollEventHandler.
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.Node;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
class PannableCanvas extends Pane {
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public PannableCanvas() {
setPrefSize(600, 600);
setStyle("-fx-background-color: lightgrey; -fx-border-color: blue;");
// add scale transform
scaleXProperty().bind(myScale);
scaleYProperty().bind(myScale);
}
/**
* Add a grid to the canvas, send it to back
*/
public void addGrid() {
double w = getBoundsInLocal().getWidth();
double h = getBoundsInLocal().getHeight();
// add grid
Canvas grid = new Canvas(w, h);
// don't catch mouse events
grid.setMouseTransparent(true);
GraphicsContext gc = grid.getGraphicsContext2D();
gc.setStroke(Color.GRAY);
gc.setLineWidth(1);
// draw grid lines
double offset = 50;
for( double i=offset; i < w; i+=offset) {
gc.strokeLine( i, 0, i, h);
gc.strokeLine( 0, i, w, i);
}
getChildren().add( grid);
grid.toBack();
}
public double getScale() {
return myScale.get();
}
public void setScale( double scale) {
myScale.set(scale);
}
public void setPivot( double x, double y) {
setTranslateX(getTranslateX()-x);
setTranslateY(getTranslateY()-y);
}
}
/**
* Mouse drag context used for scene and nodes.
*/
class DragContext {
double mouseAnchorX;
double mouseAnchorY;
double translateAnchorX;
double translateAnchorY;
}
/**
* Listeners for making the nodes draggable via left mouse button. Considers if parent is zoomed.
*/
class NodeGestures {
private DragContext nodeDragContext = new DragContext();
PannableCanvas canvas;
public NodeGestures( PannableCanvas canvas) {
this.canvas = canvas;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// left mouse button => dragging
if( !event.isPrimaryButtonDown())
return;
nodeDragContext.mouseAnchorX = event.getSceneX();
nodeDragContext.mouseAnchorY = event.getSceneY();
Node node = (Node) event.getSource();
nodeDragContext.translateAnchorX = node.getTranslateX();
nodeDragContext.translateAnchorY = node.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// left mouse button => dragging
if( !event.isPrimaryButtonDown())
return;
double scale = canvas.getScale();
Node node = (Node) event.getSource();
node.setTranslateX(nodeDragContext.translateAnchorX + (( event.getSceneX() - nodeDragContext.mouseAnchorX) / scale));
node.setTranslateY(nodeDragContext.translateAnchorY + (( event.getSceneY() - nodeDragContext.mouseAnchorY) / scale));
event.consume();
}
};
}
/**
* Listeners for making the scene's canvas draggable and zoomable
*/
class SceneGestures {
private static final double MAX_SCALE = 10.0d;
private static final double MIN_SCALE = .1d;
private DragContext sceneDragContext = new DragContext();
PannableCanvas canvas;
public SceneGestures( PannableCanvas canvas) {
this.canvas = canvas;
}
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) {
// right mouse button => panning
if( !event.isSecondaryButtonDown())
return;
sceneDragContext.mouseAnchorX = event.getSceneX();
sceneDragContext.mouseAnchorY = event.getSceneY();
sceneDragContext.translateAnchorX = canvas.getTranslateX();
sceneDragContext.translateAnchorY = canvas.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// right mouse button => panning
if( !event.isSecondaryButtonDown())
return;
canvas.setTranslateX(sceneDragContext.translateAnchorX + event.getSceneX() - sceneDragContext.mouseAnchorX);
canvas.setTranslateY(sceneDragContext.translateAnchorY + event.getSceneY() - 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 = 1.2;
double scale = canvas.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
if (event.getDeltaY() < 0)
scale /= delta;
else
scale *= delta;
scale = clamp( scale, MIN_SCALE, MAX_SCALE);
double f = (scale / oldScale)-1;
double dx = (event.getSceneX() - (canvas.getBoundsInParent().getWidth()/2 + canvas.getBoundsInParent().getMinX()));
double dy = (event.getSceneY() - (canvas.getBoundsInParent().getHeight()/2 + canvas.getBoundsInParent().getMinY()));
canvas.setScale( scale);
// note: pivot value must be untransformed, i. e. without scaling
canvas.setPivot(f*dx, f*dy);
event.consume();
}
};
public static double clamp( double value, double min, double max) {
if( Double.compare(value, min) < 0)
return min;
if( Double.compare(value, max) > 0)
return max;
return value;
}
}
/**
* An application with a zoomable and pannable canvas.
*/
public class ZoomAndScrollApplication extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Group group = new Group();
// create canvas
PannableCanvas canvas = new PannableCanvas();
// we don't want the canvas on the top/left in this example => just
// translate it a bit
canvas.setTranslateX(100);
canvas.setTranslateY(100);
// create sample nodes which can be dragged
NodeGestures nodeGestures = new NodeGestures( canvas);
Label label1 = new Label("Draggable node 1");
label1.setTranslateX(10);
label1.setTranslateY(10);
label1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Label label2 = new Label("Draggable node 2");
label2.setTranslateX(100);
label2.setTranslateY(100);
label2.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label2.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Label label3 = new Label("Draggable node 3");
label3.setTranslateX(200);
label3.setTranslateY(200);
label3.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label3.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Circle circle1 = new Circle( 300, 300, 50);
circle1.setStroke(Color.ORANGE);
circle1.setFill(Color.ORANGE.deriveColor(1, 1, 1, 0.5));
circle1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
circle1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Rectangle rect1 = new Rectangle(100,100);
rect1.setTranslateX(450);
rect1.setTranslateY(450);
rect1.setStroke(Color.BLUE);
rect1.setFill(Color.BLUE.deriveColor(1, 1, 1, 0.5));
rect1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
rect1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
canvas.getChildren().addAll(label1, label2, label3, circle1, rect1);
group.getChildren().add(canvas);
// create scene which can be dragged and zoomed
Scene scene = new Scene(group, 1024, 768);
SceneGestures sceneGestures = new SceneGestures(canvas);
scene.addEventFilter( MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scene.addEventFilter( MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scene.addEventFilter( ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
stage.setScene(scene);
stage.show();
canvas.addGrid();
}
}
I've had this exact problem, I'll show my (heavily condensed) solution. First create a class that is in charge of the zooming:
public class AnimatedZoomOperator {
private Timeline timeline;
public AnimatedZoomOperator() {
this.timeline = new Timeline(60);
}
public void zoom(Node node, double factor, double x, double y) {
// determine scale
double oldScale = node.getScaleX();
double scale = oldScale * factor;
double f = (scale / oldScale) - 1;
// determine offset that we will have to move the node
Bounds bounds = node.localToScene(node.getBoundsInLocal());
double dx = (x - (bounds.getWidth() / 2 + bounds.getMinX()));
double dy = (y - (bounds.getHeight() / 2 + bounds.getMinY()));
// timeline that scales and moves the node
timeline.getKeyFrames().clear();
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.millis(200), new KeyValue(node.translateXProperty(), node.getTranslateX() - f * dx)),
new KeyFrame(Duration.millis(200), new KeyValue(node.translateYProperty(), node.getTranslateY() - f * dy)),
new KeyFrame(Duration.millis(200), new KeyValue(node.scaleXProperty(), scale)),
new KeyFrame(Duration.millis(200), new KeyValue(node.scaleYProperty(), scale))
);
timeline.play();
}
}
Note that this uses a timeline to animate the zoom. If you don't want this, you can call node.setTranslateX(...), node.setScaleX(...), etc. directly.
Now to use this operator you can do something like this:
// Create panel
StackPane zoomPane = new StackPane();
zoomPane.getChildren().add(new Circle(100, 100, 10));
zoomPane.getChildren().add(new Circle(200, 200, 20));
// Create operator
AnimatedZoomOperator zoomOperator = new AnimatedZoomOperator();
// Listen to scroll events (similarly you could listen to a button click, slider, ...)
zoomPane.setOnScroll(new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double zoomFactor = 1.5;
if (event.getDeltaY() <= 0) {
// zoom out
zoomFactor = 1 / zoomFactor;
}
zoomOperator.zoom(zoomPane, zoomFactor, event.getSceneX(), event.getSceneY());
}
});
Try this if you want to zoom a pane in a window. Call method on an event.
private void zoomIn(Pane pane) {
Scale newScale = new Scale();
newScale.setX(pane.getScaleX() + YOUR_ZOOM_FACTOR_VALUE);
newScale.setY(pane.getScaleY() + YOUR_ZOOM_FACTOR_VALUE);
newScale.setPivotX(pane.getScaleX());
newScale.setPivotY(pane.getScaleY());
pane.getTransforms().add(newScale);
}

Scale at pivot point in an already scaled node

I'm trying to create an application with a zoomable/pannable canvas.
Features:
zoom in/out with mouse wheel at pivot points
drag nodes around on the canvas with left mouse button
drag the entire canvas with right mouse button
The zooming at the pivot point works as long as you start the zooming at scale 1. Position the mouse over a grid point and scroll the mouse wheel. The pivot point will remain where you started zooming.
Problem:
When you zoom in, then move the mouse to another point and zoom again, then the pivot point is shifted and zooming isn't happening anymore at the initial mouse position.
Example:
Here's the code:
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Scale;
import javafx.stage.Stage;
/**
* The canvas which holds all of the nodes of the application.
*/
class PannableCanvas extends Pane {
Scale scaleTransform;
public PannableCanvas() {
setPrefSize(600, 600);
setStyle("-fx-background-color: lightgrey; -fx-border-color: blue;");
// add scale transform
scaleTransform = new Scale( 1.0, 1.0);
getTransforms().add( scaleTransform);
// logging
addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
System.out.println(
"canvas event: " + ( ((event.getSceneX() - getBoundsInParent().getMinX()) / getScale()) + ", scale: " + getScale())
);
System.out.println( "canvas bounds: " + getBoundsInParent());
});
}
/**
* Add a grid to the canvas, send it to back
*/
public void addGrid() {
double w = getBoundsInLocal().getWidth();
double h = getBoundsInLocal().getHeight();
// add grid
Canvas grid = new Canvas(w, h);
// don't catch mouse events
grid.setMouseTransparent(true);
GraphicsContext gc = grid.getGraphicsContext2D();
gc.setStroke(Color.GRAY);
gc.setLineWidth(1);
// draw grid lines
double offset = 50;
for( double i=offset; i < w; i+=offset) {
// vertical
gc.strokeLine( i, 0, i, h);
// horizontal
gc.strokeLine( 0, i, w, i);
}
getChildren().add( grid);
grid.toBack();
}
public Scale getScaleTransform() {
return scaleTransform;
}
public double getScale() {
return scaleTransform.getY();
}
/**
* Set x/y scale
* #param scale
*/
public void setScale( double scale) {
scaleTransform.setX(scale);
scaleTransform.setY(scale);
}
/**
* Set x/y pivot points
* #param x
* #param y
*/
public void setPivot( double x, double y) {
scaleTransform.setPivotX(x);
scaleTransform.setPivotY(y);
}
}
/**
* Mouse drag context used for scene and nodes.
*/
class DragContext {
double mouseAnchorX;
double mouseAnchorY;
double translateAnchorX;
double translateAnchorY;
}
/**
* Listeners for making the nodes draggable via left mouse button. Considers if parent is zoomed.
*/
class NodeGestures {
private DragContext nodeDragContext = new DragContext();
PannableCanvas canvas;
public NodeGestures( PannableCanvas canvas) {
this.canvas = canvas;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// left mouse button => dragging
if( !event.isPrimaryButtonDown())
return;
nodeDragContext.mouseAnchorX = event.getSceneX();
nodeDragContext.mouseAnchorY = event.getSceneY();
Node node = (Node) event.getSource();
nodeDragContext.translateAnchorX = node.getTranslateX();
nodeDragContext.translateAnchorY = node.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// left mouse button => dragging
if( !event.isPrimaryButtonDown())
return;
double scale = canvas.getScale();
Node node = (Node) event.getSource();
node.setTranslateX(nodeDragContext.translateAnchorX + (( event.getSceneX() - nodeDragContext.mouseAnchorX) / scale));
node.setTranslateY(nodeDragContext.translateAnchorY + (( event.getSceneY() - nodeDragContext.mouseAnchorY) / scale));
event.consume();
}
};
}
/**
* Listeners for making the scene's canvas draggable and zoomable
*/
class SceneGestures {
private static final double MAX_SCALE = 10.0d;
private static final double MIN_SCALE = .1d;
private DragContext sceneDragContext = new DragContext();
PannableCanvas canvas;
public SceneGestures( PannableCanvas canvas) {
this.canvas = canvas;
}
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) {
// right mouse button => panning
if( !event.isSecondaryButtonDown())
return;
sceneDragContext.mouseAnchorX = event.getSceneX();
sceneDragContext.mouseAnchorY = event.getSceneY();
sceneDragContext.translateAnchorX = canvas.getTranslateX();
sceneDragContext.translateAnchorY = canvas.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// right mouse button => panning
if( !event.isSecondaryButtonDown())
return;
canvas.setTranslateX(sceneDragContext.translateAnchorX + event.getSceneX() - sceneDragContext.mouseAnchorX);
canvas.setTranslateY(sceneDragContext.translateAnchorY + event.getSceneY() - 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 = 1;
double scale = canvas.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
if (event.getDeltaY() < 0)
scale -= delta;
else
scale += delta;
if (scale <= MIN_SCALE) {
scale = MIN_SCALE;
} else if (scale >= MAX_SCALE) {
scale = MAX_SCALE;
}
// pivot value must be untransformed, i. e. without scaling
canvas.setPivot(
((event.getSceneX() - canvas.getBoundsInParent().getMinX()) / oldScale),
((event.getSceneY() - canvas.getBoundsInParent().getMinY()) / oldScale)
);
canvas.setScale( scale);
System.out.println( "new pivot x: " + canvas.scaleTransform.getPivotX() + "/" + canvas.scaleTransform.getPivotY() + ", new scale: " + scale);
System.out.println( "bounds: " + canvas.getBoundsInParent());
event.consume();
}
};
}
/**
* An application with a zoomable and pannable canvas.
*/
public class ScrollApplication extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Group group = new Group();
// create canvas
PannableCanvas canvas = new PannableCanvas();
// we don't want the canvas on the top/left in this example => just
// translate it a bit
canvas.setTranslateX(100);
canvas.setTranslateY(100);
// create sample nodes which can be dragged
NodeGestures nodeGestures = new NodeGestures( canvas);
Label label1 = new Label("Draggable node 1");
label1.setTranslateX(10);
label1.setTranslateY(10);
label1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Label label2 = new Label("Draggable node 2");
label2.setTranslateX(100);
label2.setTranslateY(100);
label2.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label2.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Label label3 = new Label("Draggable node 3");
label3.setTranslateX(200);
label3.setTranslateY(200);
label3.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label3.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Circle circle1 = new Circle( 300, 300, 50);
circle1.setStroke(Color.ORANGE);
circle1.setFill(Color.ORANGE.deriveColor(1, 1, 1, 0.5));
circle1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
circle1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Rectangle rect1 = new Rectangle(100,100);
rect1.setTranslateX(450);
rect1.setTranslateY(450);
rect1.setStroke(Color.BLUE);
rect1.setFill(Color.BLUE.deriveColor(1, 1, 1, 0.5));
rect1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
rect1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
canvas.getChildren().addAll(label1, label2, label3, circle1, rect1);
group.getChildren().add(canvas);
// create scene which can be dragged and zoomed
Scene scene = new Scene(group, 1024, 768);
SceneGestures sceneGestures = new SceneGestures(canvas);
scene.addEventFilter( MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scene.addEventFilter( MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scene.addEventFilter( ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
stage.setScene(scene);
stage.show();
canvas.addGrid();
}
}
It's obviously something wrong with the pivot point calculation, but I can't figure out what it is and how to fix it.
Thank you very much!
First I would recommend to not scale in linear steps but by factors to smooth the scaling:
double delta = 1.2;
if (event.getDeltaY() < 0)
scale /= delta;
else
scale *= delta;
... and to be somehow bossy, I recommend curly brackets as a good style ;-) :
double delta = 1.2;
if (event.getDeltaY() < 0) {
scale /= delta;
} else {
scale *= delta;
}
... and to use the mouse scroll value for even better quality:
double delta = 1.2;
if (event.getDeltaY() < 0) {
scale /= Math.pow(delta, -event.getDeltaY()/20);
} else {
scale *= Math.pow(delta, event.getDeltaY()/20);
}
... that is finally the same as:
scale *= Math.pow(1.01, event.getDeltaY());
Second I recomend to use the canvas translate and scale properties instead of a Transformation:
public class ZoomApplication extends Application {
static public class PannableCanvas extends Pane {
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public PannableCanvas() {
setPrefSize(600, 600);
setStyle("-fx-background-color: lightgrey; -fx-border-color: blue;");
// add scale transform
scaleXProperty().bind(myScale);
scaleYProperty().bind(myScale);
// logging
addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
System.out.println(
"canvas event: " + ( ((event.getSceneX() - getBoundsInParent().getMinX()) / getScale()) + ", scale: " + getScale())
);
System.out.println( "canvas bounds: " + getBoundsInParent());
});
}
/**
* Add a grid to the canvas, send it to back
*/
public void addGrid() {
double w = getBoundsInLocal().getWidth();
double h = getBoundsInLocal().getHeight();
// add grid
Canvas grid = new Canvas(w, h);
// don't catch mouse events
grid.setMouseTransparent(true);
GraphicsContext gc = grid.getGraphicsContext2D();
gc.setStroke(Color.GRAY);
gc.setLineWidth(1);
// draw grid lines
double offset = 50;
for( double i=offset; i < w; i+=offset) {
// vertical
gc.strokeLine( i, 0, i, h);
// horizontal
gc.strokeLine( 0, i, w, i);
}
getChildren().add( grid);
grid.toBack();
}
public double getScale() {
return myScale.get();
}
/**
* Set x/y scale
* #param myScale
*/
public void setScale( double scale) {
myScale.set(scale);
}
/**
* Set x/y pivot points
* #param x
* #param y
*/
public void setPivot( double x, double y) {
setTranslateX(getTranslateX()-x);
setTranslateY(getTranslateY()-y);
}
}
/**
* Mouse drag context used for scene and nodes.
*/
class DragContext {
double mouseAnchorX;
double mouseAnchorY;
double translateAnchorX;
double translateAnchorY;
}
/**
* Listeners for making the nodes draggable via left mouse button. Considers if parent is zoomed.
*/
class NodeGestures {
private DragContext nodeDragContext = new DragContext();
PannableCanvas canvas;
public NodeGestures( PannableCanvas canvas) {
this.canvas = canvas;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// left mouse button => dragging
if( !event.isPrimaryButtonDown())
return;
nodeDragContext.mouseAnchorX = event.getSceneX();
nodeDragContext.mouseAnchorY = event.getSceneY();
Node node = (Node) event.getSource();
nodeDragContext.translateAnchorX = node.getTranslateX();
nodeDragContext.translateAnchorY = node.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// left mouse button => dragging
if( !event.isPrimaryButtonDown())
return;
double scale = canvas.getScale();
Node node = (Node) event.getSource();
node.setTranslateX(nodeDragContext.translateAnchorX + (( event.getSceneX() - nodeDragContext.mouseAnchorX) / scale));
node.setTranslateY(nodeDragContext.translateAnchorY + (( event.getSceneY() - nodeDragContext.mouseAnchorY) / scale));
event.consume();
}
};
}
/**
* Listeners for making the scene's canvas draggable and zoomable
*/
class SceneGestures {
private static final double MAX_SCALE = 10.0d;
private static final double MIN_SCALE = .1d;
private DragContext sceneDragContext = new DragContext();
PannableCanvas canvas;
public SceneGestures( PannableCanvas canvas) {
this.canvas = canvas;
}
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) {
// right mouse button => panning
if( !event.isSecondaryButtonDown())
return;
sceneDragContext.mouseAnchorX = event.getSceneX();
sceneDragContext.mouseAnchorY = event.getSceneY();
sceneDragContext.translateAnchorX = canvas.getTranslateX();
sceneDragContext.translateAnchorY = canvas.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// right mouse button => panning
if( !event.isSecondaryButtonDown())
return;
canvas.setTranslateX(sceneDragContext.translateAnchorX + event.getSceneX() - sceneDragContext.mouseAnchorX);
canvas.setTranslateY(sceneDragContext.translateAnchorY + event.getSceneY() - 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 scale = canvas.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
scale *= Math.pow(1.01, event.getDeltaY());
if (scale <= MIN_SCALE) {
scale = MIN_SCALE;
} else if (scale >= MAX_SCALE) {
scale = MAX_SCALE;
}
double f = (scale / oldScale)-1;
double dx = (event.getSceneX() - (canvas.getBoundsInParent().getWidth()/2 + canvas.getBoundsInParent().getMinX()));
double dy = (event.getSceneY() - (canvas.getBoundsInParent().getHeight()/2 + canvas.getBoundsInParent().getMinY()));
canvas.setScale( scale);
canvas.setPivot(f*dx, f*dy);
event.consume();
}
};
}
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Group group = new Group();
// create canvas
PannableCanvas canvas = new PannableCanvas();
// we don't want the canvas on the top/left in this example => just
// translate it a bit
canvas.setTranslateX(100);
canvas.setTranslateY(100);
// create sample nodes which can be dragged
NodeGestures nodeGestures = new NodeGestures( canvas);
Label label1 = new Label("Draggable node 1");
label1.setTranslateX(10);
label1.setTranslateY(10);
label1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Label label2 = new Label("Draggable node 2");
label2.setTranslateX(100);
label2.setTranslateY(100);
label2.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label2.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Label label3 = new Label("Draggable node 3");
label3.setTranslateX(200);
label3.setTranslateY(200);
label3.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label3.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Circle circle1 = new Circle( 300, 300, 50);
circle1.setStroke(Color.ORANGE);
circle1.setFill(Color.ORANGE.deriveColor(1, 1, 1, 0.5));
circle1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
circle1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Rectangle rect1 = new Rectangle(100,100);
rect1.setTranslateX(450);
rect1.setTranslateY(450);
rect1.setStroke(Color.BLUE);
rect1.setFill(Color.BLUE.deriveColor(1, 1, 1, 0.5));
rect1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
rect1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
canvas.getChildren().addAll(label1, label2, label3, circle1, rect1);
group.getChildren().add(canvas);
// create scene which can be dragged and zoomed
Scene scene = new Scene(group, 1024, 768);
SceneGestures sceneGestures = new SceneGestures(canvas);
scene.addEventFilter( MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scene.addEventFilter( MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scene.addEventFilter( ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
stage.setScene(scene);
stage.show();
canvas.addGrid();
}
}
After some thoughts about zoom, I came to the conclusion, that it would be a good idea to
Write an independent zoom helper method to ease the zoom function
To also support the pinch-to-zoom gesture with the same method
So I wrote the following helper method:
/** Allow to zoom/scale any node with pivot at scene (x,y) coordinates.
*
* #param node
* #param delta
* #param x
* #param y
*/
public static void zoom(Node node, double factor, double x, double y) {
double oldScale = node.getScaleX();
double scale = oldScale * factor;
if (scale < 0.05) scale = 0.05;
if (scale > 50) scale = 50;
node.setScaleX(scale);
node.setScaleY(scale);
double f = (scale / oldScale)-1;
Bounds bounds = node.localToScene(node.getBoundsInLocal());
double dx = (x - (bounds.getWidth()/2 + bounds.getMinX()));
double dy = (y - (bounds.getHeight()/2 + bounds.getMinY()));
node.setTranslateX(node.getTranslateX()-f*dx);
node.setTranslateY(node.getTranslateY()-f*dy);
}
public static void zoom(Node node, ScrollEvent event) {
zoom(node, Math.pow(1.01, event.getDeltaY()), event.getSceneX(), event.getSceneY());
}
public static void zoom(Node node, ZoomEvent event) {
zoom(node, event.getZoomFactor(), event.getSceneX(), event.getSceneY());
}
allowing me to register zoom function on any node as easy as:
myView.setOnScroll(event -> GUITools.zoom(myView, event)); // mouse scroll wheel zoom
myView.setOnZoom(event -> GUITools.zoom(myView, event)); // pinch to zoom
and done...
I change your SceneGestures class so now working.
class SceneGestures {
private double oldx;
private double oldy;
double ttx=0;
double tty=0;
private static final double MAX_SCALE = 10.0d;
private static final double MIN_SCALE = .1d;
private DragContext sceneDragContext = new DragContext();
PannableCanvas canvas;
public SceneGestures( PannableCanvas canvas) {
this.canvas = canvas;
}
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) {
// right mouse button => panning
if( !event.isSecondaryButtonDown())
return;
sceneDragContext.mouseAnchorX = event.getSceneX();
sceneDragContext.mouseAnchorY = event.getSceneY();
sceneDragContext.translateAnchorX = canvas.getTranslateX();
sceneDragContext.translateAnchorY = canvas.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// right mouse button => panning
if( !event.isSecondaryButtonDown())
return;
canvas.setTranslateX(sceneDragContext.translateAnchorX + event.getSceneX() - sceneDragContext.mouseAnchorX);
canvas.setTranslateY(sceneDragContext.translateAnchorY + event.getSceneY() - 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 = 1;
double scale = canvas.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
if (event.getDeltaY() < 0)
scale -= delta;
else
scale += delta;
if (scale <= MIN_SCALE) {
scale = MIN_SCALE;
} else if (scale >= MAX_SCALE) {
scale = MAX_SCALE;
}
if (oldx==0){
ttx=event.getSceneX() ;
tty=event.getSceneY() ;
}else{
if (oldx!=event.getSceneX()){
ttx=((event.getSceneX()+oldx)/2) ;
}
if (oldy!=event.getSceneY()){
tty=((event.getSceneY()+oldy)/2);
}
}
// pivot value must be untransformed, i. e. without scaling
canvas.setPivot(
((ttx- canvas.getBoundsInParent().getMinX()) / oldScale),
((tty- canvas.getBoundsInParent().getMinY()) / oldScale)
);
// if (oldx==0){
oldx=event.getSceneX();
oldy=event.getSceneY();
// }
//try {
// Robot rbt=new Robot();
// rbt.mouseMove(512, 384);
//} catch (AWTException ex) {
// System.out.println(ex.getMessage());
// }
canvas.setScale( scale);
System.out.println( "new pivot x: " + canvas.scaleTransform.getPivotX() + "/" + canvas.scaleTransform.getPivotY() + ", new scale: " + scale);
System.out.println( "bounds: " + canvas.getBoundsInParent());
System.out.println( "old: " + oldx+" "+oldy);
System.out.println( "tt: " + ttx+" "+tty);
event.consume();
}
};
}
In FX8 you can do
final Affine accumulatedScales = new Affine();
chart.getTransforms().add(accumulatedScales);
chart.setOnScroll(new EventHandler<ScrollEvent>() {
#Override public void handle(ScrollEvent event) {
accumulatedScales.appendScale(scaleFactor, scaleFactor, event.getX(), event.getY());
}
});
and you are done
With this approach you can zoom in position with transformed, rotated nodes.
public static void zoom(Node node, double factor, ScrollEvent event) {
double oldScale = node.getScaleX();
double scale = oldScale*factor;
if (scale < MIN_ZOOM) scale = MIN_ZOOM;
if (scale > MAX_ZOOM) scale = MAX_ZOOM;
double x = event.getX();
double y = event.getY();
Point2D p0 = node.localToScene(x, y);
node.setScaleX(scale);
node.setScaleY(scale);
Point2D p1 = node.localToScene(x, y);
double deltaX = p1.getX() - p0.getX();
double deltaY = p1.getY() - p0.getY();
node.setTranslateX(node.getTranslateX() - deltaX);
node.setTranslateY(node.getTranslateY() - deltaY);
}

Resources