A6SE
A6SE

Reputation: 177

Rectangle size binding to Scene size - JavaFX

I'm trying to write a simple JavaFX application that contains a graphical representation of an NxM matrix (default 100 x 100). I would like to bind the matrix size to the Scene size, so when I resize the app window, the matrix follows it and keeps the aspect ratio. The binding works seamlessly for small matrices (for example 10x10), but when the matrix gets bigger (for example 50x50) and rectangles get smaller so they can fit my screen, the binding process becomes discontinuous. The matrix sometimes even gets bigger than window size (can't see all cells/rectangles) and matrix size seems like it's switching between integer values.

What I did is created a StackPane as a root Node inside the Scene and a GridPane as a child Node to the StackPane. I filled the GridPane with an NxM Rectangle matrix.

The code follows:

public class GridPaneExample extends Application {

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

    @Override
    public void start(Stage primaryStage) throws Exception {
        int n = 100, m = 100; // matrix size
        double rectw = 5, recth = 5; // size of each rectangle
        StackPane root = new StackPane();
        Scene scene = new Scene(root, (m + 2) * rectw, (n + 2) * recth, Color.DARKOLIVEGREEN); // creating a scene with
                                                                                                // a frame around matrix
        primaryStage.setScene(scene);
        primaryStage.sizeToScene();

        GridPane gp = new GridPane();
        root.getChildren().add(gp);
        Rectangle[][] rects = new Rectangle[n][m];
        for (int i = 0; i < n; ++i)
            for (int j = 0; j < m; ++j) {
                rects[i][j] = new Rectangle();
                rects[i][j].setWidth(rectw);
                rects[i][j].setHeight(recth);
                rects[i][j].setFill(Color.ANTIQUEWHITE);
                rects[i][j].setStrokeType(StrokeType.INSIDE);
                rects[i][j].setStrokeWidth(0.2);
                rects[i][j].setStroke(Color.GREY);

                rects[i][j].widthProperty().bind(gp.widthProperty().divide(m));
                rects[i][j].heightProperty().bind(gp.heightProperty().divide(n));

                GridPane.setRowIndex(rects[i][j], i);
                GridPane.setColumnIndex(rects[i][j], j);
                gp.getChildren().add(rects[i][j]);
            }

        rects[0][0].setFill(Color.RED);
        rects[0][m - 1].setFill(Color.RED);
        rects[n - 1][0].setFill(Color.RED);
        rects[n - 1][m - 1].setFill(Color.RED);

        StackPane.setAlignment(gp, Pos.TOP_CENTER);
        gp.minHeightProperty().bind(scene.heightProperty().subtract(2 * recth));
        gp.minWidthProperty().bind(scene.widthProperty().subtract(2 * rectw));
        gp.maxHeightProperty().bind(scene.heightProperty().subtract(2 * recth));
        gp.maxWidthProperty().bind(scene.widthProperty().subtract(2 * rectw));
        root.setLayoutY(recth);

        primaryStage.show();
        primaryStage.setMinHeight(primaryStage.getHeight());
        primaryStage.setMinWidth(primaryStage.getWidth());

    }
}

Here are some screenshots of the program showing different sized matrices.

A 10 x 10 matrix app when started (rectangle size is 20x20):

https://i.sstatic.net/k8J5y.png

A 10 x 10 matrix app when stretched:

https://i.sstatic.net/1qujl.png

A 100 x 100 matrix app when started (rectangle size is 5x5):

https://i.sstatic.net/ntmr2.png

A 100 x 100 matrix app when stretched a bit, see how the matrix gets out of bounds:

https://i.sstatic.net/adyaY.png

So, is there a way to make this binding experience smooth and consistent, maybe by using something else other than Rectangles?

Edit: A weird thing happens. When I remove the rectangle binding lines from code and launch the app with 100x100 matrix, the matrix looks as it should. The program prints out the size of the GridPane and red rectangles. They are 500x500 and 5x5 respectively, as expected. However, when I include rectangle binding (as shown in the code), the matrix gets out of bounds, as in third photo attached, but the program still prints out the same dimensions of GridPane and rectangles.

