将旋转的 ImageView 安装到应用程序 Window / 场景中
Fitting rotated ImageView into Application Window / Scene
在 JavaFX 中,我试图在应用程序中显示旋转的 ImageView Window。
因此,我将它放入一个 stackPane 中以使其始终居中,并且我已将 ImageView 的 widths/heights 和 stackPane 绑定到场景的 width/height 以尽可能大地查看它。
只要图像不旋转,这就可以正常工作。
一旦我使用 stackPane.setRotate(90) 将图像旋转 90°(并交换 width/height 的绑定),stackPane 就不再绑定到应用程序 Window 的左上角(或场景)。
如何正确放置旋转后的图像?
在示例代码中,[任意键] 将切换旋转 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();
}
}
结果:
如果不旋转,stackPane(黑色)完全适合 window,即使用鼠标调整 window 的大小,图像也具有正确的大小。
按下 [任意键] 后,stackPane 被旋转。
stackPane(黑色)似乎具有正确的 width/height,图像似乎也被正确旋转。但是stackPane已经不在左上角了???当用鼠标调整 window 大小时,它会四处移动???
为什么不直接将 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();
}
我找到了解决办法:-) Fabian 的方法启发了我(谢谢!!)还有我的老朋友 Pit 帮我调试了(也谢谢你!!)
当 resize() 应用于旋转的窗格(甚至节点 - 我没有尝试过)时,JavaFX 的布局位置算法似乎有问题:
按照 Fabian 的想法,我调试了 class Pane 的 layoutChildren() 方法。我发现 setRotate() 之后的重定位是正确的,并按预期保持子窗格的中心。但是一旦调用 resize() (这是因为将旋转的子窗格再次放入其父窗格并且总是在用户调整 window 大小时),原点计算就会出错:
上图以绿色描绘了 setRotate(90)、resize() 和 relocate() 的序列,以蓝色描绘了 setRotate(270) 的序列。在 1024x786 示例中,一个小 blue/green 圆圈描绘了相应的原点及其坐标。
分析
似乎为了计算 Pane resize() 的位置不使用 BoundsInParent-属性(参见 Node 的 JavaFX-Docu)的高度和宽度,而是使用 getWidth() 和 getHeight()这似乎反映了 BoundsInLocal。因此,对于 90° 或 270° 的旋转,高度和宽度似乎可以互换。因此,当 resize() 尝试在调整大小后再次使子窗格居中时,新原点的计算误差只是宽度和高度之间的差值 (delta=(width-height)/2) 的一半。
解决方案
对于旋转为 90 或 270 度的窗格,在调整大小后需要应用重定位(delta,-delta)。
我的实现结构遵循 Fabian 的基本思想:我构建了一个布局器 RotatablePaneLayouter:Region,它只是覆盖了 layoutChildren() 方法。在其构造函数中,它获得一个 Pane(在我的示例中为 StackPane),它可以包含任意数量的子项(在我的示例中为 ImageView)并且可以旋转。
LayoutChildren() 然后只对子窗格执行 resize() 和 relocate() 以使其完全适合 RotateablePaneLayouter 并遵守子窗格的方向。
布局助手(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);
}
}
}
使用方法:先将要旋转的Pane放入Layouter中,不要直接放置Pane。
这里是示例主程序的代码。您可以使用 space 栏将子窗格旋转 90 度、180 度、270 度,然后再旋转 0 度。您还可以使用鼠标调整 window 的大小。布局器总是设法正确放置旋转的窗格。
布局器使用示例
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();
}
}
对我来说,这似乎是 resize() 中 javaFX 错误的解决方法。
在 JavaFX 中,我试图在应用程序中显示旋转的 ImageView Window。 因此,我将它放入一个 stackPane 中以使其始终居中,并且我已将 ImageView 的 widths/heights 和 stackPane 绑定到场景的 width/height 以尽可能大地查看它。
只要图像不旋转,这就可以正常工作。
一旦我使用 stackPane.setRotate(90) 将图像旋转 90°(并交换 width/height 的绑定),stackPane 就不再绑定到应用程序 Window 的左上角(或场景)。
如何正确放置旋转后的图像?
在示例代码中,[任意键] 将切换旋转 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();
}
}
结果:
如果不旋转,stackPane(黑色)完全适合 window,即使用鼠标调整 window 的大小,图像也具有正确的大小。
按下 [任意键] 后,stackPane 被旋转。 stackPane(黑色)似乎具有正确的 width/height,图像似乎也被正确旋转。但是stackPane已经不在左上角了???当用鼠标调整 window 大小时,它会四处移动???
为什么不直接将 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();
}
我找到了解决办法:-) Fabian 的方法启发了我(谢谢!!)还有我的老朋友 Pit 帮我调试了(也谢谢你!!)
当 resize() 应用于旋转的窗格(甚至节点 - 我没有尝试过)时,JavaFX 的布局位置算法似乎有问题:
按照 Fabian 的想法,我调试了 class Pane 的 layoutChildren() 方法。我发现 setRotate() 之后的重定位是正确的,并按预期保持子窗格的中心。但是一旦调用 resize() (这是因为将旋转的子窗格再次放入其父窗格并且总是在用户调整 window 大小时),原点计算就会出错:
上图以绿色描绘了 setRotate(90)、resize() 和 relocate() 的序列,以蓝色描绘了 setRotate(270) 的序列。在 1024x786 示例中,一个小 blue/green 圆圈描绘了相应的原点及其坐标。
分析
似乎为了计算 Pane resize() 的位置不使用 BoundsInParent-属性(参见 Node 的 JavaFX-Docu)的高度和宽度,而是使用 getWidth() 和 getHeight()这似乎反映了 BoundsInLocal。因此,对于 90° 或 270° 的旋转,高度和宽度似乎可以互换。因此,当 resize() 尝试在调整大小后再次使子窗格居中时,新原点的计算误差只是宽度和高度之间的差值 (delta=(width-height)/2) 的一半。
解决方案
对于旋转为 90 或 270 度的窗格,在调整大小后需要应用重定位(delta,-delta)。
我的实现结构遵循 Fabian 的基本思想:我构建了一个布局器 RotatablePaneLayouter:Region,它只是覆盖了 layoutChildren() 方法。在其构造函数中,它获得一个 Pane(在我的示例中为 StackPane),它可以包含任意数量的子项(在我的示例中为 ImageView)并且可以旋转。
LayoutChildren() 然后只对子窗格执行 resize() 和 relocate() 以使其完全适合 RotateablePaneLayouter 并遵守子窗格的方向。
布局助手(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);
}
}
}
使用方法:先将要旋转的Pane放入Layouter中,不要直接放置Pane。
这里是示例主程序的代码。您可以使用 space 栏将子窗格旋转 90 度、180 度、270 度,然后再旋转 0 度。您还可以使用鼠标调整 window 的大小。布局器总是设法正确放置旋转的窗格。
布局器使用示例
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();
}
}
对我来说,这似乎是 resize() 中 javaFX 错误的解决方法。