我正在尝试使用可缩放/可绘制的画布创建应用程序.
特征:
>使用鼠标滚轮放大/缩小转动点
>使用鼠标左键在画布上拖动节点
>用鼠标右键拖动整个画布
只要您在比例尺1开始缩放,就可以在枢轴点进行缩放.将鼠标放在网格点上并滚动鼠标滚轮.枢轴点将保持开始缩放的位置.
问题:
当您放大时,将鼠标移动到另一点并再次放大,然后在初始鼠标位置,枢轴点移动,缩放不再发生.
例:
以下是代码:
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,i,h); // horizontal gc.strokeLine( 0,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,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,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(); } }
显然,枢轴点计算有问题,但我无法弄清楚它是什么,以及如何解决它.
非常感谢你!
解决方法
首先,我建议不要按直线步长进行分级,还可以通过因素来平滑缩放:
double delta = 1.2; if (event.getDeltaY() < 0) scale /= delta; else scale *= delta;
…而且以某种方式老板,我推荐大括号作为一个很好的风格;-):
double delta = 1.2; if (event.getDeltaY() < 0) { scale /= delta; } else { scale *= delta; }
…并使用鼠标滚动值更好的质量:
double delta = 1.2; if (event.getDeltaY() < 0) { scale /= Math.pow(delta,-event.getDeltaY()/20); } else { scale *= Math.pow(delta,event.getDeltaY()/20); }
…那最终是一样的:
scale *= Math.pow(1.01,event.getDeltaY());
第二,我建议使用画布翻译和缩放属性而不是转换:
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,h); // horizontal gc.strokeLine( 0,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,sceneGestures.getOnScrollEventHandler()); stage.setScene(scene); stage.show(); canvas.addGrid(); } }
经过一些关于缩放的想法,我得出结论,这将是一个好主意
>编写一个独立的缩放辅助方法来简化缩放功能
>也可以使用相同的方法支持捏合到缩放手势
所以我写了以下帮助方法:
/** 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.getSceneY()); }
myView.setOnScroll(event -> GUITools.zoom(myView,event)); // mouse scroll wheel zoom myView.setOnZoom(event -> GUITools.zoom(myView,event)); // pinch to zoom
并做了…