Upvotes: 0

Views: 1036

Answers (2)

n247s
n247s

Reputation: 1918

As commented earlier, this problem is fixable by calling Node#setSnapToPixel(false) on the GridPane.

Explanation

This problem occurs when Nodes become so small that a single pixel becomes too big for accurate display.

JavaFX uses a 'pixel snapping' feature by default to ensure a 'clear/clean' look. This would be mainly vissible with borders getting 'fussy/blurry' when not used.

In the backend JavaFX has defined the methods snapSpace, snapSize and snapPosition, which correspond respectively to Math.round, Math.ceil and Math.round.

This means that if a Node has a width of 2.5 with a 1 pixel border, it should have the size of (1 + 2.5 + 1) = 4.5, wich ceiled wil result in 5.0. If you have 100 elements getting a 0.5 width bonus it will result in 50 pixels overflow!

Setting 'snapToPixel' to false means it wil ignore the mechanism above, and use mixed colors per pixel. If you would look closely this 'blurr' effect is noticable on borders. But it is worth the tradeoff for the perfect layout IMO.

Upvotes: 2

jewelsea
jewelsea

Reputation: 159416

Here is a sample app I wrote a long time ago which does a similar kind of thing. It uses resizable nodes within a GridPane with a listener on the layoutBoundsProperty to pick the size of the resizable nodes. Not sure if it is really what you are interested in (the approach you have in your question, with some fixes or tweaks, is probably fine for what you wish to accomplish), but it might be worth taking a look at for an alternate approach if needed.

large grid small grid

