JavaFX正确缩放

我想在一个滚动事件中缩放窗格中的所有节点。

我到目前为止所尝试的:

  1. 当我做scaleX或scaleY时,窗格的边界分别缩放(设置窗格样式-fx-border-color: black;时可见)。 因此,如果我不是从窗格的边界开始,每个事件都不会开始,所以我需要这一切。 在这里输入图像描述

  2. 下一步,我尝试缩放每个节点,结果变得非常糟糕,就像这样 – (通过点延伸的线)。 或者如果在另一边滚动,则会更less 在这里输入图像描述

  3. 我尝试的另一种方法是缩放节点的点。 这更好,但我不喜欢它。 它看起来像point.setScaleX(point.getScaleX()+scaleX) ,适用于y和其他节点。

我创build了一个示例应用程序来演示如何在滚动事件的视口中执行缩放节点的一种方法(例如通过滚动鼠标滚轮来滚动和滚动)。

缩放放置在StackPane中的组的示例的关键逻辑:

 final double SCALE_DELTA = 1.1; final StackPane zoomPane = new StackPane(); zoomPane.getChildren().add(group); zoomPane.setOnScroll(new EventHandler<ScrollEvent>() { @Override public void handle(ScrollEvent event) { event.consume(); if (event.getDeltaY() == 0) { return; } double scaleFactor = (event.getDeltaY() > 0) ? SCALE_DELTA : 1/SCALE_DELTA; group.setScaleX(group.getScaleX() * scaleFactor); group.setScaleY(group.getScaleY() * scaleFactor); } }); 

滚动事件处理程序设置在封闭的StackPane上,这是一个可resize的窗格,因此可以展开以填充任何空白空间,并将缩放后的内容保持在窗格的中心位置。 如果您将鼠标滚轮移动到StackPane的任何位置,它将放大或缩小所包含的一组节点。

zoomyzoomyin

 import javafx.application.Application; import javafx.beans.value.*; import javafx.event.*; import javafx.geometry.Bounds; import javafx.scene.*; import javafx.scene.control.*; import javafx.scene.image.*; import javafx.scene.input.*; import javafx.scene.layout.*; import javafx.scene.paint.Color; import javafx.scene.shape.*; import javafx.stage.Stage; public class GraphicsScalingApp extends Application { public static void main(String[] args) { launch(args); } @Override public void start(final Stage stage) { final Group group = new Group( createStar(), createCurve() ); Parent zoomPane = createZoomPane(group); VBox layout = new VBox(); layout.getChildren().setAll( createMenuBar(stage, group), zoomPane ); VBox.setVgrow(zoomPane, Priority.ALWAYS); Scene scene = new Scene( layout ); stage.setTitle("Zoomy"); stage.getIcons().setAll(new Image(APP_ICON)); stage.setScene(scene); stage.show(); } private Parent createZoomPane(final Group group) { final double SCALE_DELTA = 1.1; final StackPane zoomPane = new StackPane(); zoomPane.getChildren().add(group); zoomPane.setOnScroll(new EventHandler<ScrollEvent>() { @Override public void handle(ScrollEvent event) { event.consume(); if (event.getDeltaY() == 0) { return; } double scaleFactor = (event.getDeltaY() > 0) ? SCALE_DELTA : 1/SCALE_DELTA; group.setScaleX(group.getScaleX() * scaleFactor); group.setScaleY(group.getScaleY() * scaleFactor); } }); zoomPane.layoutBoundsProperty().addListener(new ChangeListener<Bounds>() { @Override public void changed(ObservableValue<? extends Bounds> observable, Bounds oldBounds, Bounds bounds) { zoomPane.setClip(new Rectangle(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight())); } }); return zoomPane; } private SVGPath createCurve() { SVGPath ellipticalArc = new SVGPath(); ellipticalArc.setContent( "M10,150 A15 15 180 0 1 70 140 A15 25 180 0 0 130 130 A15 55 180 0 1 190 120" ); ellipticalArc.setStroke(Color.LIGHTGREEN); ellipticalArc.setStrokeWidth(4); ellipticalArc.setFill(null); return ellipticalArc; } private SVGPath createStar() { SVGPath star = new SVGPath(); star.setContent( "M100,10 L100,10 40,180 190,60 10,60 160,180 z" ); star.setStrokeLineJoin(StrokeLineJoin.ROUND); star.setStroke(Color.BLUE); star.setFill(Color.DARKBLUE); star.setStrokeWidth(4); return star; } private MenuBar createMenuBar(final Stage stage, final Group group) { Menu fileMenu = new Menu("_File"); MenuItem exitMenuItem = new MenuItem("E_xit"); exitMenuItem.setGraphic(new ImageView(new Image(CLOSE_ICON))); exitMenuItem.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { stage.close(); } }); fileMenu.getItems().setAll( exitMenuItem ); Menu zoomMenu = new Menu("_Zoom"); MenuItem zoomResetMenuItem = new MenuItem("Zoom _Reset"); zoomResetMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.ESCAPE)); zoomResetMenuItem.setGraphic(new ImageView(new Image(ZOOM_RESET_ICON))); zoomResetMenuItem.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { group.setScaleX(1); group.setScaleY(1); } }); MenuItem zoomInMenuItem = new MenuItem("Zoom _In"); zoomInMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.I)); zoomInMenuItem.setGraphic(new ImageView(new Image(ZOOM_IN_ICON))); zoomInMenuItem.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { group.setScaleX(group.getScaleX() * 1.5); group.setScaleY(group.getScaleY() * 1.5); } }); MenuItem zoomOutMenuItem = new MenuItem("Zoom _Out"); zoomOutMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.O)); zoomOutMenuItem.setGraphic(new ImageView(new Image(ZOOM_OUT_ICON))); zoomOutMenuItem.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { group.setScaleX(group.getScaleX() * 1/1.5); group.setScaleY(group.getScaleY() * 1/1.5); } }); zoomMenu.getItems().setAll( zoomResetMenuItem, zoomInMenuItem, zoomOutMenuItem ); MenuBar menuBar = new MenuBar(); menuBar.getMenus().setAll( fileMenu, zoomMenu ); return menuBar; } // icons source from: http://www.iconarchive.com/show/soft-scraps-icons-by-deleket.html // icon license: CC Attribution-Noncommercial-No Derivate 3.0 =? http://creativecommons.org/licenses/by-nc-nd/3.0/ // icon Commercial usage: Allowed (Author Approval required -> Visit artist website for details). public static final String APP_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/128/Zoom-icon.png"; public static final String ZOOM_RESET_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-icon.png"; public static final String ZOOM_OUT_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-Out-icon.png"; public static final String ZOOM_IN_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-In-icon.png"; public static final String CLOSE_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Button-Close-icon.png"; } 

