Подтвердить что ты не робот

Правильное масштабирование JavaFX

Я хочу масштабировать все узлы в панели в событии прокрутки.

То, что я пробовал до сих пор:

  • Когда я делаю scaleX или scaleY, граница области шкалы соответственно (видно при установке стиля панели -fx-border-color: black;). Поэтому не каждое событие начнется, если я не с границ панели, поэтому мне нужно все это. enter image description here

  • Следующий шаг я попытался масштабировать каждый node, и получилось очень плохо, что-то вроде этого (линии растянуты через точки). Или если прокрутка с другой стороны, это будет меньше enter image description here

  • Другим методом, который я пытался, было масштабирование точек Node. Это лучше, но Мне это не нравится. Это выглядит как point.setScaleX(point.getScaleX()+scaleX), а для y и других узлов соответственно.

4b9b3361

Ответ 1

Я создал пример приложения, чтобы продемонстрировать один подход к выполнению масштабирования node в окне просмотра в событии прокрутки (например, прокручивание и выключение путем вращения колеса мыши).

Ключ-логика к образцу для масштабирования группы, помещенной в 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, которая является изменяемой областью, поэтому она расширяется, чтобы заполнить любое пустое пространство, сохраняя масштабированное содержимое в центре. Если вы перемещаете колесико мыши в любом месте 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";
}

Обновление для масштабированного node в ScrollPane

Вышеупомянутая реализация работает хорошо, насколько это возможно, но полезно разместить увеличенный node внутри панели прокрутки, чтобы при увеличении увеличенного изображения node больше, чем у вашего доступного окна просмотра, вы можете перемещаться по масштабированному node в панели прокрутки для просмотра частей node.

Я обнаружил, что трудно добиться масштабирования в области прокрутки, поэтому я попросил о помощи в форуме JavaFX Forum.

Пользователь JavaFX для Oracle JavaFX разработал следующее решение, которое довольно хорошо решает масштабирование в пределах проблемы с ScrollPane.

Его комментарии и код были следующими:

Сначала выполняется несколько незначительных изменений: я завернул StackPane в группе, чтобы ScrollPane узнал об изменениях в преобразованиях, как в ScrollPane Javadocs. И затем я привязал минимальный размер StackPane к размеру видового экрана (сохраняя содержимое в центре, когда оно меньше, чем область просмотра).

Первоначально я думал, что должен использовать масштабное преобразование для масштабирования отображаемого центра (т.е. точки на контенте, находящемся в центре окна просмотра). Но я обнаружил, что мне по-прежнему необходимо зафиксировать положение прокрутки, чтобы сохранить тот же отображаемый центр, поэтому я отказался от этого и вернулся к использованию setScaleX() и setScaleY().

Хитрость заключается в фиксации положения прокрутки после масштабирования. Я вычислил смещение прокрутки в локальных координатах содержимого прокрутки, а затем вычислил новые значения прокрутки, необходимые после масштабирования. Это было немного сложно. Основное замечание состоит в том, что (hValue-hMin)/(hMax-hMin) = x/(contentWidth - viewportWidth), где x - горизонтальное смещение левого края окна просмотра от левого края содержимого. Тогда у вас есть centerX = x + viewportWidth/2.

После масштабирования координата x старого центра X теперь находится в центре X * scaleFactor. Поэтому нам просто нужно установить новый hValue для создания нового центра. Там немного алгебры, чтобы понять это.

После этого панорамирование путем перетаскивания было довольно простым:).

Соответствующий запрос функции добавления API высокого уровня для поддержки масштабирования и масштабирования в ScrollPane Добавить функцию scaleContent для ScrollPane. Проголосуйте за или запросите комментарий к функции, если хотите, чтобы она была реализована.

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";
}

Ответ 2

Ответ от jewelsea имеет одну проблему, если размер исходного контента в области масштабирования уже больше, чем View Port. Тогда следующий код не будет работать. zoomPane.setMinSize(newValue.getWidth(), newValue.getHeight());

В результате мы уменьшаем масштаб изображения, содержимое больше не центрируется.

Чтобы устранить эту проблему, вам нужно создать еще одну StackPane между зоной масштабирования и 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);

И в прослушивателе viewportBoundsProperty, измените область масштабирования на canvasPane

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

JavaFx слишком сложна для увеличения/уменьшения масштаба. Для достижения такого же эффекта WPF намного проще.