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

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

Что я уже пробовал:

  1. Когда я делаю scaleX или scaleY, граница панели масштабируется соответственно (видно при установке стиля панели -fx-border-color: black;). Так что не каждое событие началось бы, если бы я не выходил за границы панели, поэтому мне все это нужно.

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

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

2 ответа

Решение

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

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

Обновление для увеличенного узла в ScrollPane

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

Мне было трудно добиться масштабирования в области прокрутки, поэтому я попросил помощи в ветке форума Oracle JavaFX.

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

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

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

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

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

После масштабирования координата x старого centerX теперь становится centerX*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";
}

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

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

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

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

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

Другие вопросы по тегам