Relevant code is for the swatch node in the ColorChooser constructor from the sample code below (sorry it's a bit long, it was written for a different purpose but seems close enough that it might be worth posting in its entirety):

import javafx.application.Application;
import javafx.beans.property.*;
import javafx.event.*;
import javafx.geometry.*;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

/**
 * Sample application for using the color chooser
 */
public class ColorChooserSample extends Application {
    public void start(final Stage stage) throws Exception {
        // initialize the stage.
        stage.setTitle("Color Chooser");
        stage.initStyle(StageStyle.UTILITY);

        // create a new color chooser sized to the stage.
        final String[][] smallPalette = {
                {"aliceblue", "#f0f8ff"}, {"antiquewhite", "#faebd7"}, {"aqua", "#00ffff"}, {"aquamarine", "#7fffd4"},
                {"azure", "#f0ffff"}, {"beige", "#f5f5dc"}, {"bisque", "#ffe4c4"}, {"black", "#000000"},
                {"blanchedalmond", "#ffebcd"}, {"blue", "#0000ff"}, {"blueviolet", "#8a2be2"}, {"brown", "#a52a2a"},
                {"burlywood", "#deb887"}, {"cadetblue", "#5f9ea0"}, {"chartreuse", "#7fff00"}, {"chocolate", "#d2691e"},
                {"coral", "#ff7f50"}, {"cornflowerblue", "#6495ed"}, {"cornsilk", "#fff8dc"}, {"crimson", "#dc143c"},
                {"cyan", "#00ffff"}, {"darkblue", "#00008b"}, {"darkcyan", "#008b8b"}, {"darkgoldenrod", "#b8860b"},
        };
        final ColorChooser colorChooser = new ColorChooser(smallPalette);
        // to use the full web palette, just use the default constructor.
        // final ColorChooser colorChooser = new ColorChooser();

        final Scene scene = new Scene(colorChooser, 600, 500);

        // show the stage.
        stage.setScene(scene);
        stage.show();

        // monitor the color chooser's chosen color and respond to it.
        colorChooser.chosenColorProperty().addListener((observableValue, oldColor, newColor) ->
                System.out.println("Chose: " + colorChooser.getChosenColorName() + " " + colorChooser.getChosenColor())
        );
    }

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

/**
 * A Color Chooser Component - allows the user to select a color from a palette.
 */
class ColorChooser extends VBox {
    private final double GOLDEN_RATIO = 1.618;
    private final double MIN_TILE_SIZE = 5;
    private final double nColumns;
    private final double nRows;

    /**
     * The color the user has selected or the default initial color (the first color in the palette)
     */
    private final ReadOnlyObjectWrapper<Color> chosenColor = new ReadOnlyObjectWrapper<Color>();

    public Color getChosenColor() {
        return chosenColor.get();
    }

    public ReadOnlyObjectProperty<Color> chosenColorProperty() {
        return chosenColor.getReadOnlyProperty();
    }

    /**
     * Friendly name for the chosen color
     */
    private final ReadOnlyObjectWrapper<String> chosenColorName = new ReadOnlyObjectWrapper<String>();

    public String getChosenColorName() {
        return chosenColorName.get();
    }

    /**
     * Preferred size for a web palette tile
     */
    private DoubleProperty prefTileSize = new SimpleDoubleProperty(MIN_TILE_SIZE);

    /**
     * A palette of colors from http://docs.oracle.com/javafx/2.0/api/javafx/scene/doc-files/cssref.html#typecolor
     */
    private static final String[][] webPalette = {
            {"aliceblue", "#f0f8ff"}, {"antiquewhite", "#faebd7"}, {"aqua", "#00ffff"}, {"aquamarine", "#7fffd4"},
            {"azure", "#f0ffff"}, {"beige", "#f5f5dc"}, {"bisque", "#ffe4c4"}, {"black", "#000000"},
            {"blanchedalmond", "#ffebcd"}, {"blue", "#0000ff"}, {"blueviolet", "#8a2be2"}, {"brown", "#a52a2a"},
            {"burlywood", "#deb887"}, {"cadetblue", "#5f9ea0"}, {"chartreuse", "#7fff00"}, {"chocolate", "#d2691e"},
            {"coral", "#ff7f50"}, {"cornflowerblue", "#6495ed"}, {"cornsilk", "#fff8dc"}, {"crimson", "#dc143c"},
            {"cyan", "#00ffff"}, {"darkblue", "#00008b"}, {"darkcyan", "#008b8b"}, {"darkgoldenrod", "#b8860b"},
            {"darkgray", "#a9a9a9"}, {"darkgreen", "#006400"}, {"darkgrey", "#a9a9a9"}, {"darkkhaki", "#bdb76b"},
            {"darkmagenta", "#8b008b"}, {"darkolivegreen", "#556b2f"}, {"darkorange", "#ff8c00"}, {"darkorchid", "#9932cc"},
            {"darkred", "#8b0000"}, {"darksalmon", "#e9967a"}, {"darkseagreen", "#8fbc8f"}, {"darkslateblue", "#483d8b"},
            {"darkslategray", "#2f4f4f"}, {"darkslategrey", "#2f4f4f"}, {"darkturquoise", "#00ced1"}, {"darkviolet", "#9400d3"},
            {"deeppink", "#ff1493"}, {"deepskyblue", "#00bfff"}, {"dimgray", "#696969"}, {"dimgrey", "#696969"},
            {"dodgerblue", "#1e90ff"}, {"firebrick", "#b22222"}, {"floralwhite", "#fffaf0"}, {"forestgreen", "#228b22"},
            {"fuchsia", "#ff00ff"}, {"gainsboro", "#dcdcdc"}, {"ghostwhite", "#f8f8ff"}, {"gold", "#ffd700"},
            {"goldenrod", "#daa520"}, {"gray", "#808080"}, {"green", "#008000"}, {"greenyellow", "#adff2f"},
            {"grey", "#808080"}, {"honeydew", "#f0fff0"}, {"hotpink", "#ff69b4"}, {"indianred", "#cd5c5c"},
            {"indigo", "#4b0082"}, {"ivory", "#fffff0"}, {"khaki", "#f0e68c"}, {"lavender", "#e6e6fa"},
            {"lavenderblush", "#fff0f5"}, {"lawngreen", "#7cfc00"}, {"lemonchiffon", "#fffacd"}, {"lightblue", "#add8e6"},
            {"lightcoral", "#f08080"}, {"lightcyan", "#e0ffff"}, {"lightgoldenrodyellow", "#fafad2"}, {"lightgray", "#d3d3d3"},
            {"lightgreen", "#90ee90"}, {"lightgrey", "#d3d3d3"}, {"lightpink", "#ffb6c1"}, {"lightsalmon", "#ffa07a"},
            {"lightseagreen", "#20b2aa"}, {"lightskyblue", "#87cefa"}, {"lightslategray", "#778899"}, {"lightslategrey", "#778899"},
            {"lightsteelblue", "#b0c4de"}, {"lightyellow", "#ffffe0"}, {"lime", "#00ff00"}, {"limegreen", "#32cd32"},
            {"linen", "#faf0e6"}, {"magenta", "#ff00ff"}, {"maroon", "#800000"}, {"mediumaquamarine", "#66cdaa"},
            {"mediumblue", "#0000cd"}, {"mediumorchid", "#ba55d3"}, {"mediumpurple", "#9370db"}, {"mediumseagreen", "#3cb371"},
            {"mediumslateblue", "#7b68ee"}, {"mediumspringgreen", "#00fa9a"}, {"mediumturquoise", "#48d1cc"}, {"mediumvioletred", "#c71585"},
            {"midnightblue", "#191970"}, {"mintcream", "#f5fffa"}, {"mistyrose", "#ffe4e1"}, {"moccasin", "#ffe4b5"},
            {"navajowhite", "#ffdead"}, {"navy", "#000080"}, {"oldlace", "#fdf5e6"}, {"olive", "#808000"},
            {"olivedrab", "#6b8e23"}, {"orange", "#ffa500"}, {"orangered", "#ff4500"}, {"orchid", "#da70d6"},
            {"palegoldenrod", "#eee8aa"}, {"palegreen", "#98fb98"}, {"paleturquoise", "#afeeee"}, {"palevioletred", "#db7093"},
            {"papayawhip", "#ffefd5"}, {"peachpuff", "#ffdab9"}, {"peru", "#cd853f"}, {"pink", "#ffc0cb"},
            {"plum", "#dda0dd"}, {"powderblue", "#b0e0e6"}, {"purple", "#800080"}, {"red", "#ff0000"},
            {"rosybrown", "#bc8f8f"}, {"royalblue", "#4169e1"}, {"saddlebrown", "#8b4513"}, {"salmon", "#fa8072"},
            {"sandybrown", "#f4a460"}, {"seagreen", "#2e8b57"}, {"seashell", "#fff5ee"}, {"sienna", "#a0522d"},
            {"silver", "#c0c0c0"}, {"skyblue", "#87ceeb"}, {"slateblue", "#6a5acd"}, {"slategray", "#708090"},
            {"slategrey", "#708090"}, {"snow", "#fffafa"}, {"springgreen", "#00ff7f"}, {"steelblue", "#4682b4"},
            {"tan", "#d2b48c"}, {"teal", "#008080"}, {"thistle", "#d8bfd8"}, {"tomato", "#ff6347"},
            {"turquoise", "#40e0d0"}, {"violet", "#ee82ee"}, {"wheat", "#f5deb3"}, {"white", "#ffffff"},
            {"whitesmoke", "#f5f5f5"}, {"yellow", "#ffff00"}, {"yellowgreen", "#9acd32"}
    };

    public ColorChooser() {
        this(webPalette);
    }

    public ColorChooser(String[][] colors) {
        super();

        // create a pane for showing info on the chosen color.
        final HBox colorInfo = new HBox();
        final Label selectedColorName = new Label();
        HBox.setMargin(selectedColorName, new Insets(2, 0, 2, 10));
        colorInfo.getChildren().addAll(selectedColorName);
        chosenColorName.addListener((observableValue, oldName, newName) -> {
            if (newName != null) {
                colorInfo.setStyle("-fx-background-color: " + newName + ";");
                selectedColorName.setText(newName);
                chosenColor.set(Color.web(newName));
            }
        });

        // create a color swatch.
        final GridPane swatch = new GridPane();
        swatch.setSnapToPixel(false);

        // calculate the number of columns and rows based on the number of colors and a golden ratio for layout.
        nColumns = Math.floor(Math.sqrt(colors.length) * 2 / GOLDEN_RATIO);
        nRows = Math.ceil(colors.length / nColumns);

        // create a bunch of button controls for color selection.
        int i = 0;
        for (String[] namedColor : colors) {
            final String colorName = namedColor[0];
            final String colorHex = namedColor[1];

            // create a button for choosing a color.
            final Button colorChoice = new Button();
            colorChoice.setUserData(colorName);

            // position the button in the grid.
            GridPane.setRowIndex(colorChoice, i / (int) nColumns);
            GridPane.setColumnIndex(colorChoice, i % (int) nColumns);
            colorChoice.setMinSize(MIN_TILE_SIZE, MIN_TILE_SIZE);
            colorChoice.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);

            // add a mouseover tooltip to display more info on the colour being examined.
            // todo it would be nice to be able to have the tooltip appear immediately on mouseover, but there is no easy way to do this, (file jira feature request?)
            final Tooltip tooltip = new Tooltip(colorName);
            tooltip.setStyle("-fx-font-size: 14");
            tooltip.setContentDisplay(ContentDisplay.BOTTOM);
            final Rectangle graphic = new Rectangle(30, 30, Color.web(colorHex));
            graphic.widthProperty().bind(prefTileSize.multiply(1.5));
            graphic.heightProperty().bind(prefTileSize.multiply(1.5));
            tooltip.setGraphic(graphic);
            colorChoice.setTooltip(tooltip);

            // color the button appropriately and change it's hover functionality (doing some of this in a css sheet would be better).
            final String backgroundStyle = "-fx-background-color: " + colorHex + "; -fx-background-insets: 0; -fx-background-radius: 0;";
            colorChoice.setStyle(backgroundStyle);
            colorChoice.setOnMouseEntered(mouseEvent -> {
                final String borderStyle = "-fx-border-color: ladder(" + colorHex + ", whitesmoke 49%, darkslategrey 50%); -fx-border-width: 2;";
                colorChoice.setStyle(backgroundStyle + borderStyle);
            });
            colorChoice.setOnMouseExited(mouseEvent -> {
                final String borderStyle = "-fx-border-width: 0; -fx-border-insets: 2;";
                colorChoice.setStyle(backgroundStyle + borderStyle);
            });

            // choose the color when the button is clicked.
            colorChoice.setOnAction(new EventHandler<ActionEvent>() {
                @Override
                public void handle(ActionEvent actionEvent) {
                    chosenColorName.set((String) colorChoice.getUserData());
                }
            });

            // add the color choice to the swatch selection.
            swatch.getChildren().add(colorChoice);

            i++;
        }

        // select the first color in the chooser.
        ((Button) swatch.getChildren().get(0)).fire();

        // layout the color picker.
        getChildren().addAll(swatch, colorInfo);
        VBox.setVgrow(swatch, Priority.ALWAYS);
        setStyle("-fx-background-color: black; -fx-font-size: 16;");
        swatch.layoutBoundsProperty().addListener((observableValue, oldBounds, newBounds) -> {
            prefTileSize.set(Math.max(MIN_TILE_SIZE, Math.min(newBounds.getWidth() / nColumns, newBounds.getHeight() / nRows)));
            for (Node child : swatch.getChildrenUnmodifiable()) {
                Control tile = (Control) child;
                final double margin = prefTileSize.get() / 10;
                tile.setPrefSize(prefTileSize.get() - 2 * margin, prefTileSize.get() - 2 * margin);
                GridPane.setMargin(child, new Insets(margin));
            }
        });
    }
}

Upvotes: 2

Related Questions