2

I am trying to learn how to use javafx and I've come across some behaviours of javafx code that don't really make sense to me. I hope you can explain why javafx code produces these behaviours and how to get around them.

For illustrative purposes I wrote a small example program:

import javafx.application.Application;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.SplitPane;
import javafx.scene.control.ToggleButton;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import javafx.stage.Stage;

public class Example extends Application {
    @Override
    public void start(Stage primaryStage) {
        // Top section with image
        ImageView imageView = new ImageView(new Image("http://wfarm2.dataknet.com/static/resources/icons/set114/b4f29385.png"));
            imageView.setPreserveRatio(true);
        StackPane stackPane = new StackPane(imageView);
            imageView.fitWidthProperty().bind(stackPane.widthProperty());
            imageView.fitHeightProperty().bind(stackPane.heightProperty());
            stackPane.getStyleClass().add("stack-pane");

        // Bottom Section with CSS toggle
        ToggleButton toggle = new ToggleButton("Show CSS");
        HBox hbox = new HBox(toggle);
            hbox.setAlignment(Pos.CENTER);
            hbox.getStyleClass().add("h-box");

        // Combines the two sections
        SplitPane root = new SplitPane(stackPane, hbox);
            root.setOrientation(Orientation.VERTICAL);
            root.setResizableWithParent(hbox, false);
        Scene scene = new Scene(root);
            toggle.setOnMouseClicked(e -> {
                if (toggle.isSelected()) {
                    scene.getStylesheets().add("StyleSheet.css");
                    toggle.setText("Disable CSS");
                } else {
                    scene.getStylesheets().remove(0);
                    toggle.setText("Show CSS");
                }
            });
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

The StyleSheet.css file contains only contains these three lines:

.stack-pane {  -fx-background-color: aqua;  }
.split-pane {  -fx-background-color: crimson;  }
.h-box {  -fx-background-color: chartreuse;  }

Originally I thought this program would display an image in top section which - like the stackpane - would always be resized when the window/stage is resized. Unfortunately, this is not quite what happens: The stackpane as well as the image is not properly resized when the window/stage is shrunk. In fact, even the position of the stackpane seems to be offset and not properly aligned with the splitpane.

The following pictures are meant to serve as a visual reference: What it looks like:

  1. When the window/stage is shrunk.

    black

  2. When the window/stage is shrunk and colored.

    colored

As you can see, the reality does not meet my original expectations.

  1. Do you know why this is the case?
  2. How could one fix the example program?
  3. Is the window/stage simply not meant meant to be shrunk after startup?
  4. Can one even fix this with javafx?

Something else that I've noticed is that the image does not shrink when the window/stage is shrunk. Do you know the reason behind that? Is there a fix?

jewelsea
  • 150,031
  • 14
  • 366
  • 406

1 Answers1

1

the image does not shrink when the window/stage is shrunk

Yes, that's by design. Images and ImageViews have a fixed size. If you want to dynamically resize ImageViews then there are a few options as outlined in this answer:

How could one fix the example program?

Use one of the techniques outlined in the resizable image post above.

Is the window/stage simply not meant meant to be shrunk after startup?

Depends on how you write your code. You can write in such a way that it is not meant to be shrunk, or you can write it so that the layout and content adjusts appropriately as the stage size changes. How you accomplish this is up to you as the developer. JavaFX defines numerous methods for resizable layouts, some of which are discussed in:

The standard way to accomplish resizable layouts would be to use appropriate layout panes.

even the position of the stackpane seems to be offset and not properly aligned with the split pane

I didn't really analyze this issue with your code in detail. I think it is probably caused by a couple of things:

  1. You are binding the fitWidth/Height of your ImageView to the width/height of the StackPane containing the ImageView. This probably causes the default layout code to get a bit confused, because part of the determinant of the width/height of the StackPane is the preferred size of the children, but by varying fitWidth/Height you are changing the preferred size of the ImageView, so this makes it hard to reason about what the width and height of the StackPane should be.
  2. When there is not enough room to display the StackPane's content, the StackPane can be larger than the available area (this is explained a bit in the StackPane javadoc). So, when you make the scene bigger, the fitWidth/Height is changing to get bigger with the available area for the StackPane, but when resize to get smaller the StackPane never gets smaller because it needs to be at least as large to fit the enlarged image view that was calculated before.

Anyway, moral of the story is: don't bind the width/height of a child to the width/height of a parent container. Sometimes you can get this approach to work and sometimes it will have weird side-effects such as you observe in your example code.

Instead, you can override the layout pass logic in a parent node and perform the appropriate layout calculations there. An example of this approach is provided in the sample below. Unfortunately, there is zero official documentation by Oracle (or almost anywhere else on the net) on techniques for overriding the layout pass logic of parent nodes. This is kind of made up by the fact that, most of the time, you don't need to write custom layout logic, but can instead use the standard layout panes and controls to get the layout you need (though unfortunately, you can't in this case and must use either CSS background settings or custom layout code).

Smooth scaling using -fx-shape

Scaling a bitmap image can result in some pixellation and an image that is not very smooth. In your case the input bitmap image is very large at 512x512, so the smooth scaling is not really an issue for the large image as it would be if you only had, for instance, a smaller 32x32 image and tried to scale that up to 512x512.

As you have a simple black and white image, you can convert the image to an svg, extract the path out of the svg and use the region property -fx-shape. This is how the default JavaFX stylesheet defines shapes such as checks for checkboxes in a way that they will smoothly scale across sizes. See the file modena.css inside jfxrt.jar in your JRE implementation for samples of definitions and usage of -fx-shape.

Something like this will do that for your image (CSS style can be extracted to a CSS file for easier maintenance):

Pane tile = new Pane();
tile.setStyle("" +
        "-fx-background-color: forestgreen;" +
        "-fx-shape: \"M2470 4886 c-83 -22 -144 -60 -220 -136 -41 -41 -91 -102 -111 -135\n" +
        "-121 -200 -2027 -3510 -2046 -3554 -55 -125 -68 -272 -33 -377 56 -162 205\n" +
        "-260 431 -284 127 -13 4011 -13 4138 0 226 24 375 122 431 284 35 105 22 252\n" +
        "-33 377 -32 72 -1994 3472 -2064 3577 -135 202 -320 295 -493 248z m223 -1182\n" +
        "c122 -43 210 -155 237 -301 12 -69 3 -156 -45 -428 -34 -196 -101 -636 -120\n" +
        "-785 -8 -69 -18 -144 -21 -168 l-5 -42 -177 2 -177 3 -8 75 c-20 203 -66 500\n" +
        "-158 1030 -38 218 -40 240 -29 305 15 94 47 161 107 221 58 58 126 94 197 104\n" +
        "61 9 147 2 199 -16z m30 -1989 c64 -32 144 -111 175 -172 49 -96 49 -231 0\n" +
        "-332 -29 -59 -102 -133 -165 -164 -275 -140 -595 91 -543 391 14 80 39 130 93\n" +
        "189 86 94 160 124 293 120 76 -2 99 -7 147 -32z\";"
);
tile.setScaleY(-1);
tile.setPrefSize(64, 64);

I don't know how to make the pane only show a proportional size if the pane is resized. So I doubt that helps you much.

This would be simpler if JavaFX directly supported SVG images, but it does not.

Some layout creation and debugging device

