How to set the size of a svgPath in JavaFx - java

I have a ListView that contains ListCells, which contain an icon defined by a SvgPath.
private class CustomCell extends ListCell<String> {
private SVGPath icon = new SVGPath();
public CustomCell() {
icon.setContent("...");
}
#Override
public void updateItem(String value, boolean empty) {
super.updateItem(value, empty);
if(empty || value == null) {
setGraphic(null);
setText(null);
} else {
setGraphic(icon);
setText(value);
}
}
}
I want the icon to fill the height of the list cell (which is about 30px). But it´s always shown extremely large, and the size of the cell is set to the height of the icon.
Even if I set the size of the list cell and the icon
public CustomCell()
{
setPrefHeight(30);
setMaxHeight(30);
icon.setContent("...");
icon.maxHeight(30);
icon.prefHeight(30);
}
it doesn´t work. The height of the list cell is correct, but the icon still is shown too large.
Where am I wrong?
EDIT:
This is the path of my svg:
M 289.00,74.00 C 299.18,61.21 307.32,52.80 320.00,42.42 331.43,33.07 343.66,26.03 357.00,19.84 427.64,-12.98 509.92,2.91 564.42,58.28 583.93,78.10 595.94,99.15 605.58,125.00 607.76,130.86 611.37,144.75 612.54,151.00 613.15,154.23 613.28,160.06 615.58,162.44 617.49,164.42 624.11,165.84 627.00,166.86 634.80,169.62 639.97,172.04 647.00,176.42 673.69,193.07 692.76,221.39 695.83,253.00 700.60,302.03 676.64,345.41 630.00,364.00 621.17,367.52 608.48,370.99 599.00,371.00 599.00,371.00 106.00,371.00 106.00,371.00 96.50,370.99 87.00,368.97 78.00,366.00 36.29,352.22 6.21,312.25 6.00,268.00 5.77,219.90 34.76,179.34 81.00,165.02 96.78,160.14 107.02,161.00 123.00,161.00 124.59,150.68 130.49,137.79 136.05,129.00 150.70,105.88 173.22,88.99 200.00,82.65 213.13,79.55 219.79,79.85 233.00,80.00 247.37,80.17 264.61,85.94 277.00,93.00 279.11,86.37 284.67,79.45 289.00,74.00 Z
Its a cloud which I want to show in a specific size (e.g. 30, 30).
But I can´t find a way how to set a specific size.

