Escala correta do JavaFX

Eu quero dimensionar todos os nós em um painel em um evento de rolagem.

O que eu tentei até agora:

  1. Quando eu faço scaleX ou scaleY, a borda do painel é dimensionada respectivamente (vista quando definir o estilo do -fx-border-color: black; ). Portanto, nem todo evento começaria se eu não estivesse nas bordas do painel, então preciso de tudo. insira a descrição da imagem aqui

  2. Próximo passo tentei escalar cada nó e ficou muito ruim, algo assim – (linhas esticadas através dos pontos). Ou se rolando no outro lado, seria menos insira a descrição da imagem aqui

  3. Outro método que eu tentei foi escalar pontos do Node. É melhor, mas eu não gosto disso. Parece point.setScaleX(point.getScaleX()+scaleX) e para y e outros nós apropriadamente.

Eu criei um aplicativo de exemplo para demonstrar uma abordagem para executar o dimensionamento de um nó em uma viewport em um evento de rolagem (por exemplo, rolar para dentro e para fora rolando a roda do mouse).

A lógica principal da amostra para dimensionar um grupo colocado em um StackPane:

 final double SCALE_DELTA = 1.1; final StackPane zoomPane = new StackPane(); zoomPane.getChildren().add(group); zoomPane.setOnScroll(new EventHandler() { @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); } }); 

O manipulador de events de rolagem é definido no StackPane de fechamento, que é um painel redimensionável para que seja expandido para preencher qualquer espaço vazio, mantendo o conteúdo ampliado centralizado no painel. Se você mover a roda do mouse para qualquer lugar dentro do StackPane, ele aumentará ou diminuirá o grupo de nós.

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() { @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() { @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() { @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() { @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() { @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() { @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"; } 

Atualização para um nó com zoom em um ScrollPane

A implementação acima funciona bem, mas é útil poder colocar o nó ampliado dentro de um painel de rolagem, de modo que, ao aumentar o zoom, tornando o nó com zoom maior do que a janela de visualização disponível, você ainda pode percorrer o nó com zoom no painel de rolagem para exibir partes do nó.

Achei difícil conseguir o comportamento de zoom em um painel de rolagem, então pedi ajuda em um thread do Oracle JavaFX Forum .

O usuário do fórum do Oracle JavaFX, James_D, apresentou a seguinte solução, que resolve muito bem o zoom em um problema do ScrollPane.

Seus comentários e códigos foram os seguintes:

Algumas pequenas alterações primeiro: eu envolvi o StackPane em um grupo para que o ScrollPane estivesse ciente das alterações nas transformações, de acordo com os Javadocs do ScrollPane. E então limitei o tamanho mínimo do StackPane ao tamanho da viewport (mantendo o conteúdo centralizado quando menor que a viewport).

Inicialmente, achei que deveria usar uma transformação Escala para aplicar zoom ao redor do centro exibido (ou seja, o ponto no conteúdo que está no centro da viewport). Mas eu descobri que ainda precisava corrigir a posição de rolagem para manter o mesmo centro exibido, então abandonei isso e voltei a usar setScaleX () e setScaleY ().

O truque é corrigir a posição de rolagem após o dimensionamento. Calculei o deslocamento de rolagem nas coordenadas locais do conteúdo de rolagem e depois calculei os novos valores de rolagem necessários após a escala. Isso foi um pouco complicado. A observação básica é que (hValue-hMin) / (hMax-hMin) = x / (contentWidth – viewportWidth), em que x é o deslocamento horizontal da borda esquerda da viewport a partir da borda esquerda do conteúdo. Então você tem centerX = x + viewportWidth / 2.

Após o dimensionamento, a coordenada x do centerX antigo é agora centerX * scaleFactor. Então, só temos que definir o novo hValue para fazer o novo centro. Há um pouco de álgebra para descobrir isso.

Depois disso, o panning arrastando foi muito fácil :).

Uma solicitação de recurso correspondente para adicionar APIs de alto nível para oferecer suporte à funcionalidade de zoom e dimensionamento em um ScrollPane é Adicionar funcionalidade scaleContent ao ScrollPane . Vote ou comente na solicitação de recurso se você quiser vê-la implementada.

 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() { @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() { @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 lastMouseCoordinates = new SimpleObjectProperty(); scrollContent.setOnMousePressed(new EventHandler() { @Override public void handle(MouseEvent event) { lastMouseCoordinates.set(new Point2D(event.getX(), event.getY())); } }); scrollContent.setOnMouseDragged(new EventHandler() { @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() { @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() { @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() { @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() { @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"; } 

A resposta de jewelsea tem um problema, se o tamanho do conteúdo original no zoomPane já for maior que View Port. Então o seguinte código não irá funcionar. zoomPane.setMinSize (newValue.getWidth (), newValue.getHeight ());

O resultado é quando diminuímos o zoom, o conteúdo não é mais centrado.

Para resolver esse problema, você precisa criar outro StackPane entre o zoomPane e o ScrollPane.

  // 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); 

E no ouvinte viewportBoundsProperty, Alterar zoomPane para canvasPane

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

O JavaFx é muito complicado para zoom in / out. Para conseguir o mesmo efeito, o WPF é muito mais fácil.