Rectangle size binding to Scene size - JavaFX - java

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));
}
});
}
}

Related

Can we create partially colored text in Processing?

I want to learn to create partially colored text/String in processing(java) like below example. It can be 10% white 90% black. or any ratio for any two color. I would like to learn how to do it in processing.
Option 1: In Processing
Render your text into a PGraphics object and then iterate over the pixels[] of the object to change the colour of a proportion of the text.
In the example below, the colour ratio will not be correct unless you create a PGraphics object of the exact dimensions of the text - I'm not sure whether there is a programmatic way to determine the required dimensions. (As a result, I'm using a ratio of 0.6 to recolour the top ~50% of the text in the example).
Furthermore, noSmooth() has been called on the object as anti-aliasing will create pixels that aren't quite the original text colour, which makes replacing them more complicated than simply checking for equality (==).
import processing.core.PApplet;
import processing.core.PGraphics;
public class Text extends PApplet {
PGraphics text;
public static void main(String[] args) {
PApplet.main(Text.class);
}
#Override
public void settings() {
size(400, 400);
}
#Override
public void setup() {
makeText();
}
#Override
public void draw() {
background(55);
image(text, 100, 100);
}
public void makeText() {
final int orange = color(255,165,0);
final int yellow = color(255,255,0);
final float ratio = 0.6f;
text = createGraphics(150, 60);
text.noSmooth();
text.beginDraw();
text.fill(orange);
text.textSize(60);
text.textAlign(LEFT, TOP);
text.loadPixels();
text.text("TEXT", 0, 0);
text.endDraw();
for (int pixel = 0; pixel < (text.pixels.length * ratio); pixel++) {
if (text.pixels[pixel] == orange) {
text.pixels[pixel] = yellow;
}
}
}
}
Result:
Option 2: With JavaFX
This is a hacky method, but it gives better results since the text is anti-aliased. This method requires that your sketch be in FX2D rendering mode, which can be prescribed in the size() call.
Expose the stackPane belonging to the PApplet (this is where the JavaFX text object will be added):
Canvas canvas = (Canvas) ((PSurfaceFX) getSurface()).getNative();
StackPane p = (StackPane) canvas.getParent();
Create a JavaFX text object. I am using a CSS style (a linear gradient with immediate cutoff) for the partially-coloured effect.
javafx.scene.text.Text t = new javafx.scene.text.Text("TEXT");
t.setCache(true);
t.setFont(Font.font(null, FontWeight.NORMAL, 60));
t.setStyle("-fx-fill:linear-gradient( from 100.0% 100.0% to 100.0% 0.0%, rgb(255, 165, 0) 0.5," + "rgb(255, 255, 0)" +" 0.5);");
Finally, add the text to the PApplet's stage stackpane (which was exposed earlier):
p.getChildren().add(t);
Result (note the anti-aliasing):
Also note that you'lll need to use t.setVisible() to toggle visibility since this text element is entirely separate from Processing's drawing canvas.

How to set the size of a svgPath in JavaFx

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.

How do I make a Vbox use the entire space