Use a Region
After using SVGPath for sometime, I found its best to set it as a shape to a Region and define min/pref/max size on this region. This way their is no need to use the scaleX/Y factors, which leads to a difference in layout bounds and bounds in parent sizes.
To achieve this, we need to:
Define a new Region and set the SVGPath as the shape.
Define the min/pref/max size for this region.
Set a background color for this region which gets set as the fill for the svg shape.
MCVE
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.SVGPath;
import javafx.stage.Stage;
public class SVGPathResize extends Application {
private static final double REQUIRED_WIDTH = 50.0;
private static final double REQUIRED_HEIGHT = 30.0;
#Override
public void start(Stage primaryStage) throws Exception {
SVGPath svg = new SVGPath();
svg.setContent("M 289.00,74.00 C 299.18,61.21 307.32,52.80 320.00,42.42 331.43,33.07 343.66,26.03 357.00,19.84 427.64,-12.98 509.92,2.91 564.42,58.28 583.93,78.10 595.94,99.15 605.58,125.00 607.76,130.86 611.37,144.75 612.54,151.00 613.15,154.23 613.28,160.06 615.58,162.44 617.49,164.42 624.11,165.84 627.00,166.86 634.80,169.62 639.97,172.04 647.00,176.42 673.69,193.07 692.76,221.39 695.83,253.00 700.60,302.03 676.64,345.41 630.00,364.00 621.17,367.52 608.48,370.99 599.00,371.00 599.00,371.00 106.00,371.00 106.00,371.00 96.50,370.99 87.00,368.97 78.00,366.00 36.29,352.22 6.21,312.25 6.00,268.00 5.77,219.90 34.76,179.34 81.00,165.02 96.78,160.14 107.02,161.00 123.00,161.00 124.59,150.68 130.49,137.79 136.05,129.00 150.70,105.88 173.22,88.99 200.00,82.65 213.13,79.55 219.79,79.85 233.00,80.00 247.37,80.17 264.61,85.94 277.00,93.00 279.11,86.37 284.67,79.45 289.00,74.00 Z");
final Region svgShape = new Region();
svgShape.setShape(svg);
svgShape.setMinSize(REQUIRED_WIDTH, REQUIRED_HEIGHT);
svgShape.setPrefSize(REQUIRED_WIDTH, REQUIRED_HEIGHT);
svgShape.setMaxSize(REQUIRED_WIDTH, REQUIRED_HEIGHT);
svgShape.setStyle("-fx-background-color: black;");
Scene scene = new Scene(new StackPane(svgShape), 200, 200);
primaryStage.setScene(scene);
primaryStage.show();
}
}
Use scaleX/Y on SVGPath
You can use the scaleX / scaleY to define the width and height of a Shape.
Find the original width/height of the Shape and then find the scaling factor by dividing the required width/height by the original values.
Scaling factor for width = required width / original width
Scaling factor for height = required height / original height
Now, you can use this scaling factor to scale the shape to the required size.
shape.setScaleX(Scaling factor for width);
shape.setScaleY(Scaling factor for height);
MCVE
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.SVGPath;
import javafx.stage.Stage;
public class SVGPathResize extends Application {
private static final double REQUIRED_WIDTH = 50.0;
private static final double REQUIRED_HEIGHT = 30.0;
#Override
public void start(Stage primaryStage) throws Exception {
SVGPath svg = new SVGPath();
svg.setContent("M 289.00,74.00 C 299.18,61.21 307.32,52.80 320.00,42.42 331.43,33.07 343.66,26.03 357.00,19.84 427.64,-12.98 509.92,2.91 564.42,58.28 583.93,78.10 595.94,99.15 605.58,125.00 607.76,130.86 611.37,144.75 612.54,151.00 613.15,154.23 613.28,160.06 615.58,162.44 617.49,164.42 624.11,165.84 627.00,166.86 634.80,169.62 639.97,172.04 647.00,176.42 673.69,193.07 692.76,221.39 695.83,253.00 700.60,302.03 676.64,345.41 630.00,364.00 621.17,367.52 608.48,370.99 599.00,371.00 599.00,371.00 106.00,371.00 106.00,371.00 96.50,370.99 87.00,368.97 78.00,366.00 36.29,352.22 6.21,312.25 6.00,268.00 5.77,219.90 34.76,179.34 81.00,165.02 96.78,160.14 107.02,161.00 123.00,161.00 124.59,150.68 130.49,137.79 136.05,129.00 150.70,105.88 173.22,88.99 200.00,82.65 213.13,79.55 219.79,79.85 233.00,80.00 247.37,80.17 264.61,85.94 277.00,93.00 279.11,86.37 284.67,79.45 289.00,74.00 Z");
resize(svg, REQUIRED_WIDTH, REQUIRED_HEIGHT);
Scene scene = new Scene(new StackPane(svg), 200, 200);
primaryStage.setScene(scene);
primaryStage.show();
}
private void resize(SVGPath svg, double width, double height) {
double originalWidth = svg.prefWidth(-1);
double originalHeight = svg.prefHeight(originalWidth);
double scaleX = width / originalWidth;
double scaleY = height / originalHeight;
svg.setScaleX(scaleX);
svg.setScaleY(scaleY);
}
}
OUTPUT
Additional Explanation
A vector graphic actually doesn´t have a size
Well not really, the path that you define while creating a Shape actually defines the size of the vector graph.
For example, I can create the same shape using :
M 100 100 L 300 100 L 200 300 z
and
M 10 10 L 30 10 L 20 30 z
but the latter is 10 times smaller than the former and so its actual size is also 10 times smaller.
If we scale the smaller triangle by a factor of 10, it will grow and be of the same size.

Related

How to reverse image of a rectangle in javafx

