Установка повернутого ImageView в окно приложения / сцену - PullRequest
0 голосов
/ 02 ноября 2018

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

Это прекрасно работает, как только изображение не поворачивается.
Как только я поверну изображение на 90 °, используя stackPane.setRotate (90) (и обменяю привязку на ширину / высоту), stackPane больше не привязывается к левому верхнему углу окна приложения (или сцены).

Что я могу сделать, чтобы правильно разместить повернутое изображение?

В примере кода [любая клавиша] переключает поворот на 90 ° / 0 °, поэтому проблема с местоположением повернутого изображения становится видимой:

public class RotationTest extends Application {

  boolean rotated = false;

  public static void main(String[] args) {
    Application.launch(args);
  }

  @Override
  public void start(Stage primaryStage) {
    primaryStage.setTitle("Rotation test");

    Group root = new Group();
    Scene scene = new Scene(root, 1024,768);

    //a stackPane is used to center the image
    StackPane stackPane = new StackPane();
    stackPane.setStyle("-fx-background-color: black;");

    stackPane.prefHeightProperty().bind(scene.heightProperty());
    stackPane.prefWidthProperty().bind(scene.widthProperty());

    scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
    @Override
      public void handle(KeyEvent event) {
        //toggle rotate 90° / no rotation
        rotated = !rotated;

        stackPane.prefHeightProperty().unbind();
        stackPane.prefWidthProperty().unbind();

        if (rotated){
          stackPane.setRotate(90);
          //rotation: exchange width and height for binding to scene
          stackPane.prefWidthProperty().bind(scene.heightProperty());
          stackPane.prefHeightProperty().bind(scene.widthProperty());
        }else{
          stackPane.setRotate(0);
          //no rotation: height is height and width is width
          stackPane.prefHeightProperty().bind(scene.heightProperty());
          stackPane.prefWidthProperty().bind(scene.widthProperty());
        }
      }
    });


    final ImageView imageView = new ImageView("file:D:/test.jpg");
    imageView.setPreserveRatio(true);
    imageView.fitWidthProperty().bind(stackPane.prefWidthProperty());
    imageView.fitHeightProperty().bind(stackPane.prefHeightProperty());

    stackPane.getChildren().add(imageView);

    root.getChildren().add(stackPane);
    primaryStage.setScene(scene);
    primaryStage.show();

  }
}

Результаты:

Result with no Rotation

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

Result with Rotation of stackPane by 90 degree

После нажатия [любой клавиши] стек стека поворачивается.

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

Ответы [ 2 ]

0 голосов
/ 14 ноября 2018

Я нашел решение :-) Подход Фабиана вдохновил меня (спасибо !!) И мой старый друг Пит помог мне с отладкой (также спасибо !!)

Кажется, что у алгоритма расположения макета JavaFX есть проблема, когда resize () применяется к повернутым панелям (или даже узлам - я не пробовал):

Origin movement following setRotate and resize

Следуя идее Фабиана, я отладил метод layoutChildren () класса Pane. Я обнаружил, что перемещение после setRotate () является правильным и сохраняет центр дочерней панели, как и ожидалось. Но как только вызывается resize () (что делается из-за повторной подгонки повернутой дочерней панели к ее отцу и, кроме того, всегда, когда пользователь изменяет размеры окна), исходное вычисление становится неверным:

На рисунке выше изображена последовательность setRotate (90), resize () и relocate () зеленого цвета и то же самое для setRotate (270) синим цветом. Маленький сине-зеленый кружок изображает соответствующий источник вместе с его координатами в примере 1024x786.

Анализ

Похоже, что для вычисления положение панели resize () использует не высоту и ширину из BoundsInParent-Property (см. JavaFX-Docu of Node), а из getWidth () и getHeight (), которые, кажется, отражают BoundsInLocal. Как следствие, для поворотов на 90 ° или 270 ° высота и ширина кажутся взаимозаменяемыми. Следовательно, ошибка в расчете для нового источника составляет только половину разницы между шириной и высотой (delta = (width-height) / 2), когда resize () пытается снова центрировать дочернюю панель после изменения размера.

Решение

Перемещение (delta, -delta) необходимо применять после изменения размера панелей с поворотом = 90 или 270 градусов.