  1. For creation of layouts, try using Gluon SceneBuilder. Even if you don't use FXML, you can learn a lot easily by dynamically playing around with the UI elements and properties in SceneBuilder and observing the resultant differences such changes make in a generated FXML file.
  2. To dynamically debug the layout of an existing application at runtime, use FXExperience ScenicView.

Sample Solution

Sample output at varying stage sizes:

withoutcss

withcss

This uses the ImageViewPane that was defined in the answer to the linked question on resizing images in JavaFX.

ResizableImageSample.java

import javafx.application.Application;
import javafx.geometry.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class ResizableImageSample extends Application {
    @Override
    public void start(Stage stage) {
        // Top section with image
        ImageView imageView = new ImageView(new Image(
                "http://wfarm2.dataknet.com/static/resources/icons/set114/b4f29385.png"
        ));
        imageView.setPreserveRatio(true);
        ImageViewPane imagePane = new ImageViewPane(imageView);
        imagePane.getStyleClass().add("stack-pane");

        // Bottom Section with CSS toggle
        ToggleButton toggle = new ToggleButton("Show CSS");
        HBox hbox = new HBox(toggle);
        hbox.setAlignment(Pos.CENTER);
        hbox.getStyleClass().add("h-box");

        // Combines the two sections
        SplitPane root = new SplitPane(imagePane, hbox);
        root.setDividerPositions(0.8);
        root.setOrientation(Orientation.VERTICAL);
        SplitPane.setResizableWithParent(hbox, false);
        Scene scene = new Scene(root);
        toggle.setOnMouseClicked(e -> {
            if (toggle.isSelected()) {
                scene.getStylesheets().add(getClass().getResource("StyleSheet.css").toExternalForm());
                toggle.setText("Disable CSS");
            } else {
                scene.getStylesheets().remove(0);
                toggle.setText("Show CSS");
            }
        });

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

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

ImageViewPane.java

/*
 * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 */
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.HPos;
import javafx.geometry.VPos;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Region;

/**
 *
 * @author akouznet
 */
class ImageViewPane extends Region {

    private ObjectProperty<ImageView> imageViewProperty = new SimpleObjectProperty<>();

    public ObjectProperty<ImageView> imageViewProperty() {
        return imageViewProperty;
    }

    public ImageView getImageView() {
        return imageViewProperty.get();
    }

    public void setImageView(ImageView imageView) {
        this.imageViewProperty.set(imageView);
    }

    public ImageViewPane() {
        this(new ImageView());
    }

    @Override
    protected void layoutChildren() {
        ImageView imageView = imageViewProperty.get();
        if (imageView != null) {
            if (imageView.isPreserveRatio()) {
                if (getHeight() > getWidth()) {
                    imageView.setFitWidth(getWidth());
                    imageView.setFitHeight(0);
                } else {
                    imageView.setFitWidth(0);
                    imageView.setFitHeight(getHeight());
                }
            } else {
                imageView.setFitWidth(getWidth());
                imageView.setFitHeight(getHeight());
            }

            layoutInArea(imageView, 0, 0, getWidth(), getHeight(), 0, HPos.CENTER, VPos.CENTER);
        }
        super.layoutChildren();
    }

    public ImageViewPane(ImageView imageView) {
        imageViewProperty.addListener((observable, oldIV, newIV) -> {
            if (oldIV != null) {
                getChildren().remove(oldIV);
            }
            if (newIV != null) {
                getChildren().add(newIV);
            }
        });
        this.imageViewProperty.set(imageView);
    }
}
Community
  • 1
  • 1
jewelsea
  • 150,031
  • 14
  • 366
  • 406