更新ScrollPane中的缩放节点

上面的实现工作得很好,但是能够将缩放节点放置在滚动窗格中非常有用,因此当放大缩放的节点比可用的视口更大时,您仍然可以平移缩放滚动窗格中的节点以查看节点的部分。

我发现实现放大滚动窗格的行为很困难,所以我在Oracle JavaFX论坛线程上寻求帮助。

Oracle JavaFX论坛用户James_D提出了以下解决scheme,它很好地解决了ScrollPane问题中的缩放问题。

他的意见和代码如下:

首先进行一些小的修改:我将一个StackPane包装在一个Group中,这样ScrollPane就可以根据ScrollPane Javadocs来了解变换的变化。 然后我把StackPane的最小尺寸绑定到视口大小(当视图小于视口时,保持内容居中)。

最初我以为我应该使用缩放变换来缩放显示的中心(即在视口中心的内容上的点)。 但是,我发现我仍然需要修改滚动位置,以保持相同的显示中心,所以我放弃了这一点,并恢复使用setScaleX()和setScaleY()。

诀窍是在缩放之后修复滚动位置。 我计算滚动内容的本地坐标中的滚动偏移量,然后计算缩放后需要的新滚动值。 这有点棘手。 基本观察是(hValue-hMin)/(hMax-hMin)= x /(contentWidth – viewportWidth),其中x是视口的左边缘与内容的左边缘的水平偏移。 然后你有centerX = x + viewportWidth / 2。

缩放之后,旧的centerX的x坐标现在是centerX * scaleFactor。 所以我们只需要设置新的h值就可以成为新的中心。 有一些代数来解决这个问题。

之后,通过拖动平移是非常简单的:)。

