Automatically resize Canvas to fill the enclosing

2020-01-24 03:29发布

问题:

I recently wanted to create an animated background in JavaFX, similar to the Swing example seen here. I used a Canvas on which to draw, as shown in Working with the Canvas API, and an AnimationTimer for the drawing loop, as shown in Animation Basics. Unfortunately, I'm not sure how to resize the Canvas automatically as the enclosing Stage is resized. What is a good approach?

A similar question is examined in How to make canvas Resizable in javaFX?, but the accepted answer there lacks the binding illustrated in the accepted answer here.

回答1:

I combined both prior solutions ( @trashgod and @clataq's ) by putting the canvas in a Pane and binding it to it:

private static class CanvasPane extends Pane {

    final Canvas canvas;

    CanvasPane(double width, double height) {
        setWidth(width);
        setHeight(height);
        canvas = new Canvas(width, height);
        getChildren().add(canvas);

        canvas.widthProperty().bind(this.widthProperty());
        canvas.heightProperty().bind(this.heightProperty());
    }
}


回答2:

In the example below, the static nested class CanvasPane wraps an instance of Canvas in a Pane and overrides layoutChildren() to make the canvas dimensions match the enclosing Pane. Note that Canvas returns false from isResizable(), so "the parent cannot resize it during layout," and Pane "does not perform layout beyond resizing resizable children to their preferred sizes." The width and height used to construct the canvas become its initial size. A similar approach is used in the Ensemble particle simulation, Fireworks.java, to scale a background image while retaining its aspect ratio.

As an aside, note the difference from using fully saturated colors compared to the original. These related examples illustrate placing controls atop the animated background.

import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

/**
 * @see https://stackoverflow.com/a/31761362/230513
 * @see https://stackoverflow.com/a/8616169/230513
 */

public class Baubles extends Application {

    private static final int MAX = 64;
    private static final double WIDTH = 640;
    private static final double HEIGHT = 480;
    private static final Random RND = new Random();
    private final Queue<Bauble> queue = new LinkedList<>();
    private Canvas canvas;

    @Override
    public void start(Stage stage) {
        CanvasPane canvasPane = new CanvasPane(WIDTH, HEIGHT);
        canvas = canvasPane.getCanvas();
        BorderPane root = new BorderPane(canvasPane);
        CheckBox cb = new CheckBox("Animate");
        cb.setSelected(true);
        root.setBottom(cb);
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();

        for (int i = 0; i < MAX; i++) {
            queue.add(randomBauble());
        }
        AnimationTimer loop = new AnimationTimer() {
            @Override
            public void handle(long now) {
                GraphicsContext g = canvas.getGraphicsContext2D();
                g.setFill(Color.BLACK);
                g.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
                for (Bauble b : queue) {
                    g.setFill(b.c);
                    g.fillOval(b.x, b.y, b.d, b.d);
                }
                queue.add(randomBauble());
                queue.remove();
            }
        };
        loop.start();
        cb.selectedProperty().addListener((Observable o) -> {
            if (cb.isSelected()) {
                loop.start();
            } else {
                loop.stop();
            }
        });
    }

    private static class Bauble {

        private final double x, y, d;
        private final Color c;

        public Bauble(double x, double y, double r, Color c) {
            this.x = x - r;
            this.y = y - r;
            this.d = 2 * r;
            this.c = c;
        }
    }

    private Bauble randomBauble() {
        double x = RND.nextDouble() * canvas.getWidth();
        double y = RND.nextDouble() * canvas.getHeight();
        double r = RND.nextDouble() * MAX + MAX / 2;
        Color c = Color.hsb(RND.nextDouble() * 360, 1, 1, 0.75);
        return new Bauble(x, y, r, c);
    }

    private static class CanvasPane extends Pane {

        private final Canvas canvas;

        public CanvasPane(double width, double height) {
            canvas = new Canvas(width, height);
            getChildren().add(canvas);
        }

        public Canvas getCanvas() {
            return canvas;
        }

        @Override
        protected void layoutChildren() {
            super.layoutChildren();
            final double x = snappedLeftInset();
            final double y = snappedTopInset();
            // Java 9 - snapSize is deprecated, use snapSizeX() and snapSizeY() accordingly
            final double w = snapSize(getWidth()) - x - snappedRightInset();
            final double h = snapSize(getHeight()) - y - snappedBottomInset();
            canvas.setLayoutX(x);
            canvas.setLayoutY(y);
            canvas.setWidth(w);
            canvas.setHeight(h);
        }
    }

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


回答3:

Couldn't you do this with a Binding as well? The following seems to produce the same results without having to add the derived class.

import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.DoubleBinding;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

/**
 * @see http://stackoverflow.com/a/31761362/230513
 * @see http://stackoverflow.com/a/8616169/230513
 */

public class Baubles extends Application {

private static final int MAX = 64;
private static final double WIDTH = 640;
private static final double HEIGHT = 480;
private static final Random RND = new Random();
private final Queue<Bauble> queue = new LinkedList<>();
private Canvas canvas;

@Override
public void start(Stage stage) {
    canvas = new Canvas(WIDTH, HEIGHT);
    BorderPane root = new BorderPane(canvas);
    CheckBox cb = new CheckBox("Animate");
    cb.setSelected(true);
    root.setBottom(cb);
    Scene scene = new Scene(root);
    stage.setScene(scene);
    stage.show();

    // Create bindings for resizing.
    DoubleBinding heightBinding = root.heightProperty()
            .subtract(root.bottomProperty().getValue().getBoundsInParent().getHeight());
    canvas.widthProperty().bind(root.widthProperty());
    canvas.heightProperty().bind(heightBinding);

    for (int i = 0; i < MAX; i++) {
        queue.add(randomBauble());
    }
    AnimationTimer loop = new AnimationTimer() {
        @Override
        public void handle(long now) {
            GraphicsContext g = canvas.getGraphicsContext2D();
            g.setFill(Color.BLACK);
            g.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
            for (Bauble b : queue) {
                g.setFill(b.c);
                g.fillOval(b.x, b.y, b.d, b.d);
            }
            queue.add(randomBauble());
            queue.remove();
        }
    };
    loop.start();
    cb.selectedProperty().addListener((Observable o) -> {
        if (cb.isSelected()) {
            loop.start();
        } else {
            loop.stop();
        }
    });
}

private static class Bauble {

    private final double x, y, d;
    private final Color c;

    public Bauble(double x, double y, double r, Color c) {
        this.x = x - r;
        this.y = y - r;
        this.d = 2 * r;
        this.c = c;
    }
}

private Bauble randomBauble() {
    double x = RND.nextDouble() * canvas.getWidth();
    double y = RND.nextDouble() * canvas.getHeight();
    double r = RND.nextDouble() * MAX + MAX / 2;
    Color c = Color.hsb(RND.nextDouble() * 360, 1, 1, 0.75);
    return new Bauble(x, y, r, c);
}

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