This is javafx8, I have to write that here since i do not have enough rep to use the tag.
Simple example:
public class Test extends Application
{
public static void main(String args[]) throws Exception { launch(args); }
private Canvas canvas;
#Override
public void start(Stage primaryStage)
{
VBox box = new VBox();
Button dummyButton = new Button("some text");
canvas = new Canvas();
canvas.heightProperty().addListener(observable -> draw());
canvas.widthProperty().addListener(observable -> draw());
VBox.setVgrow(canvas, Priority.ALWAYS);
box.getChildren().addAll(dummyButton, canvas);
Scene scene = new Scene(box, 1300, 800);
primaryStage.setScene(scene);
primaryStage.show();
}
private void draw()
{
final double width = canvas.getWidth();
final double height = canvas.getHeight();
System.out.println("w, h: " + width + "; " + height);
final GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFill(Color.GREENYELLOW);
gc.fillRect(0, 0, width, height);
}
}
I probably misread the documentation of vbox, at the bottom it says that
For example, if a vbox needs the ListView to be allocated all extra space
and then does this VBox.grow thing, just as I did in my example. But the canvas is never changing his size. It always has a width and height of 0, while I expected the canvas to grow and shrink as I resize the window.
How can I get my canvas ( and I guess with that also my vbox ) to use the entire vertical space available.
Please do not suggest the use of the primary stage, in my real application I am so many layers away from that with many other elements in between. Primary stage is only here to have a runnable program.
Your draw method is a bit messed up.
private void draw() {
final double width = canvas.getWidth();
final double height = canvas.getHeight();
System.out.println("w, h: " + width + "; " + height);
final GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFill(Color.GREENYELLOW);
gc.fillRect(0, 0, width, height);
}
If we look at your method, you are doing a few things that can cause problems.
But first, as you noticed, the statement:
VBox.setVgrow(canvas, Priority.ALWAYS);
is causing you trouble. So let's just get rid of it and use your current structure.
If we just change your listeners from canvas to box:
canvas.heightProperty().addListener(observable -> draw());
canvas.widthProperty().addListener(observable -> draw());
to
box.heightProperty().addListener(observable -> draw());
box.widthProperty().addListener(observable -> draw());
We will now dynamically update things as your Vbox size changes. This means that your method draw will not be called when we resize, it wasn't doing that before so that's good!
So now back to the draw method. First of all, you are asking for the canvas width each time, when really we care about getting the canvas to match the box, so let's change that from canvas to box (You'll declare your Vbox as a field now).
private void draw() {
final double width = box.getWidth();
final double height = box.getHeight();
System.out.println("w, h: " + width + "; " + height);
final GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFill(Color.GREENYELLOW);
gc.fillRect(0, 0, width, height);
}
But this still won't work very well. That's because we never update the canvas size! So let's add a setWidth and a setHeight in
private void draw() {
final double width = box.getWidth();
final double height = box.getHeight();
canvas.setWidth(width);
canvas.setHeight(height);
System.out.println("w, h: " + width + "; " + height);
final GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFill(Color.GREENYELLOW);
gc.fillRect(0, 0, width, height);
}
With that, we'll notice that everything seems to be looking pretty good, with one exception. The button is now blocking the top of the screen and will make it so that you can't actually fill that top part. That's because the button was drawn first onto the screen and gets priority (If we switch it to be added at index 1 via a different add method call, we see the button disappears). I can't think of a way to fix that right now, but hopefully that solves part of your problem!

Scrollbar - the block increment seems variable not fixed

In the example below the block increment of the scrollbar is set to 100. However it appears that the actual value will be a value between 1 - 100 depending on where you click on the track bar.
When running the example try clicking roughly in the middle of the track several times to scroll down. When I do this I get output like:
ScrollBar Max: 400.0
LayoutY: -100.0
LayoutY: -200.0
LayoutY: -300.0
LayoutY: -375.0 // this varies depending on the click location
I am expecting the scroll units to change by 100 no matter where I click on the track. (ie, only a single label should be visible at a time).
Is this a common scrolling feature? If so, is there a way to turn it off?
Here is the code:
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Orientation;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class ScrollBarSSCCE extends Application
{
#Override
public void start(Stage stage)
{
int labelWidth = 300;
int labelHeight = 100;
String[] styles =
{
"-fx-background-color: #336699;",
"-fx-background-color: #996633;",
"-fx-background-color: #ff0000;",
"-fx-background-color: #00ff00;",
"-fx-background-color: #0000ff;"
};
VBox vb = new VBox();
for (int i = 0; i < styles.length; i++)
{
Label label = new Label();
label.setPrefWidth( labelWidth );
label.setPrefHeight( labelHeight );
label.setStyle( styles[i] );
vb.getChildren().add( label );
}
ScrollBar sc = new ScrollBar();
sc.setOrientation(Orientation.VERTICAL);
sc.setLayoutX(labelWidth);
sc.setMin(0);
sc.setMax( (styles.length - 1) * labelHeight);
System.out.println("ScrollBar Max: " + sc.getMax());
sc.setPrefHeight( labelHeight );
sc.setUnitIncrement( labelHeight / 2 );
sc.setBlockIncrement( labelHeight );
sc.valueProperty().addListener(
(ObservableValue<? extends Number> ov, Number old_val, Number new_val) ->
{
double y = -new_val.doubleValue();
System.out.println("LayoutY: " + y);
vb.setLayoutY( y );
});
Group root = new Group();
root.getChildren().addAll(vb, sc);
Scene scene = new Scene(root, labelWidth+20, labelHeight);
stage.setScene(scene);
stage.setTitle("ScrollBar SSCCE");
stage.show();
}
public static void main(String[] args)
{
launch(args);
}
}
i cant give a good explanation
System.out.println("block increment: " +sc.getBlockIncrement()); is always the same, and when i tested it, it happens on both sides going down or going up, i think it has to do with the ScrollBar.adjustValue(position); so if i am to provide some sort of solution i will say adjust the Value when you most need it.
//in your value listener you can add these codes
if(new_val.doubleValue() > old_val.doubleValue()){ //going down
if(sc.getMax()-sc.getBlockIncrement() < Math.abs(y)){
sc.adjustValue(1);
}
}else{//going up
if(old_val.doubleValue() == sc.getBlockIncrement()){
sc.adjustValue(0);
}
}
i think with this you might want to set sc.setUnitIncrement(labelHeight/2); the same as sc.setBlockIncrement(labelHeight); as that code might ruing the flow of UnitIncrement
hope it hepls :)
The ScrollBar.adjustValue(position) method is responsible for this behaviour.
It basically has three steps:
determine a "position value" by using the relative location of the mouse pressed
determine the "new value" by using the block increment value (this is the value I want)
Determine whether to use the "position value" or the "new value".
Since I always want the "new value" I just removed step 3 from the adjustValue(...) method.
Here is the code for the custom ScrollBar:
ScrollBar sc = new ScrollBar()
{
#Override
public void adjustValue(double position)
{
// figure out the "value" associated with the specified position
double posValue = ((getMax() - getMin()) * Utils.clamp(0, position, 1)) + getMin();
double newValue;
if (Double.compare(posValue, getValue()) != 0)
{
if (posValue > getValue())
{
newValue = getValue() + getBlockIncrement();
}
else
{
newValue = getValue() - getBlockIncrement();
}
setValue( Utils.clamp(getMin(), newValue, getMax()) );
}
}
};