Структура моей реализации следует основной идее Фабиана: я создал макет RotatablePaneLayouter: Region, который просто перезаписывает метод layoutChildren (). В своем конструкторе он получает панель (в моем примере StackPane), которая может содержать любое количество дочерних элементов (в моем примере ImageView) и которую можно вращать.

Затем LayoutChildren () просто выполняет resize () и relocate () для дочерней панели, чтобы полностью разместить ее в RotateablePaneLayouter с учетом ориентации дочерней панели.

Помощник Layouter (RotateablePaneLayouter: Region)

public class RotatablePaneLayouter extends Region {
  private Pane child;

  public RotatablePaneLayouter(Pane child) {
    getChildren().add(child);
    this.child = child;

    // make sure layout gets invalidated when the child orientation changes
    child.rotateProperty().addListener(new ChangeListener<Number>() {
      @Override
      public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
        requestLayout();
      }
    });
  }

  @Override
  protected void layoutChildren() {
    // set fit sizes:
    //resize child to fit into RotatablePane and correct movement caused by resizing if necessary
    if ((child.getRotate() == 90)||(child.getRotate() == 270)) {
      //vertical
      child.resize( getHeight(), getWidth() ); //exchange width and height
      // and relocate to correct movement caused by resizing
      double delta = (getWidth() - getHeight()) / 2;
      child.relocate(delta,-delta);
    } else {
      //horizontal
      child.resize( getWidth(), getHeight() ); //keep width and height
      //with 0° or 180° resize does no movement to be corrected
      child.relocate(0,0);
    }
  }
}

Чтобы использовать его: сначала поместите панель, которую нужно повернуть, в Layouter, а не устанавливайте панель напрямую.

Здесь код основной программы примера. Вы можете использовать пробел, чтобы повернуть дочернюю панель на 90, 180, 270 и снова на 0 градусов. Вы также можете изменить размер окна с помощью мыши. Layouter всегда удается правильно разместить повернутую панель.

Пример использования Layouter

public class RotationTest extends Application {
  public static void main(String[] args) {
   Application.launch(args);
  }

  @Override
  public void start(Stage primaryStage) {

    //image in a StackPane to be rotated
    final ImageView imageView = new ImageView("file:D:/Test_org.jpg");
    imageView.setPreserveRatio(true);
    StackPane stackPane = new StackPane(imageView); //a stackPane is used to center the image
    stackPane.setStyle("-fx-background-color: black;");
    imageView.fitWidthProperty().bind(stackPane.widthProperty());
    imageView.fitHeightProperty().bind(stackPane.heightProperty());

    //container for layouting rotated Panes
    RotatablePaneLayouter root = new RotatablePaneLayouter(stackPane);
    root.setStyle("-fx-background-color: blue;");

    Scene scene = new Scene(root, 1024,768);

    scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
      @Override
      public void handle(KeyEvent event) {
        if (event.getCode() == KeyCode.SPACE) {
          //rotate additionally 90°
          stackPane.setRotate((stackPane.getRotate() + 90) % 360);
        }
      }
    });

    primaryStage.setTitle("Rotation test");
    primaryStage.setScene(scene);
    primaryStage.show();
  }
}

Для меня это похоже на обход ошибки javaFX в resize ().

0 голосов
/ 02 ноября 2018

Почему бы просто не оставить Group и предпочтительные размеры вне уравнения?

Корень автоматически изменяется в соответствии со сценой, и вы можете использовать его свойства width / height, чтобы связать свойства fitWidth и fitHeight:

private static void setRotated(boolean rotated, ImageView targetNode, Pane parent) {
    double angle;
    if (rotated) {
        angle = 90;
        targetNode.fitWidthProperty().bind(parent.heightProperty());
        targetNode.fitHeightProperty().bind(parent.widthProperty());
    } else {
        angle = 0;
        targetNode.fitWidthProperty().bind(parent.widthProperty());
        targetNode.fitHeightProperty().bind(parent.heightProperty());
    }
    targetNode.setRotate(angle);
}