I have a javafx application and a rectangle inside it and I use rectangle.setFill() for filling the rectangle with an image. I want to know how I can reverse the image of my rectangle. ( I want to reverse it both vertically and horizontally and their combinations.)
Let's say the image I put on my rectangle is blue and has a red circle on the upper right part. I want to have images with the red circle being located in the lower right, lower left, and upper left side of the rectangle.
I have found some solutions with Canvas and GraphicsContext but it seems they are not applicable to Rectangle. Any solutions for Rectangle?
Also, I have put my rectangle in an anchorpane, if it's important to know.
The easiest option is probably to transform the node, rather than trying to flip the image itself. Two benefits of that are:
JavaFX provides easy ways to transform (translate, rotate, scale, etc.) nodes, and
You can use a single Image for all nodes.
The simplest transform to use in this case, as pointed out by #mipa, is scaling. To flip the node horizontally use node.setScaleX(-1). To flip the node vertically use node.setScaleY(-1).
Here's an example showing all four of your desired orientations:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.ImagePattern;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
double width = 250;
double height = 250;
Image image = new Image(/* your image URL */, width, height, true, true);
ImagePattern fill = new ImagePattern(image);
Rectangle normal = new Rectangle(width, height, fill);
Rectangle horizontal = new Rectangle(width, height, fill);
Rectangle vertical = new Rectangle(width, height, fill);
Rectangle both = new Rectangle(width, height, fill);
flipNode(horizontal, true, false);
flipNode(vertical, false, true);
flipNode(both, true, true);
GridPane grid = new GridPane();
grid.setVgap(10);
grid.setHgap(10);
grid.setPadding(new Insets(10));
grid.setAlignment(Pos.CENTER);
grid.add(normal, 0, 0);
grid.add(horizontal, 1, 0);
grid.add(vertical, 0, 1);
grid.add(both, 1, 1);
primaryStage.setScene(new Scene(grid));
primaryStage.show();
}
private void flipNode(Node node, boolean horiztonally, boolean vertically) {
node.setScaleX(horiztonally ? -1 : 1);
node.setScaleY(vertically ? -1 : 1);
}
}
The other transform you could use is rotation, but the above is easier.

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.imgur.com/I2vMLSD.png
A 10 x 10 matrix app when stretched:
https://i.imgur.com/9rR3e0g.png
A 100 x 100 matrix app when started (rectangle size is 5x5):
https://i.imgur.com/1KCKg6W.png
A 100 x 100 matrix app when stretched a bit, see how the matrix gets out of bounds:
https://i.imgur.com/pG8DxXA.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.
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.
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.
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));
}
});
}
}

Javafx setscale problems when enlarging group by dragging rectangle in the group