How to add a value marker to JavaFX chart?

I am trying to build a series chart using JavaFX, where data is inserted dynamically.
Each time that a new value is inserted I would like to check if this is the highest value so far, and if so, I want to draw an horizontal line to show that this is the maximum value.
In JFree chart I would have used a ValueMarker, but I am trying to do the same with JavaFX.
I tried using the Line object, but it is definitely not the same, because I cannot provide the Chart values, it takes the relative pixel positions in the windows.
Here is the screenshot of chart I want to achieve:
http://postimg.org/image/s5fkupsuz/
Any suggestions?
Thank you.
To convert chart values to pixels you can use method NumberAxis#getDisplayPosition() which return actual coordinates of the chart nodes.
Although these coordinates are relative to chart area, which you can find out by next code:
Node chartArea = chart.lookup(".chart-plot-background");
Bounds chartAreaBounds = chartArea.localToScene(chartArea.getBoundsInLocal());
Note localToScene() method which allows you to convert any coordinates to Scene ones. Thus you can use them to update your value marker coordinates. Make sure you make localToScene call after your Scene have been shown.
See sample program below which produces next chart:
public class LineChartValueMarker extends Application {
private Line valueMarker = new Line();
private XYChart.Series<Number, Number> series = new XYChart.Series<>();
private NumberAxis yAxis;
private double yShift;
private void updateMarker() {
// find maximal y value
double max = 0;
for (Data<Number, Number> value : series.getData()) {
double y = value.getYValue().doubleValue();
if (y > max) {
max = y;
}
}
// find pixel position of that value
double displayPosition = yAxis.getDisplayPosition(max);
// update marker
valueMarker.setStartY(yShift + displayPosition);
valueMarker.setEndY(yShift + displayPosition);
}
#Override
public void start(Stage stage) {
LineChart<Number, Number> chart = new LineChart<>(new NumberAxis(0, 100, 10), yAxis = new NumberAxis(0, 100, 10));
series.getData().add(new XYChart.Data(0, 0));
series.getData().add(new XYChart.Data(10, 20));
chart.getData().addAll(series);
Pane pane = new Pane();
pane.getChildren().addAll(chart, valueMarker);
Scene scene = new Scene(pane);
// add new value on mouseclick for testing
chart.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
series.getData().add(new XYChart.Data(series.getData().size() * 10, 30 + 50 * new Random().nextDouble()));
updateMarker();
}
});
stage.setScene(scene);
stage.show();
// find chart area Node
Node chartArea = chart.lookup(".chart-plot-background");
Bounds chartAreaBounds = chartArea.localToScene(chartArea.getBoundsInLocal());
// remember scene position of chart area
yShift = chartAreaBounds.getMinY();
// set x parameters of the valueMarker to chart area bounds
valueMarker.setStartX(chartAreaBounds.getMinX());
valueMarker.setEndX(chartAreaBounds.getMaxX());
updateMarker();
}
public static void main(String[] args) {
launch();
}
}

Categories