Resizable Layout and resizable image

This is the follow up of the previous article on resize with JavaFX. In this article I will show you how I created a Layout manager that has top and left insets which stay constant when the window is resized to a size larger than the original, but reduce proportionally if the size becomes smaller. The second point addressed here are resizable images.

ResizeLayout_nonResizedResizeLayout_enlargedResizeLayout_small

 

 

 

 

 

The code for this is split up into two classes, the layout manager:

package javafxtest;

import java.util.List;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;

public class StretchLeftBottomLayout extends StackPane {
    private final int topMargin;
    private final int leftMargin;
    private Double originalWidth = null;
    private Double originalHeight = null;

    public StretchLeftBottomLayout(int topMargin, int leftMargin) {
        this.topMargin = topMargin;
        this.leftMargin = leftMargin;
     }

    @Override
    protected void layoutChildren() {
        List<Node> managed = getManagedChildren();
        double width = getWidth();
        double height = getHeight();
        if (originalWidth == null) {
            originalWidth = width;
        }
        if (originalHeight == null) {
            originalHeight = height;
        }
        double scaleX = Math.min(computeScaleX(width), 1);
        double scaleY = Math.min(computeScaleY(height), 1);
        double top = getInsets().getTop() + topMargin*scaleY;
        double right = getInsets().getRight();
        double left = getInsets().getLeft() + leftMargin*scaleX;
        double bottom = getInsets().getBottom();
System.out.println(scaleX+" -> "+left+", "+scaleY+" -> "+top);
System.out.println(width+" "+height+" "+top+" "+right+" "+left+" "+bottom+" "+getAlignment().getVpos()+" "+VPos.BASELINE);
        double baselineOffset = height/2;
        for (int i = 0; i < managed.size(); i++) {
            Node child = managed.get(i);
            Pos childAlignment = StretchLeftBottomLayout.getAlignment(child);
            layoutInArea(child, left, top,
                           width - left - right, height - top - bottom,
                           baselineOffset, getMargin(child),
                           childAlignment != null? childAlignment.getHpos() : getAlignment().getHpos(),
                           childAlignment != null? childAlignment.getVpos() : getAlignment().getVpos());
        }
    }

    private double computeScaleX(double width) {
        return width / originalWidth;
    }
     private double computeScaleY(double height) {
        return height / originalHeight;
    }
}

and the calling code:

