内部带有可拖动节点的 JavaFX 平移和缩放
JavaFX Pan and Zoom with Draggable Nodes Inside
我有一个简单的 JavaFX 平移和缩放应用程序,如下所示。平移和缩放功能效果很好,但我也希望能够拖放圆形节点。我遇到的问题是滚动窗格首先获取所有鼠标事件,因此我无法将拖放分配给圆圈。是否可以有一个 draggable/zoomable 滚动窗格并且还可以将一个节点拖到窗格内?
Screenshot
这是我正在使用的代码:
package sample;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Main extends Application {
private ScrollPane scrollPane = new ScrollPane();
private final DoubleProperty zoomProperty = new SimpleDoubleProperty(1.0d);
private final DoubleProperty deltaY = new SimpleDoubleProperty(0.0d);
private final Group group = new Group();
ImageView bigImageView = null;
PanAndZoomPane panAndZoomPane = null;
Pane featuresPane = new Pane();
@Override
public void start(Stage primaryStage) throws Exception{
bigImageView = new ImageView();
StackPane bigStackpane = new StackPane();
bigStackpane.getChildren().add(bigImageView);
bigStackpane.getChildren().add(featuresPane);
featuresPane.toFront();
featuresPane.setOpacity(.8);
Circle circle = new Circle();
circle.setCenterX(200);
circle.setCenterY(200);
circle.setRadius(100);
circle.setFill(Color.RED);
circle.setOnMouseClicked(e -> {
System.out.println("circle clicked");
});
featuresPane.getChildren().add(circle);
scrollPane.setPannable(true);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
group.getChildren().add(bigImageView);
group.getChildren().add(bigStackpane);
panAndZoomPane = new PanAndZoomPane();
zoomProperty.bind(panAndZoomPane.myScale);
deltaY.bind(panAndZoomPane.deltaY);
panAndZoomPane.getChildren().add(group);
SceneGestures sceneGestures = new SceneGestures(panAndZoomPane);
scrollPane.setContent(panAndZoomPane);
panAndZoomPane.toBack();
scrollPane.addEventFilter( MouseEvent.MOUSE_CLICKED, sceneGestures.getOnMouseClickedEventHandler());
scrollPane.addEventFilter( MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scrollPane.addEventFilter( MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scrollPane.addEventFilter( ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
AnchorPane bigImageAnchorPane = new AnchorPane();
bigImageAnchorPane.getChildren().add(scrollPane);
Image image = new Image("https://i.imgur.com/8p1XBag.jpg");
bigImageView.setImage(image);
bigImageAnchorPane.setTopAnchor(scrollPane, 1.0d);
bigImageAnchorPane.setRightAnchor(scrollPane, 1.0d);
bigImageAnchorPane.setBottomAnchor(scrollPane, 1.0d);
bigImageAnchorPane.setLeftAnchor(scrollPane, 1.0d);
BorderPane root = new BorderPane(bigImageAnchorPane);
Label label = new Label("Pan and Zoom Test");
root.setTop(label);
Scene scene = new Scene(root, 1000, 1000);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
class PanAndZoomPane extends Pane {
public static final double DEFAULT_DELTA = 1.5d; //1.3d
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public DoubleProperty deltaY = new SimpleDoubleProperty(0.0);
private Timeline timeline;
public PanAndZoomPane() {
this.timeline = new Timeline(30);//60
// add scale transform
scaleXProperty().bind(myScale);
scaleYProperty().bind(myScale);
}
public double getScale() {
return myScale.get();
}
public void setScale( double scale) {
myScale.set(scale);
}
public void setPivot( double x, double y, double scale) {
// note: pivot value must be untransformed, i. e. without scaling
// timeline that scales and moves the node
timeline.getKeyFrames().clear();
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.millis(100), new KeyValue(translateXProperty(), getTranslateX() - x)), //200
new KeyFrame(Duration.millis(100), new KeyValue(translateYProperty(), getTranslateY() - y)), //200
new KeyFrame(Duration.millis(100), new KeyValue(myScale, scale)) //200
);
timeline.play();
}
public double getDeltaY() {
return deltaY.get();
}
public void setDeltaY( double dY) {
deltaY.set(dY);
}
}
/**
* Mouse drag context used for scene and nodes.
*/
class DragContext {
double mouseAnchorX;
double mouseAnchorY;
double translateAnchorX;
double translateAnchorY;
}
/**
* Listeners for making the scene's canvas draggable and zoomable
*/
public class SceneGestures {
private DragContext sceneDragContext = new DragContext();
PanAndZoomPane panAndZoomPane;
public SceneGestures( PanAndZoomPane canvas) {
this.panAndZoomPane = canvas;
}
public EventHandler<MouseEvent> getOnMouseClickedEventHandler() {
return onMouseClickedEventHandler;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
public EventHandler<ScrollEvent> getOnScrollEventHandler() {
return onScrollEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
sceneDragContext.mouseAnchorX = event.getX();
sceneDragContext.mouseAnchorY = event.getY();
sceneDragContext.translateAnchorX = panAndZoomPane.getTranslateX();
sceneDragContext.translateAnchorY = panAndZoomPane.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
panAndZoomPane.setTranslateX(sceneDragContext.translateAnchorX + event.getX() - sceneDragContext.mouseAnchorX);
panAndZoomPane.setTranslateY(sceneDragContext.translateAnchorY + event.getY() - sceneDragContext.mouseAnchorY);
event.consume();
}
};
/**
* Mouse wheel handler: zoom to pivot point
*/
private EventHandler<ScrollEvent> onScrollEventHandler = new EventHandler<ScrollEvent>() {
@Override
public void handle(ScrollEvent event) {
double delta = PanAndZoomPane.DEFAULT_DELTA;
double scale = panAndZoomPane.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
panAndZoomPane.setDeltaY(event.getDeltaY());
if (panAndZoomPane.deltaY.get() < 0) {
scale /= delta;
} else {
scale *= delta;
}
double f = (scale / oldScale)-1;
double dx = (event.getX() - (panAndZoomPane.getBoundsInParent().getWidth()/2 + panAndZoomPane.getBoundsInParent().getMinX()));
double dy = (event.getY() - (panAndZoomPane.getBoundsInParent().getHeight()/2 + panAndZoomPane.getBoundsInParent().getMinY()));
panAndZoomPane.setPivot(f*dx, f*dy, scale);
event.consume();
}
};
/**
* Mouse click handler
*/
private EventHandler<MouseEvent> onMouseClickedEventHandler = new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
if (event.getButton().equals(MouseButton.PRIMARY)) {
if (event.getClickCount() == 2) {
System.out.println("Image Layer Double Clicked...");
}else{
System.out.println("Image Layer Clicked...");
}
}
}
};
}
}
您的代码正在通过事件 过滤器 添加行为。这些过滤器在事件 捕获 阶段被调用,这意味着它们在事件到达您的圈子之前被调用。您应该努力通过在事件 冒泡 阶段调用的事件 处理程序 来实现您的行为。然后您可以使用事件来防止它们到达祖先,允许您在没有 scrolling/panning 滚动窗格内容的情况下拖动您的圆圈。有关事件处理和传播的更多信息,请查看 this tutorial。
这是一个概念验证,它将缩放处理添加到滚动窗格的内容中,仍然让您绕着一个圆圈拖动:
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage primaryStage) {
// using your example image
ImageView imageView = new ImageView("https://i.imgur.com/8p1XBag.jpg");
Circle circle = new Circle(100, 100, 25, Color.FIREBRICK);
circle.setOnMousePressed(
e -> {
// prevent pannable ScrollPane from changing cursor on drag-detected (implementation
// detail)
e.setDragDetect(false);
Point2D offset =
new Point2D(e.getX() - circle.getCenterX(), e.getY() - circle.getCenterY());
circle.setUserData(offset);
e.consume(); // prevents MouseEvent from reaching ScrollPane
});
circle.setOnMouseDragged(
e -> {
// prevent pannable ScrollPane from changing cursor on drag-detected (implementation
// detail)
e.setDragDetect(false);
Point2D offset = (Point2D) circle.getUserData();
circle.setCenterX(e.getX() - offset.getX());
circle.setCenterY(e.getY() - offset.getY());
e.consume(); // prevents MouseEvent from reaching ScrollPane
});
// the zoom-able content of the ScrollPane
Group group = new Group(imageView, circle);
// wrap Group in another Group since it's the former that's scaled and
// Groups only take transformations of their **children** into account (not themselves)
StackPane content = new StackPane(new Group(group));
content.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
// due to later configuration, the StackPane will always cover the entire viewport
content.setOnScroll(
e -> {
if (e.isShortcutDown() && e.getDeltaY() != 0) {
if (e.getDeltaY() < 0) {
group.setScaleX(Math.max(group.getScaleX() - 0.1, 0.5));
} else {
group.setScaleX(Math.min(group.getScaleX() + 0.1, 5.0));
}
group.setScaleY(group.getScaleX());
e.consume(); // prevents ScrollEvent from reaching ScrollPane
}
});
// use StackPane (or some other resizable node) as content since Group is not
// resizable. Note StackPane will center content if smaller than viewport.
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setVbarPolicy(ScrollBarPolicy.NEVER);
scrollPane.setHbarPolicy(ScrollBarPolicy.NEVER);
scrollPane.setPannable(true);
// ensure StackPane content always has at least the same dimensions as the viewport
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
primaryStage.setScene(new Scene(scrollPane, 1000, 650));
primaryStage.show();
}
}
请注意,这并不完全复制您示例的行为。它不使用动画,也不缩放轴心点。但希望它能帮助你在申请中取得进展。
我有一个简单的 JavaFX 平移和缩放应用程序,如下所示。平移和缩放功能效果很好,但我也希望能够拖放圆形节点。我遇到的问题是滚动窗格首先获取所有鼠标事件,因此我无法将拖放分配给圆圈。是否可以有一个 draggable/zoomable 滚动窗格并且还可以将一个节点拖到窗格内?
Screenshot
这是我正在使用的代码:
package sample;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Main extends Application {
private ScrollPane scrollPane = new ScrollPane();
private final DoubleProperty zoomProperty = new SimpleDoubleProperty(1.0d);
private final DoubleProperty deltaY = new SimpleDoubleProperty(0.0d);
private final Group group = new Group();
ImageView bigImageView = null;
PanAndZoomPane panAndZoomPane = null;
Pane featuresPane = new Pane();
@Override
public void start(Stage primaryStage) throws Exception{
bigImageView = new ImageView();
StackPane bigStackpane = new StackPane();
bigStackpane.getChildren().add(bigImageView);
bigStackpane.getChildren().add(featuresPane);
featuresPane.toFront();
featuresPane.setOpacity(.8);
Circle circle = new Circle();
circle.setCenterX(200);
circle.setCenterY(200);
circle.setRadius(100);
circle.setFill(Color.RED);
circle.setOnMouseClicked(e -> {
System.out.println("circle clicked");
});
featuresPane.getChildren().add(circle);
scrollPane.setPannable(true);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
group.getChildren().add(bigImageView);
group.getChildren().add(bigStackpane);
panAndZoomPane = new PanAndZoomPane();
zoomProperty.bind(panAndZoomPane.myScale);
deltaY.bind(panAndZoomPane.deltaY);
panAndZoomPane.getChildren().add(group);
SceneGestures sceneGestures = new SceneGestures(panAndZoomPane);
scrollPane.setContent(panAndZoomPane);
panAndZoomPane.toBack();
scrollPane.addEventFilter( MouseEvent.MOUSE_CLICKED, sceneGestures.getOnMouseClickedEventHandler());
scrollPane.addEventFilter( MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scrollPane.addEventFilter( MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scrollPane.addEventFilter( ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
AnchorPane bigImageAnchorPane = new AnchorPane();
bigImageAnchorPane.getChildren().add(scrollPane);
Image image = new Image("https://i.imgur.com/8p1XBag.jpg");
bigImageView.setImage(image);
bigImageAnchorPane.setTopAnchor(scrollPane, 1.0d);
bigImageAnchorPane.setRightAnchor(scrollPane, 1.0d);
bigImageAnchorPane.setBottomAnchor(scrollPane, 1.0d);
bigImageAnchorPane.setLeftAnchor(scrollPane, 1.0d);
BorderPane root = new BorderPane(bigImageAnchorPane);
Label label = new Label("Pan and Zoom Test");
root.setTop(label);
Scene scene = new Scene(root, 1000, 1000);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
class PanAndZoomPane extends Pane {
public static final double DEFAULT_DELTA = 1.5d; //1.3d
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public DoubleProperty deltaY = new SimpleDoubleProperty(0.0);
private Timeline timeline;
public PanAndZoomPane() {
this.timeline = new Timeline(30);//60
// add scale transform
scaleXProperty().bind(myScale);
scaleYProperty().bind(myScale);
}
public double getScale() {
return myScale.get();
}
public void setScale( double scale) {
myScale.set(scale);
}
public void setPivot( double x, double y, double scale) {
// note: pivot value must be untransformed, i. e. without scaling
// timeline that scales and moves the node
timeline.getKeyFrames().clear();
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.millis(100), new KeyValue(translateXProperty(), getTranslateX() - x)), //200
new KeyFrame(Duration.millis(100), new KeyValue(translateYProperty(), getTranslateY() - y)), //200
new KeyFrame(Duration.millis(100), new KeyValue(myScale, scale)) //200
);
timeline.play();
}
public double getDeltaY() {
return deltaY.get();
}
public void setDeltaY( double dY) {
deltaY.set(dY);
}
}
/**
* Mouse drag context used for scene and nodes.
*/
class DragContext {
double mouseAnchorX;
double mouseAnchorY;
double translateAnchorX;
double translateAnchorY;
}
/**
* Listeners for making the scene's canvas draggable and zoomable
*/
public class SceneGestures {
private DragContext sceneDragContext = new DragContext();
PanAndZoomPane panAndZoomPane;
public SceneGestures( PanAndZoomPane canvas) {
this.panAndZoomPane = canvas;
}
public EventHandler<MouseEvent> getOnMouseClickedEventHandler() {
return onMouseClickedEventHandler;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
public EventHandler<ScrollEvent> getOnScrollEventHandler() {
return onScrollEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
sceneDragContext.mouseAnchorX = event.getX();
sceneDragContext.mouseAnchorY = event.getY();
sceneDragContext.translateAnchorX = panAndZoomPane.getTranslateX();
sceneDragContext.translateAnchorY = panAndZoomPane.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
panAndZoomPane.setTranslateX(sceneDragContext.translateAnchorX + event.getX() - sceneDragContext.mouseAnchorX);
panAndZoomPane.setTranslateY(sceneDragContext.translateAnchorY + event.getY() - sceneDragContext.mouseAnchorY);
event.consume();
}
};
/**
* Mouse wheel handler: zoom to pivot point
*/
private EventHandler<ScrollEvent> onScrollEventHandler = new EventHandler<ScrollEvent>() {
@Override
public void handle(ScrollEvent event) {
double delta = PanAndZoomPane.DEFAULT_DELTA;
double scale = panAndZoomPane.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
panAndZoomPane.setDeltaY(event.getDeltaY());
if (panAndZoomPane.deltaY.get() < 0) {
scale /= delta;
} else {
scale *= delta;
}
double f = (scale / oldScale)-1;
double dx = (event.getX() - (panAndZoomPane.getBoundsInParent().getWidth()/2 + panAndZoomPane.getBoundsInParent().getMinX()));
double dy = (event.getY() - (panAndZoomPane.getBoundsInParent().getHeight()/2 + panAndZoomPane.getBoundsInParent().getMinY()));
panAndZoomPane.setPivot(f*dx, f*dy, scale);
event.consume();
}
};
/**
* Mouse click handler
*/
private EventHandler<MouseEvent> onMouseClickedEventHandler = new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
if (event.getButton().equals(MouseButton.PRIMARY)) {
if (event.getClickCount() == 2) {
System.out.println("Image Layer Double Clicked...");
}else{
System.out.println("Image Layer Clicked...");
}
}
}
};
}
}
您的代码正在通过事件 过滤器 添加行为。这些过滤器在事件 捕获 阶段被调用,这意味着它们在事件到达您的圈子之前被调用。您应该努力通过在事件 冒泡 阶段调用的事件 处理程序 来实现您的行为。然后您可以使用事件来防止它们到达祖先,允许您在没有 scrolling/panning 滚动窗格内容的情况下拖动您的圆圈。有关事件处理和传播的更多信息,请查看 this tutorial。
这是一个概念验证,它将缩放处理添加到滚动窗格的内容中,仍然让您绕着一个圆圈拖动:
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage primaryStage) {
// using your example image
ImageView imageView = new ImageView("https://i.imgur.com/8p1XBag.jpg");
Circle circle = new Circle(100, 100, 25, Color.FIREBRICK);
circle.setOnMousePressed(
e -> {
// prevent pannable ScrollPane from changing cursor on drag-detected (implementation
// detail)
e.setDragDetect(false);
Point2D offset =
new Point2D(e.getX() - circle.getCenterX(), e.getY() - circle.getCenterY());
circle.setUserData(offset);
e.consume(); // prevents MouseEvent from reaching ScrollPane
});
circle.setOnMouseDragged(
e -> {
// prevent pannable ScrollPane from changing cursor on drag-detected (implementation
// detail)
e.setDragDetect(false);
Point2D offset = (Point2D) circle.getUserData();
circle.setCenterX(e.getX() - offset.getX());
circle.setCenterY(e.getY() - offset.getY());
e.consume(); // prevents MouseEvent from reaching ScrollPane
});
// the zoom-able content of the ScrollPane
Group group = new Group(imageView, circle);
// wrap Group in another Group since it's the former that's scaled and
// Groups only take transformations of their **children** into account (not themselves)
StackPane content = new StackPane(new Group(group));
content.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
// due to later configuration, the StackPane will always cover the entire viewport
content.setOnScroll(
e -> {
if (e.isShortcutDown() && e.getDeltaY() != 0) {
if (e.getDeltaY() < 0) {
group.setScaleX(Math.max(group.getScaleX() - 0.1, 0.5));
} else {
group.setScaleX(Math.min(group.getScaleX() + 0.1, 5.0));
}
group.setScaleY(group.getScaleX());
e.consume(); // prevents ScrollEvent from reaching ScrollPane
}
});
// use StackPane (or some other resizable node) as content since Group is not
// resizable. Note StackPane will center content if smaller than viewport.
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setVbarPolicy(ScrollBarPolicy.NEVER);
scrollPane.setHbarPolicy(ScrollBarPolicy.NEVER);
scrollPane.setPannable(true);
// ensure StackPane content always has at least the same dimensions as the viewport
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
primaryStage.setScene(new Scene(scrollPane, 1000, 650));
primaryStage.show();
}
}
请注意,这并不完全复制您示例的行为。它不使用动画,也不缩放轴心点。但希望它能帮助你在申请中取得进展。