I am having an issue with scaling a group containing shapes, and dragging the shapes around.
In the example below, I have a group containing 2 rectangles, after scaling the group, if I drag one of the rectangles, the other will go negative the distance of the other, i.e., if I drag the rectangle r2 down, the rectangle r will go up, etc
I am wondering if this is some sort of bug, or what the issue is here?
The code below is a working example of what my issue is.
NOTE: I ran a test on the bounds, and it only changes when the dragged rectangle moves outside the current bounds.
if moving to the bottom right the new bounds will be something similar to
BoundingBox [minX:250.0, minY:250.0, minZ:0.0, width:301.7606201171875, height:338.6553955078125, depth:0.0, maxX:551.7606201171875, maxY:588.6553955078125, maxZ:0.0]
Even though the other rectangle moves outside of the minX and minY bounds.
import javafx.scene.shape.Rectangle;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class SampleScale extends Application {
#Override
public void start(Stage primaryStage) {
Group g = new Group();
Rectangle r = new Rectangle(250,250,10,10);
Rectangle r2 = new Rectangle(260,260,10,10);
r2.setFill(Color.RED);
r2.setOnMouseDragged(event
->
{
r2.setTranslateX(r2.getTranslateX() + (event.getX() -r2.getX()));
r2.setTranslateY(r2.getTranslateY() + (event.getY() -r2.getY()));
g.autosize();
event.consume();
});
Pane p = new Pane();
p.setPrefSize(1000, 1000);
g.getChildren().addAll(r,r2);
p.getChildren().addAll(g);// comment out to test bottom code;
//default works
//g.setScaleX(1);
//g.setScaleY(1);
//causes other node to move negative distance of the other object
// i.e., if r2 is dragged down, r will move up, etc.
g.setScaleX(2);
g.setScaleY(2);
//-------------------------
//this works if not placed inside a group
// p.getChildren().addAll(r,r2);
// r.setScaleX(2);
// r.setScaleY(2);
// r2.setScaleX(2);
// r2.setScaleY(2);
Scene scene = new Scene(p, 1300, 1250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
The scaleX and scaleY properties use the center of a node as pivot point for scaling. Since moving r2 resizes the Group, the other children also move.
You could apply a Scale transform with pivot point (0, 0) instead:
// g.setScaleX(2);
// g.setScaleY(2);
g.getTransforms().add(new Scale(2, 2, 0, 0));

JavaFx 8 - Scaling / zooming ScrollPane relative to mouse position

I need to zoom in / out on a scroll pane, relative to the mouse position.
I currently achieve the zooming functionality by wrapping my content in a Group, and scaling the group itself. I create a new Scale object with a custom pivot. (Pivot is set to the mouse position)
This works perfectly for where the Group's initial scale is 1.0, however scaling afterwards does not scale in the correct direction - I believe this is because the relative mouse position changes when the Group has been scaled.
My code:
#Override
public void initialize(URL location, ResourceBundle resources) {
Delta initial_mouse_pos = new Delta();
anchorpane.setOnScrollStarted(event -> {
initial_mouse_pos.x = event.getX();
initial_mouse_pos.y = event.getY();
});
anchorpane.setOnScroll(event -> {
double zoom_fac = 1.05;
double delta_y = event.getDeltaY();
if(delta_y < 0) {
zoom_fac = 2.0 - zoom_fac;
}
Scale newScale = new Scale();
newScale.setPivotX(initial_mouse_pos.x);
newScale.setPivotY(initial_mouse_pos.y);
newScale.setX( content_group.getScaleX() * zoom_fac );
newScale.setY( content_group.getScaleY() * zoom_fac );
content_group.getTransforms().add(newScale);
event.consume();
});
}
private class Delta { double x, y; }
How do I get the correct mouse position at different levels of scaling? Is there a completely different way to zooming the ScrollPane that is easier?
This is a scalable, pannable JavaFX ScrollPane :
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
public class ZoomableScrollPane extends ScrollPane {
private double scaleValue = 0.7;
private double zoomIntensity = 0.02;
private Node target;
private Node zoomNode;
public ZoomableScrollPane(Node target) {
super();
this.target = target;
this.zoomNode = new Group(target);
setContent(outerNode(zoomNode));
setPannable(true);
setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
setFitToHeight(true); //center
setFitToWidth(true); //center
updateScale();
}
private Node outerNode(Node node) {
Node outerNode = centeredNode(node);
outerNode.setOnScroll(e -> {
e.consume();
onScroll(e.getTextDeltaY(), new Point2D(e.getX(), e.getY()));
});
return outerNode;
}
private Node centeredNode(Node node) {
VBox vBox = new VBox(node);
vBox.setAlignment(Pos.CENTER);
return vBox;
}
private void updateScale() {
target.setScaleX(scaleValue);
target.setScaleY(scaleValue);
}
private void onScroll(double wheelDelta, Point2D mousePoint) {
double zoomFactor = Math.exp(wheelDelta * zoomIntensity);
Bounds innerBounds = zoomNode.getLayoutBounds();
Bounds viewportBounds = getViewportBounds();
// calculate pixel offsets from [0, 1] range
double valX = this.getHvalue() * (innerBounds.getWidth() - viewportBounds.getWidth());
double valY = this.getVvalue() * (innerBounds.getHeight() - viewportBounds.getHeight());
scaleValue = scaleValue * zoomFactor;
updateScale();
this.layout(); // refresh ScrollPane scroll positions & target bounds
// convert target coordinates to zoomTarget coordinates
Point2D posInZoomTarget = target.parentToLocal(zoomNode.parentToLocal(mousePoint));
// calculate adjustment of scroll position (pixels)
Point2D adjustment = target.getLocalToParentTransform().deltaTransform(posInZoomTarget.multiply(zoomFactor - 1));
// convert back to [0, 1] range
// (too large/small values are automatically corrected by ScrollPane)
Bounds updatedInnerBounds = zoomNode.getBoundsInLocal();
this.setHvalue((valX + adjustment.getX()) / (updatedInnerBounds.getWidth() - viewportBounds.getWidth()));
this.setVvalue((valY + adjustment.getY()) / (updatedInnerBounds.getHeight() - viewportBounds.getHeight()));
}
}
Did you try to remove the setOnScrollStarted-event and move its content to the setOnScroll-event?
Doing so reduces the need of your extra Delta-class and the computations of your mouse-positions are always on par with the current zoom factor.
I implemented the same thing and it works the way you are describing it.
Somehow like this:
#Override
public void initialize(URL location, ResourceBundle resources) {
anchorpane.setOnScroll(event -> {
double zoom_fac = 1.05;
if(delta_y < 0) {
zoom_fac = 2.0 - zoom_fac;
}
Scale newScale = new Scale();
newScale.setPivotX(event.getX);
newScale.setPivotY(event.getY);
newScale.setX( content_group.getScaleX() * zoom_fac );
newScale.setY( content_group.getScaleY() * zoom_fac );
content_group.getTransforms().add(newScale);
event.consume();
});
}
I believe this is a duplicate of this question which involves the same concepts at work. If you don't really care if it zooms relative to your mouse and just prefer it zoom in the center look at this question. If you need any more help comment below.
Assuming you want to have the following zoom behavior:
When the mouse wheel is pushed forward/backward the object under the cursor will be scaled up/down and the area under the cursor is now centered within the zooming area.
Eg. pushing the wheel forward while pointing at a place left from the center of the zooming area results in a 'up-scale and move right' action.
The scaling thing is as simple as you have already done so far.
The tricky part is the move action. There are some problem you have to consider within your calculations:
You have to calculate the difference from the center and the
position of the cursor. You can calculate this value by subtracting
the center point (cp) from the mouse position (mp).
If your zoom level is 1 and you point 50px left from the center you want to move your object 50px to the right, because 1px of your screen corresponds to one 1px of your object (picture). But if you doubled the size of your object, than 2 screen pixel are equal to on object pixel. You have to consider this when moving the object, because the translating part is always done before the scaling part. In other words you are moving your object in original size and the scaling is only the second independent step.
How is the scaling done? In JavaFX you simply set some scale-properties and JavaFX does the rest for you. But what does it exactly? The important thing to know is, where the fixed point is while zooming the object. If you scale an object there will be one point which stays fixed at its position while all other points are moving towards this point or moving way from it.
As the documentation of JavaFX says the center of the zoomed object will be the fixed point.
Defines the factor by which coordinates are scaled about the center of
the object along the X axis of this Node.
That means you have to ensure that your visual center point is equal to the one JavaFX uses while scaling you object. You can achieve this if you wrap your object within a container. Now zoom the container instead of the object and position the object within the container to fit your needs.
I hope this helps. If you need more help please offer a short working example project.

Difference scene.layout & scene.Group [duplicate]

In JavaFX, what is the difference between a Pane and a Group? I can't make out any difference.
A Group is not resizable (meaning that its size is not managed by its parent in the scene graph), and takes on the union of the bounds of its child nodes. (So, in other words, the local bounds of a Group will be the smallest rectangle containing the bounds of all the child nodes). If it is larger than the space it is allocated in its parent, it will be clipped.
By contrast, a Pane is resizable, so its size is set by its parent, which essentially determine its bounds.
Here is a quick demo. The Group is on top and the Pane below. Both contain a fixed blue square at (100,100) and a green square which is moved by pressing the left/right arrow keys. Note how at the beginning, the blue square appears in the top left corner of the group, because the local bounds of the group start at the top-leftmost point of all its child nodes (i.e. the local bounds of the group extend from (100, 100) right and down). As you move the green rectangles "off screen", the group adjusts its bounds to incorporate the changes, wherever possible, whereas the pane remains fixed.
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class GroupVsPaneDemo extends Application {
#Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
Group group = new Group();
VBox.setVgrow(group, Priority.NEVER);
VBox.setVgrow(pane, Priority.NEVER);
VBox vbox = new VBox(group, pane);
Rectangle rect1 = new Rectangle(100, 100, 100, 100);
Rectangle rect2 = new Rectangle(100, 100, 100, 100);
Rectangle rect3 = new Rectangle(200, 200, 100, 100);
Rectangle rect4 = new Rectangle(200, 200, 100, 100);
rect1.setFill(Color.BLUE);
rect2.setFill(Color.BLUE);
rect3.setFill(Color.GREEN);
rect4.setFill(Color.GREEN);
group.getChildren().addAll(rect1, rect3);
pane.getChildren().addAll(rect2, rect4);
Scene scene = new Scene(vbox, 800, 800);
scene.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
double deltaX ;
switch(e.getCode()) {
case LEFT:
deltaX = -10 ;
break ;
case RIGHT:
deltaX = 10 ;
break ;
default:
deltaX = 0 ;
}
rect3.setX(rect3.getX() + deltaX);
rect4.setX(rect4.getX() + deltaX);
});
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The few important difference between Pane and Group is that :
Pane can have its own size, where as a Group will take on the collective bounds of its children and is not directly resizable.
Pane can be used when you want to position its nodes at absolute position.
Also, note that Group was designed to be very lightweight and doesn't support a lot of styles. For example, you can't set border or background color for the group.
See this answer for more details.

Categories