@Override
public void start(Stage primaryStage) {
    Image image = new Image("file:D:/test.jpg");
    ImageView imageView = new ImageView(image);
    imageView.setPreserveRatio(true);

    StackPane root = new StackPane(imageView);
    root.setStyle("-fx-background-color: black;");

    // initialize unrotated
    setRotated(false, imageView, root);

    Scene scene = new Scene(root, 1024, 768);

    scene.setOnKeyPressed(evt -> {
        // toggle between 0° and 90° rotation
        setRotated(imageView.getRotate() == 0, imageView, root);
    });

    primaryStage.setScene(scene);
    primaryStage.show();
}

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

Вы могли бы реализовать свой собственный регион, хотя бы исправить это:

public class CenteredImage extends Region {
    private final BooleanProperty rotated = new SimpleBooleanProperty();
    private final ImageView imageView = new ImageView();

    public CenteredImage() {
        // make sure layout gets invalidated when the image changes
        InvalidationListener listener = o -> requestLayout();
        imageProperty().addListener(listener);
        rotated.addListener((o, oldValue, newValue) -> {
            imageView.setRotate(newValue ? 90 : 0);
            requestLayout();
        });
        getChildren().add(imageView);
        imageView.setPreserveRatio(true);
    }

    public final BooleanProperty rotatedProperty() {
        return rotated;
    }

    public final void setRotated(boolean value) {
        this.rotated.set(value);
    }

    public boolean isRotated() {
        return rotated.get();
    }

    public final void setImage(Image value) {
        imageView.setImage(value);
    }

    public final Image getImage() {
        return imageView.getImage();
    }

    public final ObjectProperty<Image> imageProperty() {
        return imageView.imageProperty();
    }

    @Override
    protected double computeMinWidth(double height) {
        return 0;
    }

    @Override
    protected double computeMinHeight(double width) {
        return 0;
    }

    @Override
    protected double computePrefWidth(double height) {
        Image image = getImage();
        Insets insets = getInsets();

        double add = 0;
        if (image != null && height > 0) {
            height -= insets.getBottom() + insets.getTop();
            add = isRotated()
                    ? height / image.getWidth()  * image.getHeight()
                    : height / image.getHeight()  * image.getWidth();
        }

        return insets.getLeft() + insets.getRight() + add;
    }

    @Override
    protected double computePrefHeight(double width) {
        Image image = getImage();
        Insets insets = getInsets();

        double add = 0;
        if (image != null && width > 0) {
            width -= insets.getLeft() + insets.getRight();
            add = isRotated()
                    ? width / image.getHeight()  * image.getWidth()
                    : width / image.getWidth()  * image.getHeight();
        }

        return insets.getTop() + insets.getBottom() + add;
    }

    @Override
    protected double computeMaxWidth(double height) {
        return Double.MAX_VALUE;
    }

    @Override
    protected double computeMaxHeight(double width) {
        return Double.MAX_VALUE;
    }

    @Override
    protected void layoutChildren() {
        Insets insets = getInsets();
        double left = insets.getLeft();
        double top = insets.getTop();
        double availableWidth = getWidth() - left - insets.getRight();
        double availableHeight = getHeight() - top - insets.getBottom();

        // set fit sizes
        if (isRotated()) {
            imageView.setFitWidth(availableHeight);
            imageView.setFitHeight(availableWidth);
        } else {
            imageView.setFitWidth(availableWidth);
            imageView.setFitHeight(availableHeight);
        }

        // place image
        layoutInArea(imageView, left, top, availableWidth, availableHeight, 0, null, false,
                false, HPos.CENTER, VPos.CENTER);

    }

}
@Override
public void start(Stage primaryStage) {
    Image image = new Image("file:D:/test.jpg");
    ImageView imageView = new ImageView(image);
    imageView.setPreserveRatio(true);

    CenteredImage imageArea = new CenteredImage();
    imageArea.setImage(image);

    imageArea.setStyle("-fx-background-color: black;");

    imageArea.setPrefWidth(300);

    SplitPane splitPane = new SplitPane(new Region(), imageArea);
    SplitPane.setResizableWithParent(imageArea, true);

    Scene scene = new Scene(splitPane, 1024, 768);

    scene.setOnKeyPressed(evt -> {
        // toggle between 0° and 90° rotation
        imageArea.setRotated(!imageArea.isRotated());
    });

    primaryStage.setScene(scene);
    primaryStage.show();
}
...