JavaFx Canvas only resizes in height but not in width - java

I have started programming this app for ploting temperature data, when the screen gets resized, the canvas should resize as well. It initializes correctly but when I want to resize the window, the draw() method will only resize the height, according to the stackpanes height value it is bound to but it will ignore the width entirely. The Listener won't even fire. This I find very strange. Also I have to set the minSize for the StackPane, otherwise nothing will be draw at all. I'm not using FXML.
Does anybody have any ideas?
Edit I changed line 58 from setRight() to setCenter, as mentioned in the solution from InternetUnexplorer. But I was still curious about why it is so, so I did some research. I found this in the internet:
Top/Bottom area: Can shrink/expand horizontally and keep the height
unchanged.
Left/Right area: Can shrink/expand vertically and keep the length
unchanged.
Center area: Can shrink/expand in both directions.
http://o7planning.org/en/10629/javafx-borderpane-layout-tutorial
Here my main class:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class main extends Application {
#Override
public void start(Stage mainStage) throws Exception {
TempViewerWindow view = new TempViewerWindow();
ReadData controller = new ReadData(view);
controller.selectAll();
mainStage.setScene(new Scene(view));
//mainStage.setMinWidth(500);
//mainStage.setMinHeight(400);
mainStage.setTitle("Temperature Viewer");
mainStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
And here my view class:
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
public class TempViewerWindow extends BorderPane {
public TableView<TempData> tableView;
public Canvas canvas;
public GraphicsContext gc;
public StackPane holder;
public TempViewerWindow() {
tableView = new TableView<>();
TableColumn<TempData, String> col_date = new TableColumn<>("Date");
TableColumn<TempData, Float> col_temperature = new TableColumn<>("Temperature");
col_date.setCellValueFactory(e -> e.getValue()
.dateProperty());
col_temperature.setCellValueFactory(e -> e.getValue()
.temperatureProperty()
.asObject());
tableView.getColumns()
.addAll(col_date, col_temperature);
setLeft(tableView);
holder = new StackPane();
holder.setMinSize(400, 400); // must be here for some reason
canvas = new Canvas();
gc = canvas.getGraphicsContext2D();
canvas.widthProperty()
.bind(holder.widthProperty()
.subtract(10));
canvas.heightProperty()
.bind(holder.heightProperty()
.subtract(10));
canvas.widthProperty()
.addListener(observable -> redraw(gc));
canvas.heightProperty()
.addListener(observable -> redraw(gc));
holder.getChildren()
.add(canvas);
setCenter(holder); //here was the bug
}
private void redraw(GraphicsContext gc) {
gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
gc.setFill(Color.WHITE);
gc.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
System.out.println("redraw!");
}
}

Your canvas is being placed in the right of your BorderPane, when you should be placing it in the center.
If you change line 54 of TempViewerWindow from setRight(holder) to setCenter(holder), the scaling works properly.

You're using a BorderPane as parent for holder. The holder is used as right child of the BorderPane. However the right/left children are resized to their preferred widths and are resized to fill the height between the top and bottom nodes. (See javadoc)
The left and right children will be resized to their preferred widths and extend the length between the top and bottom nodes.
If you'd use holder as center, it's width would be adjusted the way you expect it.
Alternatively you could use a HBox as base class for TempViewerWindow to achieve this kind of behavior.
public TempViewerWindow() {
...
this.getChildren().addAll(tableView, holder);
HBox.setHgrow(holder, Priority.ALWAYS);
HBox.setHgrow(tableView, Priority.NEVER);
this.setFillHeight(true);
}

Related

How to center text without using any layout?

import javafx.geometry.VPos;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.text.TextBoundsType;
import javafx.stage.Stage;
public class HorizontalTextAlignment extends javafx.application.Application {
#Override
public void start(Stage stage) {
var rectangle = new Rectangle(600, 100, Color.TURQUOISE);
var text = new Text(rectangle.getWidth() / 2, rectangle.getHeight() / 2, "TEXT");
text.setBoundsType(TextBoundsType.VISUAL);
text.setFont(new Font(100));
text.setTextAlignment(TextAlignment.CENTER);
text.setTextOrigin(VPos.CENTER);
stage.setScene(new Scene(new Pane(rectangle, text)));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I would need to center the text without using any layout. As you may notice, while vertical alignment works, horizontal doesn't. Of course, this could be solved by a simple calculation.
// You need to change the horizontal position of the text for this to work.
var text = new Text(rectangle.getLayoutX(), rectangle.getHeight() / 2, "TEXT");
...
var rectangleWidth = rectangle.getWidth();
var textWidth = text.getLayoutBounds().getWidth();
text.setLayoutX((rectangleWidth - textWidth) / 2);
The problem is, although it's not easy to see in the picture that it's not exactly in the middle.
This is good to see if you only change the initialization.
var text = new Text(rectangle.getLayoutX(), rectangle.getHeight() / 2, "TEXT");
However, this cannot be solved by simply subtracting the desired value (in this case -2) from the horizontal position, because when I change the text.
It is necessary to subtract 9. And that is the problem. I can never know in advance how much must be deducted from the horizontal position.
How do I solve this, please?
Thank you
This could all be accomplished much more easily by using a StackPane (e.g. as shown in #Oboe's answer). Using the appropriate layout should always be your first choice. However, the rest of my answer shows how to do it manually.
The x and y properties define the origin point of the text node. The x-coordinate determines where the left side of the text starts. However, the y-coordinate can be customized somewhat via the textOrigin property.
X (always)
|
Y (VPos.TOP)-------╔════════════════════════════════╗
║ ║
Y (VPos.CENTER)----║ Text Node ║
║ ║
Y (VPos.BOTTOM)----╚════════════════════════════════╝
Note: There's also VPos.BASELINE (the default) but I don't know how to visualize that.
As you can see, when you set the x property to rectangle.getWidth() / 2 you are aligning the left side of the text node with the horizontal center of the rectangle. If you want to align the horizontal center of the text node with the horizontal center of the rectangle you have to take the width of the text node into account. Here's an example:
import javafx.application.Application;
import javafx.geometry.VPos;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.text.TextBoundsType;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage stage) {
var rectangle = new Rectangle(600, 100, Color.TURQUOISE);
var text = new Text("TEXT");
text.setBoundsType(TextBoundsType.VISUAL);
text.setFont(new Font(100));
text.setTextAlignment(TextAlignment.CENTER);
text.setTextOrigin(VPos.CENTER);
// WARNING: Assumes Rectangle is at (0, 0)
text.setX(rectangle.getWidth() / 2 - text.getLayoutBounds().getWidth() / 2);
text.setY(rectangle.getHeight() / 2);
stage.setScene(new Scene(new Pane(rectangle, text)));
stage.show();
}
}
Notice that I set the x property after setting everything else (e.g. font, text, etc.). That's necessary for the resulting text's width to be known. Of course, this is not responsive; if the the dimensions of the rectangle or text change you have to manually recompute the new x value. The ideal solution, when we ignore layouts, is to use a bindings. Something like:
// WARNING: Assumes Rectangle is at (0, 0)
text.xProperty()
.bind(
Bindings.createDoubleBinding(
() -> rectangle.getWidth() / 2 - text.getLayoutBounds().getWidth() / 2,
rectangle.widthProperty(),
text.layoutBoundsProperty()));
Unfortunately, changing the x property changes the layout bounds of the text and the above leads to a StackOverflowError. The same is true of the text's bounds-in-local (this is a characteristic of shapes). There is a solution though if you don't mind positioning the text via its layoutX and layoutY properties:
// WARNING: Assumes Rectangle is at (0, 0)
// leave text.x = 0 and text.y = 0
text.layoutXProperty()
.bind(
Bindings.createDoubleBinding(
() -> rectangle.getWidth() / 2 - text.getLayoutBounds().getWidth() / 2,
rectangle.widthProperty(),
text.layoutBoundsProperty()));
// assumes textOrigin = VPos.CENTER
text.layoutYProperty().bind(rectangle.heightProperty().divide(2));
Note the layoutX and layoutY properties come from Node. They're used by layouts to position their children. But since you're not using a layout which automatically positions its children you can safely set them manually. There's also the translateX and translateY properties. These add onto the layout properties. For example, ignoring other things, the final x-position will be layoutX + translateX (similar for the final y-position); for a Text node the final x-position would be x + layoutX + translateX (again, similar for the final y-position).
You may have noticed that all my examples warn about assuming the rectangle is at the origin of its parent. To fix that issue you simply have to add the rectangles x and y positions to the text's layout position.
Instead of a Text object, you can use a Label with its alignment set to center the text. Then you just need to force the Label’s minimum width to match the parent’s width (which is, roughly, mimicking what some layouts do):
#Override
public void start(Stage stage) {
var rectangle = new Rectangle(600, 100, Color.TURQUOISE);
var text = new Label("TEXT");
text.setFont(new Font(100));
text.setAlignment(Pos.CENTER);
Pane pane = new Pane(rectangle, text);
text.minWidthProperty().bind(pane.widthProperty());
text.minHeightProperty().bind(pane.heightProperty());
stage.setScene(new Scene(pane));
stage.show();
}
If are not forced to use those specific Nodes, I would use Label instead of Text and StackPane instead of Pane:
Rectangle rectangle = new Rectangle(600, 100, Color.TURQUOISE);
Label label = new Label("TEXT");
label.setAlignment(Pos.CENTER);
label.setFont(new Font(100));
StackPane.setAlignment(label, Pos.CENTER);
StackPane.setAlignment(rectangle, Pos.CENTER);
StackPane pane = new StackPane(rectangle, label);
pane.prefWidthProperty().bind(rectangle.widthProperty());
pane.prefHeightProperty().bind(rectangle.heightProperty());
stage.setScene(new Scene(pane));
stage.show();
Update
Given that you cannot use StackPane, here is a solution completely out of the box.
You can create a new Shape subtracting the Text from a Rectangle. Then you will have two Rectangles, one with the text transparent and another one for the background (that will give color to the text).
The benefit of these approach is that you end with two Shapes with the same bounds and will be easier to layout in the Scene.
import javafx.application.Application;
import javafx.geometry.VPos;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.text.TextBoundsType;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage stage) {
Rectangle rectangle = new Rectangle(600, 100);
Text text = new Text("TEXT");
text.setTextAlignment(TextAlignment.CENTER);
text.setBoundsType(TextBoundsType.VISUAL);
text.setFont(new Font(100));
text.setTextOrigin(VPos.CENTER);
text.setX(rectangle.getWidth() / 2 - text.getLayoutBounds().getWidth() / 2);
text.setY(rectangle.getHeight() / 2);
Shape shape = Shape.subtract(rectangle, text);
shape.setFill(Color.TURQUOISE);
shape.layoutXProperty().bind(rectangle.layoutXProperty());
shape.layoutYProperty().bind(rectangle.layoutYProperty());
shape.translateXProperty().bind(rectangle.translateXProperty());
shape.translateYProperty().bind(rectangle.translateYProperty());
stage.setScene(new Scene(new Pane(rectangle, shape)));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}

Scaling Nodes in Pane within AnchorPane JavaFX

I have a 2 Nodes (One Text, the other a Circle) in a Pane, and that pane is within an AnchorPane. The reason why I did this is I wanted to use the automatic scaling property that is a result of resizing the AnchorPane to adjust the size of the Nodes (like stretching and shrinking).
I also have a test background to see if I could scale the background aswell.
My issue is that while the background scales when I resize the window, the Nodes do not.
Heres my code:
package javacode;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.BackgroundImage;
import javafx.scene.layout.BackgroundPosition;
import javafx.scene.layout.BackgroundRepeat;
import javafx.scene.layout.BackgroundSize;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class Main extends Application {
public boolean debug = false;
Pane stack;
#Override
public void start(Stage stage) {
// Check for updates
/*
System.out.println("Checking for update");
boolean newUpdate = Updater.updateAvalible();
System.out.println("New update avalible: " + newUpdate);
if (newUpdate) {
Updater.showUpdatePrompt();
}
*/
// Get the debug background image
Image debugbackgroundimage = new Image(
this.getClass().getClassLoader().getResourceAsStream("resources/debugbackground.jpg"));
// Create a stack pane to add all the objects into
stack = new Pane();
// Setup the background
stack.setBackground(new Background(new BackgroundImage(debugbackgroundimage, BackgroundRepeat.NO_REPEAT,
BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(
debugbackgroundimage.getWidth(), debugbackgroundimage.getHeight(), false, false, true, true))));
// Set the preferred and minimum sizes for the stack
stack.setPrefSize(debugbackgroundimage.getWidth(), debugbackgroundimage.getHeight());
stack.setMinSize(16, 9);
// We need an anchor pane go help automatically constrain the maximum and
// minimum sizes of things
AnchorPane anchor = new AnchorPane();
AnchorPane.setTopAnchor(stack, 0.0);
AnchorPane.setBottomAnchor(stack, 0.0);
AnchorPane.setLeftAnchor(stack, 0.0);
AnchorPane.setRightAnchor(stack, 0.0);
// Add the stack to the anchor pane
anchor.getChildren().addAll(stack);
// Set the anchor background to a light gray, that way we can check for overlap
anchor.setBackground(new Background(new BackgroundFill(Color.LIGHTGRAY, CornerRadii.EMPTY, Insets.EMPTY)));
// Set the scene for the visualizer, use the anchor pane defined above
Scene mCatScene = new Scene(anchor);
// Set the contents of the window to that of the scene
stage.setScene(mCatScene);
// Show the window (stage)
stage.show();
// Add a test node to check if it scales properly
Text test = new Text(String.format("Test\nFoo bar"));
test.setFont(new Font("Arial", 15));
test.setWrappingWidth(500);
test.setFill(Color.RED);
test.setLayoutX(0);
test.setLayoutY(50);
Circle test2 = new Circle();
test2.setLayoutX(50);
test2.setLayoutY(120);
test2.setFill(Color.GREEN);
test2.setRadius(10);
stack.getChildren().add(0, test);
stack.getChildren().add(1, test2);
}
public static void main(String[] args) {
Main This = new Main();
// Check if debug mode is enabled (Basically check for console spam enabled)
try {
This.debug = Boolean.parseBoolean(args[0]);
} catch (Exception e) {
This.debug = false;
}
launch(args);
}
}
Here is the background image I used:
Edit: Here is a gif of how the background reacts (which is ideal), vs how the Nodes react (not ideal)

JavaFX FlowPane within ScrollPane, dynamically adjust scrollpane's content and size

I currently have a ScrollPane with a FlowPane as content. The FlowPane currently initializes with no children nodes, a fixed width and a pref/min height (but no max height).
While adding items to the FlowPane at runtime (I click some UI element and something is added to the FlowPane), the ScrollPane should adjust its height in the case that the addition to the FlowPane no longer fits.
I don't understand how to set the height of the flowPane and ScrollPane so that this works - if that's the problem to begin with. At the moment, whenever the addition to the FlowPane doesn't fit its initial height, the content is added, but not visible. The scrollbar belonging to the ScrollPane never adjusts its height - if it did, I could just scroll further down and see the content.
Let's say I have a ScrollPane with some width and height, some viewport width/height, and a FlowPane with some width/height - What should my settings be for the min/pref/max sizes? How can I make a scrollPane adjust its scrollbar behaviour or make the content visible?
The ScrollPane's setFitToHeight is already set to true, which didn't seem to change anything.
import javafx.application.Application;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class FlowPaneTest extends Application
{
public static void main(String[] args)
{
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception
{
// borderPane rootPane
BorderPane borderPane = new BorderPane();
borderPane.setMinSize(600, 600);
borderPane.setPrefSize(600, 600);
borderPane.setMaxSize(600, 600);
// container for the two scrollPanes below
FlowPane flow = new FlowPane();
borderPane.setRight(flow);
// two scrollPanes, each should resize it's height (width should be fixed) if
// children are added beyond it's current height
ScrollPane top = new ScrollPane();
ScrollPane bottom = new ScrollPane();
FlowPane scrollPaneContent = new FlowPane();
top.setContent(scrollPaneContent);
bottom.setContent(scrollPaneContent);
flow.getChildren().add(top);
flow.getChildren().add(bottom);
borderPane.setOnMouseClicked(new EventHandler<Event>()
{
#Override
public void handle(Event event)
{
Label l = new Label("test");
l.setMinSize(100, 100);
l.setPrefSize(100, 100);
l.setMaxSize(100, 100);
scrollPaneContent.getChildren().add(l);
}
});
// size settings
int width = 300, height = 300;
top.setHvalue(0.5);
top.setMinViewportHeight(height);
top.setPrefViewportHeight(height);
top.setMinViewportWidth(width);
top.setPrefViewportWidth(width);
top.setHbarPolicy(ScrollBarPolicy.ALWAYS);
top.setFitToHeight(true);
top.setMinSize(width, height);
top.setPrefSize(width, height);
top.setMaxWidth(width);
scrollPaneContent.setMinSize(width, height);
scrollPaneContent.setPrefSize(width, height);
scrollPaneContent.setMaxWidth(width);
scrollPaneContent.setPrefHeight(height);
bottom.setMinSize(width, height);
bottom.setPrefSize(width, height);
bottom.setMaxWidth(width);
bottom.setHvalue(0.5);
bottom.setMinViewportHeight(height);
bottom.setPrefViewportHeight(height);
bottom.setMinViewportWidth(width);
bottom.setPrefViewportWidth(width);
bottom.setHbarPolicy(ScrollBarPolicy.ALWAYS);
top.setFitToHeight(true);
bottom.setFitToHeight(true);
// stage
Scene scene = new Scene(borderPane, 600.0, 600.0);
primaryStage.setScene(scene);
primaryStage.show();
}
}
Try to give the ScrollPane pref height and width and add this line
scrollPane.setFitToWidth(true);
Ending up with something similar to this ugly bit of code - It listens to the number of children in the pane and increases the size every time something is added to the list of children:
topSubPane.getChildren().addListener(new ListChangeListener()
{
#Override
public void onChanged(Change c)
{
c.next();
topSubPane.setPrefHeight(topSubPane.getHeight() + 50);
}
});
Works, but feels like an unorthodox hack. Is there really no regular way of doing this?

JavaFX: canvas improperly resized while in the TabsPane

I'm trying to create an app that used JavaFX for GUI. The main windows has a set of tabs, one of which has a canvas that is used for visualization. I have noticed that resizing a canvas is not an easy task and there are lots of the tips and question ralated to this.
I took this code as the example. It works fine, until I try to put it into a TabsPane. When I enlarge the window the image enlarges too, but when I shirk the window, canvas is not shinked! Moreover, the panel on the top of the tab is not shrinked too.
Here is a screenhot of the window that demostrates the issue:
Here is the code:
import javafx.application.Application;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
/**
* Tip 1: A canvas resizing itself to the size of
* the parent pane.
*/
public class Tip1ResizableCanvas extends Application {
class ResizableCanvas extends Canvas {
public ResizableCanvas() {
// Redraw canvas when size changes.
widthProperty().addListener(evt -> draw());
heightProperty().addListener(evt -> draw());
}
private void draw() {
double width = getWidth();
double height = getHeight();
GraphicsContext gc = getGraphicsContext2D();
gc.clearRect(0, 0, width, height);
gc.setStroke(Color.RED);
gc.strokeLine(0, 0, width, height);
gc.strokeLine(0, height, width, 0);
}
#Override
public boolean isResizable() {
return true;
}
#Override
public double prefWidth(double height) {
return getWidth();
}
#Override
public double prefHeight(double width) {
return getHeight();
}
}
#Override
public void start(Stage stage) throws Exception {
TabPane tabs = new TabPane();
Tab tab = new Tab("tab", createNewTaskArea());
tabs.getTabs().add(tab);
stage.setScene(new Scene(tabs, 500, 500));
stage.setTitle("Tip 1: Resizable Canvas");
stage.show();
}
private Pane createNewTaskArea() {
Label newTaskNameQuery = new Label("Task name:");
TextField newTaskName = new TextField("Untitled task");
newTaskNameQuery.setLabelFor(newTaskName);
GridPane topPane = new GridPane();
topPane.setPadding(new Insets(5));
topPane.setHgap(5);
topPane.setVgap(10);
topPane.add(newTaskNameQuery, 0, 0);
topPane.add(newTaskName, 1, 0);
GridPane.setHalignment(newTaskNameQuery, HPos.RIGHT);
GridPane.setHgrow(newTaskName, Priority.ALWAYS);
ResizableCanvas canvas = new ResizableCanvas();
VBox panel = new VBox();
// Bind canvas size to stack pane size.
panel.widthProperty().addListener(observable -> canvas.setWidth(panel.getWidth()));
panel.heightProperty().addListener(
observable -> canvas.setHeight(panel.getHeight() - topPane.getHeight())
);
panel.getChildren().addAll(topPane, canvas);
return panel;
}
public static void main(String[] args) {
launch(args);
}
}
Is it possible to make it work properly?
It seems that the TabPane causes the problems, becasue if I remove it and make contents of the tab to be contents of the entire window everything works just like as I expect.
This does not work since for the VBox to reduce it's size, the Canvas's size would need to be reduced which does not happen, since the Canvas size is only decreased when the VBox size is reduced...
It's much easier to simply put the Canvas inside a resizeable parent, set the appropriate layout parameters for it and resize the Canvas based on the size of this parent:
class ResizableCanvas extends Canvas {
public ResizableCanvas() {
// Redraw canvas when size changes.
widthProperty().addListener(evt -> draw());
heightProperty().addListener(evt -> draw());
}
private void draw() {
double width = getWidth();
double height = getHeight();
GraphicsContext gc = getGraphicsContext2D();
gc.clearRect(0, 0, width, height);
gc.setStroke(Color.RED);
gc.strokeLine(0, 0, width, height);
gc.strokeLine(0, height, width, 0);
}
}
VBox panel = new VBox();
// parent to be resized
Pane pane = new Pane(canvas);
// grow/shrink pane when VBox height is increased/decreased
VBox.setVgrow(pane, Priority.ALWAYS);
// bind canvas size to parent size
canvas.widthProperty().bind(pane.widthProperty());
canvas.heightProperty().bind(pane.heightProperty());
panel.getChildren().addAll(topPane, pane);

Frosted Glass Effect in JavaFX?

I'm making an iOS7-themed JavaFX2/FXML project and I was wondering how I could make a Rectangle object have a iOS7-like frosted glass effect.
I'd also like it to have a small shadow. This is tricky, since you might be able to see the shadow behind the semi-transparent object. I'd just like it to be present around the edges.
Is this possible? Here's a picture showing the desired effect (not including the small drop-shadow):
UPDATE: Here's a continuation of the issue. This is going to look amazing :D.
Sample Solution
Run the program below and scroll or swipe up to show the glass pane.
The purpose of the program is just to sample the techniques involved not to act as a general purpose library for the frost effect.
import javafx.animation.*;
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.geometry.Rectangle2D;
import javafx.scene.*;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.effect.*;
import javafx.scene.image.*;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
// slides a frost pane in on scroll or swipe up; slides it out on scroll or swipe down.
public class Frosty extends Application {
private static final double W = 330;
private static final double H = 590;
private static final double BLUR_AMOUNT = 60;
private static final Duration SLIDE_DURATION = Duration.seconds(0.4);
private static final double UPPER_SLIDE_POSITION = 100;
private static final Effect frostEffect =
new BoxBlur(BLUR_AMOUNT, BLUR_AMOUNT, 3);
#Override public void start(Stage stage) {
DoubleProperty y = new SimpleDoubleProperty(H);
Node background = createBackground();
Node frost = freeze(background, y);
Node content = createContent();
content.setVisible(false);
Scene scene = new Scene(
new StackPane(
background,
frost,
content
)
);
stage.setScene(scene);
stage.show();
addSlideHandlers(y, content, scene);
}
// create a background node to be frozen over.
private Node createBackground() {
Image backgroundImage = new Image(
getClass().getResourceAsStream("ios-screenshot.png")
);
ImageView background = new ImageView(backgroundImage);
Rectangle2D viewport = new Rectangle2D(0, 0, W, H);
background.setViewport(viewport);
return background;
}
// create some content to be displayed on top of the frozen glass panel.
private Label createContent() {
Label label = new Label("The overlaid text is clear and the background below is frosty.");
label.setStyle("-fx-font-size: 25px; -fx-text-fill: midnightblue;");
label.setEffect(new Glow());
label.setMaxWidth(W - 20);
label.setWrapText(true);
return label;
}
// add handlers to slide the glass panel in and out.
private void addSlideHandlers(DoubleProperty y, Node content, Scene scene) {
Timeline slideIn = new Timeline(
new KeyFrame(
SLIDE_DURATION,
new KeyValue(
y,
UPPER_SLIDE_POSITION
)
)
);
slideIn.setOnFinished(e -> content.setVisible(true));
Timeline slideOut = new Timeline(
new KeyFrame(
SLIDE_DURATION,
new KeyValue(
y,
H
)
)
);
scene.setOnSwipeUp(e -> {
slideOut.stop();
slideIn.play();
});
scene.setOnSwipeDown(e -> {
slideIn.stop();
slideOut.play();
content.setVisible(false);
});
// scroll handler isn't necessary if you have a touch screen.
scene.setOnScroll((ScrollEvent e) -> {
if (e.getDeltaY() < 0) {
slideOut.stop();
slideIn.play();
} else {
slideIn.stop();
slideOut.play();
content.setVisible(false);
}
});
}
// create a frosty pane from a background node.
private StackPane freeze(Node background, DoubleProperty y) {
Image frostImage = background.snapshot(
new SnapshotParameters(),
null
);
ImageView frost = new ImageView(frostImage);
Rectangle filler = new Rectangle(0, 0, W, H);
filler.setFill(Color.AZURE);
Pane frostPane = new Pane(frost);
frostPane.setEffect(frostEffect);
StackPane frostView = new StackPane(
filler,
frostPane
);
Rectangle clipShape = new Rectangle(0, y.get(), W, H);
frostView.setClip(clipShape);
clipShape.yProperty().bind(y);
return frostView;
}
public static void main(String[] args) { launch(args); }
}
Source image
Save this image parallel to the Java source as a file named ios-screenshot.png and have your build system copy it to the target directory for the binary output of the build.
Answers to additional questions
"JDK 8," would that happen to be a requirement of this?
The sample code above is written against JDK 8. Porting it back to JDK 7 by replacing the lambda calls with anonymous inner classes is pretty trivial.
In general, Java 7 is pretty dated for JavaFX work. I advise upgrading at your earliest convenience to work with a Java 8 minimum version.
initiating your Panes with arguments
More convenient constructors for most parent nodes is a Java 8 feature. You can easily convert the Java 8 format:
StackPane stack = new StackPane(child1, child2);
To Java 7:
StackPane stack = new StackPane();
stack.getChildren().setAll(child1, child2);
will this working if the desktop is behind a frosty pane?
Not directly, you can create a new question for that.
Update: Related Questions
User created: JavaFX effect on background to allow the frosted effect to apply to a window over a desktop background.
Another user created: How do I create a JavaFX transparent stage with shadows on only the border? to apply a halo shadow effect around this window.

Categories