package javafxtest;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class OverlayLayoutResize2Test extends Application {

    @Override
    public void start(Stage stage) throws FileNotFoundException {
        final StackPane root = new StackPane();
        File f = new File("/home/andi/Pictures/Adam.jpg");
        Image image = new Image(new FileInputStream(f));
        final double width = image.getWidth();
        final double height = image.getHeight();
        Canvas background = new Canvas(width, height);
        GraphicsContext context = background.getGraphicsContext2D();
        context.drawImage(image, 0, 0);

        root.getChildren().add(background);

        StretchLeftBottomLayout animationPane = new StretchLeftBottomLayout(54, 23);
        Region red = new Region();
        red.setStyle("-fx-background-color: #FF0000;");
        animationPane.getChildren().add(red);
        root.getChildren().add(animationPane);

        Scene scene = new Scene(root, width, height);

        stage.setTitle("Resize Layout 2 Overlay Test");
        stage.setScene(scene);
        stage.show();
    }

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

As in a real application a red rectangle is not terribly exciting I need to have an image that can be resized and displayed. Unfortunately both Canvas and ImageView are not resizable. So it is supposed that the ImageView should become resizable some when. Therefore I had to tweek the code a bit on my own:

package ch.sahits.javafx.test;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

/**
 *
 * @author andi
 */
public class ResizeableCanvas extends Application {

    @Override
    public void start(Stage primaryStage) {
        try {
            StackPane root = new StackPane();
            Scene scene = new Scene(root, 300, 250);
            Image img = new Image(getClass().getResource("kajak4_small.png").openStream());
            Canvas canvas = new Canvas(root.getWidth(), root.getHeight());
            GraphicsContext context = canvas.getGraphicsContext2D();
            context.drawImage(img, 0, 0, root.getWidth(), root.getHeight());

            // Binding
            canvas.widthProperty().bind(root.widthProperty());
            canvas.heightProperty().bind(root.heightProperty());

            final ResizeChangeListener resizeChangeListener = new ResizeChangeListener(root, context, img);

            canvas.widthProperty().addListener(resizeChangeListener);
            canvas.heightProperty().addListener(resizeChangeListener);

            root.getChildren().add(canvas);            

            primaryStage.setTitle("Resizable Canvas Test");
            primaryStage.setScene(scene);
            primaryStage.show();
        } catch (IOException ex) {
            Logger.getLogger(ResizeableCanvas.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

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

    private static class ResizeChangeListener implements ChangeListener<Number> {

        private final Pane parent;
        private final GraphicsContext context;
        private final Image img;

        public ResizeChangeListener(Pane parent, GraphicsContext context, Image image) {
            this.parent = parent;
            this.context = context;
            this.img = image;
        }

        @Override
        public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
            final double width = parent.getWidth();
            final double height = parent.getHeight();
            context.clearRect(0, 0, width, height);
            context.drawImage(img, 0, 0, width, height);
        }
    }
}

The main point here is that the width and height properties of the Canvas are bound to the same properties of the StackPane. Additionally there is a ChangeListener registered on these two properties of the Context which will redraw the Image with the correct proportions:

resizedJavaFXCanvas

The next step the would be to refactor this code into a separate component, which did not work, as this forum entry can confirm.

Never the less as I invested some time into figuring this out, I came across some useful resources on the web:

  • JavaFX 2.0 Layout: A Class Tour illustrates the class hierarchy and dependencies of the Control. However I should not that the current class hierarchy is not the same (the Control class extends the Region for example)
  • Henrik’s article about custom controls. He explains the intrinsic correlation between Control, CSS, Skin and behavior. Current code from the repository suggests, that the Behavior has dropped out and is merged into the Skin. That’s also what Gerrit Grunwald mentioned on his JUGS presentation.
  • A JavaFX 2.0 Custom Control is another article on this subject by Eric Bruno
  • Very important is also the link to the OpenJFX repository.
  • Last but not least are the talks of Gerrit Grunwald and Jonathan Giles at JavaOne 2012.

The code for the resizeable canvas is available as NetBeans project export. Contained is also the code of the not working (as expected) custom control.

It was pointed out to me in the OTN forum, that the ImageViewPane from this feature request could be used:

package ch.sahits.javafx.test;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.HPos;
import javafx.geometry.VPos;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Region;

/**
 *
 * @author akouznet
 */
public 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) {
            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(new ChangeListener<ImageView>() {

            @Override
            public void changed(ObservableValue<? extends ImageView> arg0, ImageView oldIV, ImageView newIV) {
                if (oldIV != null) {
                    getChildren().remove(oldIV);
                }
                if (newIV != null) {
                    getChildren().add(newIV);
                }
            }
        });
        this.imageViewProperty.set(imageView);
    }
}
The calling code then looks like this:
package ch.sahits.javafx.test;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class ImageViewPaneTest extends Application {

    @Override
    public void start(Stage primaryStage) {
        try {
            Image img = new Image( getClass().getResource("kajak4_small.png").openStream());
            ImageViewPane imageViewPane = new ImageViewPane(new ImageView(img));

            StackPane root = new StackPane();
            root.getChildren().add(imageViewPane);

            Scene scene = new Scene(root, 300, 250);

            primaryStage.setTitle("ImageViewPaneTest");
            primaryStage.setScene(scene);
            primaryStage.show();
        } catch (IOException ex) {
            Logger.getLogger(ImageViewPaneTest.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}
and produces the same result as the first example with the inline Canvas.

Schreibe einen Kommentar