添加高级API以支持ScrollPane中的缩放和缩放function的相应function请求是将ScrollPane 添加scaleContentfunction 。 投票或评论function请求,如果你想看到它的实施。

 import javafx.application.Application; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.*; import javafx.event.*; import javafx.geometry.Bounds; import javafx.geometry.Point2D; import javafx.scene.*; import javafx.scene.control.*; import javafx.scene.image.*; import javafx.scene.input.*; import javafx.scene.layout.*; import javafx.scene.paint.Color; import javafx.scene.shape.*; import javafx.stage.Stage; public class GraphicsScalingApp extends Application { public static void main(String[] args) { launch(args); } @Override public void start(final Stage stage) { final Group group = new Group(createStar(), createCurve()); Parent zoomPane = createZoomPane(group); VBox layout = new VBox(); layout.getChildren().setAll(createMenuBar(stage, group), zoomPane); VBox.setVgrow(zoomPane, Priority.ALWAYS); Scene scene = new Scene(layout); stage.setTitle("Zoomy"); stage.getIcons().setAll(new Image(APP_ICON)); stage.setScene(scene); stage.show(); } private Parent createZoomPane(final Group group) { final double SCALE_DELTA = 1.1; final StackPane zoomPane = new StackPane(); zoomPane.getChildren().add(group); final ScrollPane scroller = new ScrollPane(); final Group scrollContent = new Group(zoomPane); scroller.setContent(scrollContent); scroller.viewportBoundsProperty().addListener(new ChangeListener<Bounds>() { @Override public void changed(ObservableValue<? extends Bounds> observable, Bounds oldValue, Bounds newValue) { zoomPane.setMinSize(newValue.getWidth(), newValue.getHeight()); } }); scroller.setPrefViewportWidth(256); scroller.setPrefViewportHeight(256); zoomPane.setOnScroll(new EventHandler<ScrollEvent>() { @Override public void handle(ScrollEvent event) { event.consume(); if (event.getDeltaY() == 0) { return; } double scaleFactor = (event.getDeltaY() > 0) ? SCALE_DELTA : 1 / SCALE_DELTA; // amount of scrolling in each direction in scrollContent coordinate // units Point2D scrollOffset = figureScrollOffset(scrollContent, scroller); group.setScaleX(group.getScaleX() * scaleFactor); group.setScaleY(group.getScaleY() * scaleFactor); // move viewport so that old center remains in the center after the // scaling repositionScroller(scrollContent, scroller, scaleFactor, scrollOffset); } }); // Panning via drag.... final ObjectProperty<Point2D> lastMouseCoordinates = new SimpleObjectProperty<Point2D>(); scrollContent.setOnMousePressed(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { lastMouseCoordinates.set(new Point2D(event.getX(), event.getY())); } }); scrollContent.setOnMouseDragged(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { double deltaX = event.getX() - lastMouseCoordinates.get().getX(); double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth(); double deltaH = deltaX * (scroller.getHmax() - scroller.getHmin()) / extraWidth; double desiredH = scroller.getHvalue() - deltaH; scroller.setHvalue(Math.max(0, Math.min(scroller.getHmax(), desiredH))); double deltaY = event.getY() - lastMouseCoordinates.get().getY(); double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight(); double deltaV = deltaY * (scroller.getHmax() - scroller.getHmin()) / extraHeight; double desiredV = scroller.getVvalue() - deltaV; scroller.setVvalue(Math.max(0, Math.min(scroller.getVmax(), desiredV))); } }); return scroller; } private Point2D figureScrollOffset(Node scrollContent, ScrollPane scroller) { double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth(); double hScrollProportion = (scroller.getHvalue() - scroller.getHmin()) / (scroller.getHmax() - scroller.getHmin()); double scrollXOffset = hScrollProportion * Math.max(0, extraWidth); double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight(); double vScrollProportion = (scroller.getVvalue() - scroller.getVmin()) / (scroller.getVmax() - scroller.getVmin()); double scrollYOffset = vScrollProportion * Math.max(0, extraHeight); return new Point2D(scrollXOffset, scrollYOffset); } private void repositionScroller(Node scrollContent, ScrollPane scroller, double scaleFactor, Point2D scrollOffset) { double scrollXOffset = scrollOffset.getX(); double scrollYOffset = scrollOffset.getY(); double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth(); if (extraWidth > 0) { double halfWidth = scroller.getViewportBounds().getWidth() / 2 ; double newScrollXOffset = (scaleFactor - 1) * halfWidth + scaleFactor * scrollXOffset; scroller.setHvalue(scroller.getHmin() + newScrollXOffset * (scroller.getHmax() - scroller.getHmin()) / extraWidth); } else { scroller.setHvalue(scroller.getHmin()); } double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight(); if (extraHeight > 0) { double halfHeight = scroller.getViewportBounds().getHeight() / 2 ; double newScrollYOffset = (scaleFactor - 1) * halfHeight + scaleFactor * scrollYOffset; scroller.setVvalue(scroller.getVmin() + newScrollYOffset * (scroller.getVmax() - scroller.getVmin()) / extraHeight); } else { scroller.setHvalue(scroller.getHmin()); } } private SVGPath createCurve() { SVGPath ellipticalArc = new SVGPath(); ellipticalArc.setContent("M10,150 A15 15 180 0 1 70 140 A15 25 180 0 0 130 130 A15 55 180 0 1 190 120"); ellipticalArc.setStroke(Color.LIGHTGREEN); ellipticalArc.setStrokeWidth(4); ellipticalArc.setFill(null); return ellipticalArc; } private SVGPath createStar() { SVGPath star = new SVGPath(); star.setContent("M100,10 L100,10 40,180 190,60 10,60 160,180 z"); star.setStrokeLineJoin(StrokeLineJoin.ROUND); star.setStroke(Color.BLUE); star.setFill(Color.DARKBLUE); star.setStrokeWidth(4); return star; } private MenuBar createMenuBar(final Stage stage, final Group group) { Menu fileMenu = new Menu("_File"); MenuItem exitMenuItem = new MenuItem("E_xit"); exitMenuItem.setGraphic(new ImageView(new Image(CLOSE_ICON))); exitMenuItem.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { stage.close(); } }); fileMenu.getItems().setAll(exitMenuItem); Menu zoomMenu = new Menu("_Zoom"); MenuItem zoomResetMenuItem = new MenuItem("Zoom _Reset"); zoomResetMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.ESCAPE)); zoomResetMenuItem.setGraphic(new ImageView(new Image(ZOOM_RESET_ICON))); zoomResetMenuItem.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { group.setScaleX(1); group.setScaleY(1); } }); MenuItem zoomInMenuItem = new MenuItem("Zoom _In"); zoomInMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.I)); zoomInMenuItem.setGraphic(new ImageView(new Image(ZOOM_IN_ICON))); zoomInMenuItem.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { group.setScaleX(group.getScaleX() * 1.5); group.setScaleY(group.getScaleY() * 1.5); } }); MenuItem zoomOutMenuItem = new MenuItem("Zoom _Out"); zoomOutMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.O)); zoomOutMenuItem.setGraphic(new ImageView(new Image(ZOOM_OUT_ICON))); zoomOutMenuItem.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { group.setScaleX(group.getScaleX() * 1 / 1.5); group.setScaleY(group.getScaleY() * 1 / 1.5); } }); zoomMenu.getItems().setAll(zoomResetMenuItem, zoomInMenuItem, zoomOutMenuItem); MenuBar menuBar = new MenuBar(); menuBar.getMenus().setAll(fileMenu, zoomMenu); return menuBar; } // icons source from: // http://www.iconarchive.com/show/soft-scraps-icons-by-deleket.html // icon license: CC Attribution-Noncommercial-No Derivate 3.0 =? // http://creativecommons.org/licenses/by-nc-nd/3.0/ // icon Commercial usage: Allowed (Author Approval required -> Visit artist // website for details). public static final String APP_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/128/Zoom-icon.png"; public static final String ZOOM_RESET_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-icon.png"; public static final String ZOOM_OUT_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-Out-icon.png"; public static final String ZOOM_IN_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-In-icon.png"; public static final String CLOSE_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Button-Close-icon.png"; } 

如果zoomPane中原始内容的大小已经大于View Port,那么jewelsea的答案就有一个问题。 那么下面的代码将不起作用。 zoomPane.setMinSize(newValue.getWidth(),newValue.getHeight());

结果是当我们缩小,内容不再居中。

要解决此问题,您需要在zoomPane和ScrollPane之间创build另一个StackPane。

  // Create a zoom pane for zoom in/out final StackPane zoomPane = new StackPane(); zoomPane.getChildren().add(group); final Group zoomContent = new Group(zoomPane); // Create a pane for holding the content, when the content is smaller than the view port, // it will stay the view port size, make sure the content is centered final StackPane canvasPane = new StackPane(); canvasPane.getChildren().add(zoomContent); final Group scrollContent = new Group(canvasPane); // Scroll pane for scrolling scroller = new ScrollPane(); scroller.setContent(scrollContent); 

并在viewportBoundsProperty侦听器中,将zoomPane更改为canvasPane

 // Set the minimum canvas size canvasPane.setMinSize(newValue.getWidth(), newValue.getHeight()); 

JavaFx太放大/缩小了。 为了达到同样的效果,WPF要容易得多。