I have this simple class:
Test.java:
import javafx.animation.FadeTransition;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Test extends Application {
#Override
public void start(Stage stage) throws Exception {
Pane pane = new Pane();
Button testButton = new Button("Test");
testButton.setStyle("-fx-background-color: green;");
pane.getChildren().add(testButton);
pane.setStyle("-fx-background-color: red;");
FadeTransition transition = new FadeTransition(Duration.millis(5000), pane);
transition.setFromValue(1.0);
transition.setToValue(0.0);
transition.setCycleCount(Timeline.INDEFINITE);
transition.setAutoReverse(true);
transition.play();
Scene scene = new Scene(pane, 500, 500);
stage.setMinWidth(500);
stage.setMinHeight(500);
stage.setTitle("Test");
stage.setResizable(false);
stage.setScene(scene);
stage.show();
}
}
It looks like this:
when it fades however it becomes this:
How do I make it so that the fade transition only affects the red background and doesn't affect the green button?
So that it looks like this:
using stackpane
You can use StackPane as root and both : Pane and Button children of stackpane . Button is not affected by transition since is no longer child of pane .
if you need different aligments for different nodes you can use static method setAligment from StackPane class , wich requires a child node and position as arguments
public class App extends Application {
#Override
public void start(Stage stage) throws Exception {
Pane pane = new Pane();
Button testButton = new Button("Test");
testButton.setStyle("-fx-background-color: green;");
StackPane stackPane = new StackPane(pane,testButton);
stackPane.setAlignment(Pos.TOP_LEFT);
pane.setStyle("-fx-background-color: red;");
FadeTransition transition = new FadeTransition(Duration.millis(5000), pane);
transition.setFromValue(1.0);
transition.setToValue(0.0);
transition.setCycleCount(Timeline.INDEFINITE);
transition.setAutoReverse(true);
transition.play();
Scene scene = new Scene(stackPane, 500, 500);
stage.setMinWidth(500);
stage.setMinHeight(500);
stage.setTitle("Test");
stage.setResizable(false);
stage.setScene(scene);
stage.show();
}
}
This uses the alternative approach suggested by Slaw in the comments:
you could also animate the background color. Though it's a little complicated, since you can't animate the background property directly. You have to animate the color and set the background yourself every time the color changes (Color implements Interpolatable).
I also would prefer Giovanni's solution over this. This solution is mainly offered as an example of how you might create a custom transition.
There are two solutions provided.
The key to these solutions is the same in both cases, extend Transition and override the interpolate method to set the required background fill for a given interpolation value.
Interpolates between two colors (the start color and a transparent color).
protected void interpolate(double frac) {
Color cur = from.interpolate(to, frac);
target.setBackground(BackgroundUtil.createBackground(cur));
}
OR
Interpolates just the opacity portion of a single color.
protected void interpolate(double frac) {
Color cur = Color.color(
color.getRed(),
color.getGreen(),
color.getBlue(),
color.getOpacity() * frac
);
target.setBackground(BackgroundUtil.createBackground(cur));
}
Executable Example
The output of the example is similar to the screenshot in Giovanni's solution.
import javafx.animation.Transition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Duration;
public class BackgroundFade extends Application {
#Override
public void start(Stage stage) throws Exception {
Button testButton = new Button("Test");
testButton.setStyle("-fx-background-color: green;");
Pane pane = new Pane(testButton);
pane.setPrefSize(500, 500);
pane.setBackground(
BackgroundUtil.createBackground(Color.RED)
);
//applyBackgroundColorTransition(pane);
applyBackgroundOpacityTransition(pane);
stage.setScene(new Scene(pane));
stage.show();
}
private void applyBackgroundColorTransition(Pane pane) {
BackgroundColorTransition backgroundColorTransition = new BackgroundColorTransition(
Duration.seconds(5),
pane,
Color.RED,
Color.TRANSPARENT
);
backgroundColorTransition.setCycleCount(Transition.INDEFINITE);
backgroundColorTransition.setAutoReverse(true);
backgroundColorTransition.play();
}
private void applyBackgroundOpacityTransition(Pane pane) {
BackgroundOpacityTransition backgroundOpacityTransition = new BackgroundOpacityTransition(
Duration.seconds(5),
pane,
Color.RED
);
backgroundOpacityTransition.setCycleCount(Transition.INDEFINITE);
backgroundOpacityTransition.setAutoReverse(true);
backgroundOpacityTransition.play();
}
}
class BackgroundColorTransition extends Transition {
private final Region target;
private final Color from;
private final Color to;
public BackgroundColorTransition(Duration duration, Region target, Color from, Color to) {
setCycleDuration(duration);
this.target = target;
this.from = from;
this.to = to;
}
#Override
protected void interpolate(double frac) {
Color cur = from.interpolate(to, frac);
target.setBackground(BackgroundUtil.createBackground(cur));
}
}
class BackgroundOpacityTransition extends Transition {
private final Region target;
private final Color color;
public BackgroundOpacityTransition(Duration duration, Region target, Color color) {
setCycleDuration(duration);
this.target = target;
this.color = color;
}
#Override
protected void interpolate(double frac) {
Color cur = Color.color(
color.getRed(),
color.getGreen(),
color.getBlue(),
color.getOpacity() * frac
);
target.setBackground(BackgroundUtil.createBackground(cur));
}
}
class BackgroundUtil {
public static Background createBackground(Color color) {
return new Background(
new BackgroundFill(
color, null, null
)
);
}
}
Related
I'm developing a simple image editing functionality as a part of a larger JavaFX application, but I'm having some trouble to work out the undo/zoom and draw requirements together.
My requirements are the following:
The user should be able to:
Draw freehand on the image
Zoom in and out the image
Undo the changes
If the canvas is bigger than the window, it should have scroll-bars.
How I implemented these requirements:
The Drawing is done by starting a line when the mouse is pressed on the canvas, stroking it when it is dragged and closing the path when the button is released.
The Zoom works by scaling the canvas to a higher or lower value.
The Undo method takes a snapshot of the current state of the canvas when the mouse is pressed (before any change is made) and push it to a Stack of Images. When I need to undo some change I pop the last image of the Stack and draw it on the canvas, replacing the current image by the last one.
To have scroll-bars I just place the Canvas inside a Group and a ScrollPane.
Everything works fine, except when I try to draw on a scaled canvas. Due to the way I implemented the Undo functionality, I have to scale it back to 1, take a snapshot of the Node then scale it back to the size it was before. When this happens and the user is dragging the mouse the image position changes below the mouse pointer, causing it to draw a line that shouldn't be there.
Normal (unscaled canvas):
Bug (scaled canvas)
I tried the following approaches to solve the problem:
Don't re-scale to take the snapshot - Doesn't cause the unwanted line, but I end up with different image sizes in the stack, if it's smaller (zoomed out) when the snapshot was taken I now have a lower resolution of the image that I can't scale up without losing quality.
Tweak the logic and put the pushUndo call to the mouseReleased event - It almost worked, but when the user scrolled to a place and it's drawing there, the re-scaling causes the image to scroll back to the top-left;
Tried to search an way to "clone" or serialize the canvas and store the object state in the Stack - Didn't found anything I was able to adapt, and JavaFX doesn't support serialization of its objects.
I think the problem can be solved either by reworking the undo functionality as it doesn't need to re-scale the canvas to copy its state or by changing the way I zoom the canvas without scaling it, but I'm out of ideas on how to implement either of those options.
Below is the functional code example to reproduce the problem:
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.Stack;
public class Main extends Application {
Stack<Image> undoStack;
Canvas canvas;
double canvasScale;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
canvasScale = 1.0;
undoStack = new Stack<>();
BorderPane borderPane = new BorderPane();
HBox hbox = new HBox(4);
Button btnUndo = new Button("Undo");
btnUndo.setOnAction(actionEvent -> undo());
Button btnIncreaseZoom = new Button("Increase Zoom");
btnIncreaseZoom.setOnAction(actionEvent -> increaseZoom());
Button btnDecreaseZoom = new Button("Decrease Zoom");
btnDecreaseZoom.setOnAction(actionEvent -> decreaseZoom());
hbox.getChildren().addAll(btnUndo, btnIncreaseZoom, btnDecreaseZoom);
ScrollPane scrollPane = new ScrollPane();
Group group = new Group();
canvas = new Canvas();
canvas.setWidth(400);
canvas.setHeight(300);
group.getChildren().add(canvas);
scrollPane.setContent(group);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setLineWidth(2.0);
gc.setStroke(Color.RED);
canvas.setOnMousePressed(mouseEvent -> {
pushUndo();
gc.beginPath();
gc.lineTo(mouseEvent.getX(), mouseEvent.getY());
});
canvas.setOnMouseDragged(mouseEvent -> {
gc.lineTo(mouseEvent.getX(), mouseEvent.getY());
gc.stroke();
});
canvas.setOnMouseReleased(mouseEvent -> {
gc.lineTo(mouseEvent.getX(), mouseEvent.getY());
gc.stroke();
gc.closePath();
});
borderPane.setTop(hbox);
borderPane.setCenter(scrollPane);
Scene scene = new Scene(borderPane, 800, 600);
stage.setScene(scene);
stage.show();
}
private void increaseZoom() {
canvasScale += 0.1;
canvas.setScaleX(canvasScale);
canvas.setScaleY(canvasScale);
}
private void decreaseZoom () {
canvasScale -= 0.1;
canvas.setScaleX(canvasScale);
canvas.setScaleY(canvasScale);
}
private void pushUndo() {
// Restore the canvas scale to 1 so I can get the original scale image
canvas.setScaleX(1);
canvas.setScaleY(1);
// Get the image with the snapshot method and store it on the undo stack
Image snapshot = canvas.snapshot(null, null);
undoStack.push(snapshot);
// Set the canvas scale to the value it was before the method
canvas.setScaleX(canvasScale);
canvas.setScaleY(canvasScale);
}
private void undo() {
if (!undoStack.empty()) {
Image undoImage = undoStack.pop();
canvas.getGraphicsContext2D().drawImage(undoImage, 0, 0);
}
}
}
Consider drawing Shape objects, in this case Path objects, and apply scale to them:
import java.util.Stack;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;
public class Main extends Application {
private Path path;
private Stack<Path> undoStack;
private Group group;
private double scale = 1;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
undoStack = new Stack<>();
Button btnUndo = new Button("Undo");
btnUndo.setOnAction(actionEvent -> undo());
Button btnIncreaseZoom = new Button("Increase Zoom");
btnIncreaseZoom.setOnAction(actionEvent -> increaseZoom());
Button btnDecreaseZoom = new Button("Decrease Zoom");
btnDecreaseZoom.setOnAction(actionEvent -> decreaseZoom());
HBox hbox = new HBox(4, btnUndo, btnIncreaseZoom, btnDecreaseZoom);
group = new Group();
BorderPane root = new BorderPane(new Pane(group), hbox, null,null, null);
Scene scene = new Scene(root, 300, 400);
root.setOnMousePressed(mouseEvent -> newPath(mouseEvent.getX(), mouseEvent.getY()));
root.setOnMouseDragged(mouseEvent -> addToPath(mouseEvent.getX(), mouseEvent.getY()));
primaryStage.setScene(scene);
primaryStage.show();
}
private void newPath(double x, double y) {
path = new Path();
path.setStrokeWidth(1);
path.setStroke(Color.BLACK);
path.getElements().add(new MoveTo(x,y));
group.getChildren().add(path);
undoStack.add(path);
}
private void addToPath(double x, double y) {
path.getElements().add(new LineTo(x, y));
}
private void increaseZoom() {
scale += 0.1;
reScale();
}
private void decreaseZoom () {
scale -= 0.1;
reScale();
}
private void reScale(){
for(Path path : undoStack){
path.setScaleX(scale);
path.setScaleY(scale);
}
}
private void undo() {
if(! undoStack.isEmpty()){
Node node = undoStack.pop();
group.getChildren().remove(node);
}
}
}
I solved the problem by extending the Canvas component and adding a second canvas in the extended class to act as a copy of the main canvas.
Every time I made a change in the canvas I do the same change in this "carbon" canvas. When I need to re-scale the canvas to get the snapshot (the root of my problem) I just re-scale the "carbon" canvas back to 1 and get my snapshot from it. This doesn't cause the drag of the mouse in the main canvas, as it remains scaled during this process. Probably this isn't the optimal solution, but it works.
Below is the code for reference, to anyone who may have a similar problem in the future.
ExtendedCanvas.java
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import java.util.Stack;
public class ExtendedCanvas extends Canvas {
private final double ZOOM_SCALE = 0.1;
private final double MAX_ZOOM_SCALE = 3.0;
private final double MIN_ZOOM_SCALE = 0.2;
private double currentScale;
private final Stack<Image> undoStack;
private final Stack<Image> redoStack;
private final Canvas carbonCanvas;
private final GraphicsContext gc;
private final GraphicsContext carbonGc;
public ExtendedCanvas(double width, double height){
super(width, height);
carbonCanvas = new Canvas(width, height);
undoStack = new Stack<>();
redoStack = new Stack<>();
currentScale = 1.0;
gc = this.getGraphicsContext2D();
carbonGc = carbonCanvas.getGraphicsContext2D();
setEventHandlers();
}
private void setEventHandlers() {
this.setOnMousePressed(mouseEvent -> {
pushUndo();
gc.beginPath();
gc.lineTo(mouseEvent.getX(), mouseEvent.getY());
carbonGc.beginPath();
carbonGc.lineTo(mouseEvent.getX(), mouseEvent.getY());
});
this.setOnMouseDragged(mouseEvent -> {
gc.lineTo(mouseEvent.getX(), mouseEvent.getY());
gc.stroke();
carbonGc.lineTo(mouseEvent.getX(), mouseEvent.getY());
carbonGc.stroke();
});
this.setOnMouseReleased(mouseEvent -> {
gc.lineTo(mouseEvent.getX(), mouseEvent.getY());
gc.stroke();
gc.closePath();
carbonGc.lineTo(mouseEvent.getX(), mouseEvent.getY());
carbonGc.stroke();
carbonGc.closePath();
});
}
public void zoomIn() {
if (currentScale < MAX_ZOOM_SCALE ) {
currentScale += ZOOM_SCALE;
setScale(currentScale);
}
}
public void zoomOut() {
if (currentScale > MIN_ZOOM_SCALE) {
currentScale -= ZOOM_SCALE;
setScale(currentScale);
}
}
public void zoomNormal() {
currentScale = 1.0;
setScale(currentScale);
}
private void setScale(double value) {
this.setScaleX(value);
this.setScaleY(value);
carbonCanvas.setScaleX(value);
carbonCanvas.setScaleY(value);
}
private void pushUndo() {
redoStack.clear();
undoStack.push(getSnapshot());
}
private Image getSnapshot(){
carbonCanvas.setScaleX(1);
carbonCanvas.setScaleY(1);
Image snapshot = carbonCanvas.snapshot(null, null);
carbonCanvas.setScaleX(currentScale);
carbonCanvas.setScaleY(currentScale);
return snapshot;
}
public void undo() {
if (hasUndo()) {
Image redo = getSnapshot();
redoStack.push(redo);
Image undoImage = undoStack.pop();
gc.drawImage(undoImage, 0, 0);
carbonGc.drawImage(undoImage, 0, 0);
}
}
public void redo() {
if (hasRedo()) {
Image undo = getSnapshot();
undoStack.push(undo);
Image redoImage = redoStack.pop();
gc.drawImage(redoImage, 0, 0);
carbonGc.drawImage(redoImage, 0, 0);
}
}
public boolean hasUndo() {
return !undoStack.isEmpty();
}
public boolean hasRedo() {
return !redoStack.isEmpty();
}
}
Main.java
package com.felipepaschoal;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class Main extends Application {
ExtendedCanvas extendedCanvas;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
BorderPane borderPane = new BorderPane();
HBox hbox = new HBox(4);
Button btnUndo = new Button("Undo");
btnUndo.setOnAction(actionEvent -> extendedCanvas.undo());
Button btnRedo = new Button("Redo");
btnRedo.setOnAction(actionEvent -> extendedCanvas.redo());
Button btnDecreaseZoom = new Button("-");
btnDecreaseZoom.setOnAction(actionEvent -> extendedCanvas.zoomOut());
Button btnResetZoom = new Button("Reset");
btnResetZoom.setOnAction(event -> extendedCanvas.zoomNormal());
Button btnIncreaseZoom = new Button("+");
btnIncreaseZoom.setOnAction(actionEvent -> extendedCanvas.zoomIn());
hbox.getChildren().addAll(
btnUndo,
btnRedo,
btnDecreaseZoom,
btnResetZoom,
btnIncreaseZoom
);
ScrollPane scrollPane = new ScrollPane();
Group group = new Group();
extendedCanvas = new ExtendedCanvas(300,200);
group.getChildren().add(extendedCanvas);
scrollPane.setContent(group);
borderPane.setTop(hbox);
borderPane.setCenter(scrollPane);
Scene scene = new Scene(borderPane, 600, 400);
stage.setScene(scene);
stage.show();
}
}
I've googled enough but still can find solution to get only single resize event when user releases left mouse button. For example the following solution from here
stage.titleProperty().bind(
scene.widthProperty().asString().
concat(" : ").
concat(scene.heightProperty().asString()));
When user clicks mouse left button and starts resizing the stage we will get very many events (using property listeners) while he does resizing. However, I want to get only one event - when the user completes resizing and releases mouse left button.
Another solution is here This solution significantly decreases amount of events but still doesn't let to get only one.
How to get only one resize event after user releases mouse button?
As far as I know, the mouse event handlers that resize the stage are managed natively, and so there is no way to access those purely in JavaFX - to do this the way you describe would require writing native libraries and hooking into them.
If you are doing some heavy computation (or other work that takes a long time) in response to the change in size of the stage, your best bet is probably to write code that only processes one change at a time, and just processes the last known change when it can.
An example of this is:
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class StageResizeThrottling extends Application {
private Random rng = new Random();
#Override
public void start(Stage primaryStage) {
BlockingQueue<Point2D> dimensionChangeQueue = new ArrayBlockingQueue<>(1);
ChangeListener<Number> dimensionChangeListener = (obs, oldValue, newValue) -> {
dimensionChangeQueue.clear();
dimensionChangeQueue.add(new Point2D(primaryStage.getWidth(), primaryStage.getHeight()));
};
primaryStage.widthProperty().addListener(dimensionChangeListener);
primaryStage.heightProperty().addListener(dimensionChangeListener);
Thread processDimensionChangeThread = new Thread(() -> {
try {
while (true) {
System.out.println("Waiting for change in size");
Point2D size = dimensionChangeQueue.take();
System.out.printf("Detected change in size to [%.1f, %.1f]: processing%n", size.getX(), size.getY());
process(size, primaryStage);
System.out.println("Done processing");
}
} catch (InterruptedException letThreadExit) { }
});
processDimensionChangeThread.setDaemon(true);
processDimensionChangeThread.start();
Scene scene = new Scene(new StackPane(), 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private void process(Point2D stageDimension, Stage stage) throws InterruptedException {
// simulate slow process:
Thread.sleep(500 + rng.nextInt(1000));
final String title = String.format("Width: %.0f Height: %.0f", stageDimension.getX(), stageDimension.getY());
Platform.runLater(() -> stage.setTitle(title));
}
public static void main(String[] args) {
launch(args);
}
}
Note that this will always process the very first change immediately, and then process the latest change when each previously-processed change has finished processing. If no further changes have occurred, it will wait until one does occur and then process it immediately. If you like, you can combine this with the timer-based technique you linked for coalescing the changes in the listener, which will typically remove the very first change that is processed (which is usually redundant as it is almost always followed by subsequent changes). The following changes will wait until no resizes have occurred for 300ms before submitting one to the queue for processing (the thread still behaves the same way - it will process the latest change, and when that processing is complete, wait for another one):
BlockingQueue<Point2D> dimensionChangeQueue = new ArrayBlockingQueue<>(1);
PauseTransition coalesceChanges = new PauseTransition(Duration.millis(300));
coalesceChanges.setOnFinished(e -> {
dimensionChangeQueue.clear();
dimensionChangeQueue.add(new Point2D(primaryStage.getWidth(), primaryStage.getHeight()));
});
ChangeListener<Number> dimensionChangeListener = (obs, oldValue, newValue) ->
coalesceChanges.playFromStart();
primaryStage.widthProperty().addListener(dimensionChangeListener);
primaryStage.heightProperty().addListener(dimensionChangeListener);
There's some tuning here, which is a tradeoff between latency and over-eagerness in processing changes. You probably want the pause transition to last something shorter than the average processing time of the change in screen size, but not an order of magnitude shorter.
The code guarantees that no more than one change will be processed at a time and that the latest change will eventually be processed if no more changes occur. This is probably about as good as you can get without accessing native user events. (And it would also handle programmatic changes in the stage size, which a mouse handler would not handle.)
I tried to create an example to achieve what you are looking for, I ended up with this, it is not perfect but when I tested it, it looked like it could help:
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import javafx.util.Duration;
public class OneEventJavaFX extends Application{
double originalWidth = 400; // the initial width of Scene when the program starts
double originalHeight = 400; // the initial height of Scene when the program starts
// boolean property to be observed in order to know the completion of stage resize
BooleanProperty completedProperty = new SimpleBooleanProperty(false);
Timeline timeline;
#Override
public void start(Stage stage) throws Exception {
Pane root = new Pane(); // simple root as example just for test purpose
Scene scene = new Scene(root, 400,400);
stage.setScene(scene);
stage.setTitle("OneEventJavaFX");
stage.show();
// because I could not find a way to implement MouseEvent.MOUSE_RELEASED
// on the stage to notify the completion on resizing, I had to use a TimeLine
// the duration should consider the time the user usually take to finish every resize
// duration is tricky, Very Slow Resizing V.S Very Fast Resizing!
timeline = new Timeline(new KeyFrame(Duration.seconds(1), e ->{
System.out.println("Resizing Should Be Completed By Now!");
originalWidth = scene.getWidth(); // record the new scene size
originalHeight = scene.getHeight();
completedProperty.setValue(false);
}));
// change listener, to be added to and removed from the scene
ChangeListener<Number> changeListener= (observable, oldValue, newValue) ->{
System.out.println("I am Detecting an Event!"); // test
// once the size changed
if(originalWidth-scene.getWidth()>1 || scene.getWidth()-originalWidth>1 ||
originalHeight-scene.getHeight()>1 || scene.getHeight()-originalHeight>1){
completedProperty.set(true); // notify that completion should be considered
System.out.println("I Stopped! No More Events!");
timeline.play(); // and start counting the time
}};
// add the change listener when the program starts up
scene.widthProperty().addListener(changeListener);
scene.heightProperty().addListener(changeListener);
System.out.println("ChangeListener Added At Startup!");
// now listen to the change of the boolean property value
// instead of the size changes, it should NOT take a lot of work
// then accordingly add and remove change listener!
completedProperty.addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable,
Boolean notComplete, Boolean complete) {
if (complete) {
scene.widthProperty().removeListener(changeListener);
scene.heightProperty().removeListener(changeListener);
System.out.println("ChangeListener Removed!");
}
else{
scene.widthProperty().addListener(changeListener);
scene.heightProperty().addListener(changeListener);
System.out.println("ChangeListener Added Back!");
}
}
});
}
public static void main(String[] args) {
launch();
}
}
Test While Resizing
ChangeListener Added At Startup!
I am Detecting an Event!
I am Detecting an Event!
ChangeListener Removed!
I Stopped! No More Events!
Resizing Should Be Completed By Now!
ChangeListener Added Back!
UPDATE:
I have been working on solving this question, I believe this approach can achieve what you want.
The idea is as follows:
Create UNDECORATED Stage and Make it Resizable.
Create a Title Bar and add it to the Stage.
Now the Mouse Events can be detected on the Border of the Stage (because basically it happens on the Scene).
Create Double Property for both the Width and Height of Stage and add Change Listener to listen to the Changes.
The changes in the Stage Width & Height will only be recorded at the beginning of the drag and when user RELEASES the Mouse.
Explanations in Comments.
The whole solution can be found here as an archive file (Why? Because I tried to post it here fully but the Body Limit is 30000 Character!) .
OneEventStage Class:
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
/**
* This class customize a given Stage to record the changes
* of its size only when user starts and finishes resizing (recording one event)
* #author Yahya Almardeny
* #version 28/05/2017
*/
public class OneEventStage{
private double originalWidth; // the initial width of Scene when the program starts
private double originalHeight; // the initial height of Scene when the program starts
private TitleBar titleBar; // can be customized by the setter method (by default I made it for Windows 10 style)
private boolean started, alreadyFullScreen;
private DoubleProperty widthChange, heightChange; // record the changes in size
public Scene s;
public BorderPane scene; // this will be considered as a Scene when used in the program
public OneEventStage(Stage stage, double width, double height){
originalWidth = width; originalHeight = height;
widthChange = new SimpleDoubleProperty(originalWidth);
heightChange = new SimpleDoubleProperty(originalHeight);
started = false;
titleBar = new TitleBar("");
scene = new BorderPane();
scene.setTop(titleBar.getTitleBar());
s = new Scene(scene, originalWidth,originalHeight);
stage.initStyle(StageStyle.UNDECORATED);
stage.setScene(s);
ResizeHelper.addResizeListener(stage);
Task<Void> task = new Task<Void>(){
#Override
protected Void call() throws Exception {
Platform.runLater(new Runnable(){
#Override
public void run() {
// change listener, to be added to and removed from the scene
ChangeListener<Number> changeListener= (observable, oldValue, newValue) ->{
if(isFullScreen()){
widthChange.setValue(stage.getWidth());
heightChange.setValue(stage.getHeight());
alreadyFullScreen=true;
}
else if (alreadyFullScreen){ // coming from full screen mode
widthChange.setValue(Screen.getPrimary().getVisualBounds().getWidth());
heightChange.setValue(Screen.getPrimary().getVisualBounds().getHeight());
widthChange.setValue(originalWidth);
heightChange.setValue(originalHeight);
alreadyFullScreen = false;
}
else if(!alreadyFullScreen && !started){
started = true; // to inform the detecting Mouse Release Event is required
}
};
s.setOnMouseReleased(e->{
if(started){ // if this happens particularly after changing the size/dragging
originalWidth = stage.getWidth(); // record the new scene size
originalHeight = stage.getHeight();
widthChange.setValue(originalWidth); // add it
heightChange.setValue(originalHeight);
started = false;
}
});
// add the change listener when the program starts up
s.widthProperty().addListener(changeListener);
s.heightProperty().addListener(changeListener);
}
});
return null;
}};
new Thread(task).start();
}
/*
* to detected if user clicked on maximize button or double click on the title bar
*/
private boolean isFullScreen(){
return this.s.getWindow().getWidth()==Screen.getPrimary().getVisualBounds().getWidth() &&
this.s.getWindow().getHeight()==Screen.getPrimary().getVisualBounds().getHeight();
}
public DoubleProperty getWidthChange() {
return widthChange;
}
public DoubleProperty getHeightChange() {
return heightChange;
}
public TitleBar getTitleBar() {
return titleBar;
}
public void setTitleBar(TitleBar titleBar) {
this.titleBar = titleBar;
}
public void setTitle(String title){
titleBar.getTitle().setText(title);
}
}
OneEventStageTest Class:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
* Implementing an Example of OneEventStage to test it
* #author Yahya Almardeny
* #version 28/05/2017
*/
public class OneEventStageTest extends Application{
#Override
public void start(Stage primaryStage) throws Exception {
// create stage
OneEventStage stage = new OneEventStage(primaryStage, 400,400);
stage.setTitle("One Event Stage");
// simple containers and its components for testing purpose
VBox container = new VBox();
container.setAlignment(Pos.CENTER);
HBox widthInfoContainer = new HBox();
widthInfoContainer.setAlignment(Pos.CENTER);
Label widthChangeL = new Label("Width Changes");
TextField widthChangeV = new TextField();
widthChangeV.setEditable(false);
widthInfoContainer.getChildren().addAll(widthChangeL, widthChangeV);
HBox.setMargin(widthChangeL, new Insets(10));
HBox.setMargin(widthChangeV, new Insets(10));
HBox heightInfoContainer = new HBox();
heightInfoContainer.setAlignment(Pos.CENTER);
Label heightChangeL = new Label("Height Changes");
TextField heightChangeV = new TextField();
heightChangeV.setEditable(false);
heightInfoContainer.getChildren().addAll(heightChangeL, heightChangeV);
HBox.setMargin(heightChangeL, new Insets(10));
HBox.setMargin(heightChangeV, new Insets(10));
container.getChildren().addAll(widthInfoContainer, heightInfoContainer);
//////////////////////////////////////////////////////////////////////////
DoubleProperty widthChange = stage.getWidthChange();
DoubleProperty heightChange = stage.getHeightChange();
// listen to the changes (Testing)
widthChange.addListener((obs, old, newV)->{
Platform.runLater(new Runnable(){
#Override
public void run() {
widthChangeV.setText("From(" + old.doubleValue() + ") To(" + newV.doubleValue() + ")");
}
});
});
heightChange.addListener((obs, old, newV)->{
Platform.runLater(new Runnable(){
#Override
public void run() {
heightChangeV.setText("From(" + old.doubleValue() + ") To(" + newV.doubleValue() + ")");
}
});
});
//////////////////////////////////////////////////////////////////////////////////////
// represent a root but in fact it's inside the real root (BorderPane in the OneEventStage Class!).
StackPane root = new StackPane();
root.setAlignment(Pos.CENTER);
root.getChildren().add(container);
stage.scene.setCenter(root);
primaryStage.show();
}
public static void main(String[] args) {
launch();
}
}
TitleBar Class:
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.stage.Screen;
import javafx.stage.Stage;
/**
* This class to create a default/customized Title Bar
* to be added to Undecorated Stage in JavaFX Application
* #author Yahya Almardeny
* #version 27/05/2017
*/
public class TitleBar {
private HBox titleBar;
private ImageView icon;
private StackPane close, minimize, maximize; // represent customized components for the title bar (by using the second constructor)
private Image maximizeBefore, maximizeAfter; // for changing maximize icon when it's full screen
private Label title;
private double height, stageWidth, stageHeight, x,y, offsetX, offsetY;
private double screenWidth = Screen.getPrimary().getVisualBounds().getWidth(),
screenHeight = Screen.getPrimary().getVisualBounds().getHeight();
private Color backgroundColor;
private StackPane maximizeButton; // for default title bar
private Label minimizeButton, closeButton; // for default title bar
private Stage stage;
private boolean intialized = false, fromMax = false;
public static enum Components {ICON,TITLE,MINIMIZE,MAXIMIZE,CLOSE;}
/**
* the default constructor, appearance of Windows 10
* #param title
*/
public TitleBar(String title){
titleBar = new HBox();
icon = new ImageView(new Image(TitleBar.class.getResourceAsStream("/icon/icon.png")));
icon.setFitWidth(15); this.icon.setFitHeight(13);
closeButton = new Label("×");
closeButton.setFont(Font.font("Times New Roman", 25));
closeButton.setPrefWidth(46);
closeButton.setAlignment(Pos.CENTER);
minimizeButton = new Label("—");
minimizeButton.setFont(Font.font(10));
minimizeButton.setPrefWidth(46);
minimizeButton.setPrefHeight(29);
minimizeButton.setAlignment(Pos.CENTER);
maximizeButton = maximiazeButton();
this.title = new Label(title);
final Pane space = new Pane();
HBox.setHgrow(space,Priority.ALWAYS);
titleBar.getChildren().addAll(this.icon, this.title,space,this.minimizeButton, this.maximizeButton, this.closeButton);
titleBar.setAlignment(Pos.CENTER_RIGHT);
HBox.setMargin(this.icon, new Insets(0,5,0,10)); // top,right, bottom, left
initalize(); // private method to get the Stage for first time
setDefaultControlsFunctionality(); // private method to add the default controls functionality
}
/**
* This is constructor to create a custom title bar
* #param icon
* #param minimize
* #param maximize
* #param close
* #param title
*/
public TitleBar(Image icon, Image minimize, Image maximizeBefore, Image maximizeAfter, Image close, String title){
titleBar = new HBox();
this.icon = new ImageView(icon);
this.icon.setFitWidth(15); this.icon.setFitHeight(14); // values can be changed via setters
this.close = new StackPane();
this.close.setPrefSize(25, 20);
this.close.getChildren().add(new ImageView(close));
((ImageView) this.close.getChildren().get(0)).setFitWidth(20);
((ImageView) this.close.getChildren().get(0)).setFitHeight(20);
this.minimize = new StackPane();
this.minimize.setPrefSize(25, 20);
this.minimize.getChildren().add(new ImageView(minimize));
((ImageView) this.minimize.getChildren().get(0)).setFitWidth(20);
((ImageView) this.minimize.getChildren().get(0)).setFitHeight(20);
this.maximizeBefore = maximizeBefore;
this.maximize = new StackPane();
this.maximize.setPrefSize(25, 20);
this.maximize.getChildren().add(new ImageView(maximizeBefore));
((ImageView) this.maximize.getChildren().get(0)).setFitWidth(20);
((ImageView) this.maximize.getChildren().get(0)).setFitHeight(20);
this.maximizeAfter = maximizeAfter;
this.title = new Label(title);
final Pane space = new Pane();
HBox.setHgrow(space,Priority.ALWAYS);
titleBar.getChildren().addAll(this.icon, this.title,space,this.minimize, this.maximize, this.close);
titleBar.setAlignment(Pos.CENTER_RIGHT);
HBox.setMargin(this.icon, new Insets(0,5,0,10)); // top,right, bottom, left
HBox.setMargin(this.close, new Insets(0,5,0,0));
initalize();
setCustomizedControlsFunctionality();
}
/**
* create the default maximize button
* #return container
*/
private StackPane maximiazeButton(){
StackPane container = new StackPane();
Rectangle rect = new Rectangle(8,8);
rect.setFill(Color.TRANSPARENT);
rect.setStroke(Color.BLACK);
container.setPrefWidth(46);
container.getChildren().add(rect);
return container;
}
/**
* To get the Stage of the application for one time only
* as well as adding listener to iconifiedProperty()
*/
private void initalize(){
titleBar.setOnMouseEntered(e->{ // the entire block will be executed only once
if(!intialized){
// get the stage and assign it to the Stage field
stage = ((Stage)titleBar.getScene().getWindow());
// add listener toiconifiedProperty()
stage.iconifiedProperty().addListener(ee->{
if(!stage.isIconified()){
stage.setMaximized(true);
if(fromMax){ // if already maximized
stage.setWidth(screenWidth);
stage.setHeight(screenHeight);
stage.setX(0);
stage.setY(0);
}
else{
stage.setWidth(stageWidth);
stage.setHeight(stageHeight);
stage.setX(x);
stage.setY(y);
}
try { // to remove the flash
Thread.sleep(10);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
stage.setOpacity(1.0);
}
});
intialized=true;
}
});
}
/**
* To add functionality to title bar controls
* via event listeners
*/
private void setDefaultControlsFunctionality(){
// Double-Click on Title Bar
titleBar.setOnMouseClicked(e->{
if(e.getClickCount()==2){
maximizefunctonality();
}
});
//Maximize Control
maximizeButton.setOnMouseEntered(e->{// highlight when hover
maximizeButton.setBackground(
new Background(new BackgroundFill(Color.LIGHTGRAY,null,null)));
((Rectangle)maximizeButton.getChildren().get(0)).setFill(Color.LIGHTGRAY);
if(maximizeButton.getChildren().size()==2){
((Rectangle)maximizeButton.getChildren().get(1)).setFill(Color.LIGHTGRAY);
}
});
maximizeButton.setOnMouseExited(e->{ // remove highlight
maximizeButton.setBackground(
new Background(new BackgroundFill(Color.TRANSPARENT,null,null)));
((Rectangle)maximizeButton.getChildren().get(0)).setFill(Color.TRANSPARENT);
if(maximizeButton.getChildren().size()==2){
((Rectangle)maximizeButton.getChildren().get(1)).setFill(Color.WHITE);
}
});
maximizeButton.setOnMouseClicked(e->{
maximizefunctonality();
});
//Close Control
closeButton.setOnMouseEntered(e->{
closeButton.setBackground(
new Background(new BackgroundFill(Color.CRIMSON,null,null)));
closeButton.setTextFill(Color.WHITE);
});
closeButton.setOnMouseExited(e->{
closeButton.setBackground(
new Background(new BackgroundFill(Color.TRANSPARENT,null,null)));
closeButton.setTextFill(Color.BLACK);
});
closeButton.setOnMouseClicked(e->{
stage.close();
});
//Minimize Control
minimizeButton.setOnMouseEntered(e->{
minimizeButton.setBackground(
new Background(new BackgroundFill(Color.LIGHTGRAY,null,null)));
});
minimizeButton.setOnMouseExited(e->{
minimizeButton.setBackground(
new Background(new BackgroundFill(Color.TRANSPARENT,null,null)));
});
minimizeButton.setOnMouseClicked(e->{
if(!stage.isIconified()){ // if it's not minimized
if(fromMax){ // check if it's already full screen(maximized)
stage.setOpacity(0.0);
stage.setIconified(true); // minimize it
}
else{ // if it's not -> record the size and position
stageWidth = stage.getWidth();
stageHeight = stage.getHeight();
x = stage.getX();
y = stage.getY();
stage.setOpacity(0.0);
stage.setIconified(true); // minimize it
}
}
});
// to make title bar movable
titleBar.setOnMousePressed(e->{
if(stage.getWidth()<screenWidth || stage.getHeight()<screenHeight){
offsetX = e.getScreenX() - stage.getX();
offsetY = e.getScreenY() - stage.getY();
}
});
titleBar.setOnMouseDragged(e->{
if(stage.getWidth()<screenWidth || stage.getHeight()<screenHeight){
stage.setX(e.getScreenX() - offsetX);
stage.setY(e.getScreenY() - offsetY);
}
});
}
private void maximizefunctonality(){
Rectangle rect = (Rectangle) maximizeButton.getChildren().get(0);
if(stage.getWidth()<screenWidth||stage.getHeight()<screenHeight){
// get the previous size + position
stageWidth = stage.getWidth();
stageHeight = stage.getHeight();
x = stage.getX();
y = stage.getY();
// maximize it
stage.setWidth(screenWidth);
stage.setHeight(screenHeight);
stage.centerOnScreen();
// change the maximize button appearance
rect.setTranslateX(2);
rect.setTranslateY(-2);
Rectangle rect1 = new Rectangle(8,8);
rect1.setFill(Color.WHITE);
rect1.setStroke(Color.BLACK);
maximizeButton.getChildren().add(rect1);
fromMax = true;
}
else{ // if already maximized -> return to previous size + position
stage.setWidth(stageWidth);
stage.setHeight(stageHeight);
stage.setX(x);
stage.setY(y);
fromMax = false;
// change the maximize button appearance
rect.setTranslateX(0);
rect.setTranslateY(0);
maximizeButton.getChildren().remove(1);
}
}
private void setCustomizedControlsFunctionality(){
//Maximize Control
maximize.setOnMouseClicked(e->{
if(stage.getWidth()<screenWidth||stage.getHeight()<screenHeight){
// get the previous size + position
stageWidth = stage.getWidth();
stageHeight = stage.getHeight();
x = stage.getX();
y = stage.getY();
// maximize it
stage.setWidth(screenWidth);
stage.setHeight(screenHeight);
stage.centerOnScreen();
// change the maximize button appearance
((ImageView) maximize.getChildren().get(0)).setImage(maximizeAfter);
fromMax = true;
}
else{ // if already maximized -> return to previous size + position
stage.setWidth(stageWidth);
stage.setHeight(stageHeight);
stage.setX(x);
stage.setY(y);
fromMax = false;
// change the maximize button appearance
((ImageView) maximize.getChildren().get(0)).setImage(maximizeBefore);
}
});
close.setOnMouseClicked(e->{
stage.close();
});
//Minimize Control
minimize.setOnMouseClicked(e->{
if(!stage.isIconified()){ // if it's not minimized
if(fromMax){ // check if it's already full screen(maximized)
stage.setOpacity(0.0);
stage.setIconified(true); // minimize it
}
else{ // if it's not -> record the size and position
stageWidth = stage.getWidth();
stageHeight = stage.getHeight();
x = stage.getX();
y = stage.getY();
stage.setOpacity(0.0);
stage.setIconified(true); // minimize it
}
}
});
// to make title bar movable
titleBar.setOnMousePressed(e->{
if(stage.getWidth()<screenWidth || stage.getHeight()<screenHeight){
offsetX = e.getScreenX() - stage.getX();
offsetY = e.getScreenY() - stage.getY();
}
});
titleBar.setOnMouseDragged(e->{
if(stage.getWidth()<screenWidth || stage.getHeight()<screenHeight){
stage.setX(e.getScreenX() - offsetX);
stage.setY(e.getScreenY() - offsetY);
}
});
}
/**
* To change margins/insets to the Title Bar components
* #param component
* #param top
* #param right
* #param bottom
* #param left
*/
public void setInsets(Components component, double top, double right, double bottom, double left){
switch(component){
case TITLE:
HBox.setMargin(title, new Insets(top, right, bottom ,left));
break;
case ICON:
HBox.setMargin(icon, new Insets(top, right, bottom ,left));
break;
case CLOSE:
HBox.setMargin(close, new Insets(top, right, bottom ,left));
break;
case MAXIMIZE:
HBox.setMargin(maximize, new Insets(top, right, bottom ,left));
break;
case MINIMIZE:
HBox.setMargin(minimize, new Insets(top, right, bottom ,left));
break;
}
}
public void setControlsSpace(Components component, double width, double height){
switch(component){
case CLOSE:
close.setPrefSize(width, height);
break;
case MAXIMIZE:
maximize.setPrefSize(width, height);
break;
case MINIMIZE:
minimize.setPrefSize(width, height);
break;
case TITLE:
//do nothing
break;
case ICON:
// do nothing
break;
}
}
public void addHoverEffect(Components component, Color defaultColor, Color onHover, Cursor cursor){
}
//reset of the class
{...}
}
ResizeHelper Class:
{....}
Test
I use certain events to place small rectangles on an HBox. Their placement is perfect when the window has not been resized, but when you for example go from small to fullscreen, their placement is wrong (of course, because they get a certain value for X at the time of placement - this is measured by getting the width of the HBox at that specific moment).
Question:
How can I make these positions dynamic, so when I resize the window, they stay in proportion?
Pictures:
Code:
#FXML HBox tagLine; // initializes the HBox
...
public void addTag(String sort) {
Rectangle rect = new Rectangle(20, tagLine.getHeight());
double pos = timeSlider.getValue() / 100 * tagLine.getWidth(); // retrieves position at the moment of being called
rect.setTranslateX(pos);
rect.setOnMouseEntered(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
showNotification("Gemarkeerde gebeurtenis: " + sort);
}
});
rect.setOnMouseExited(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
notificationHide.play();
}
});
tagLine.getChildren().add(rect);
}
Few things that you need to take into account while translating a shape with accountable size is that you need to :
Translate the shape from its center
If the translation is dependent on the width of a Node, listen to the changes made to the with of that particular node and make changes to the translate property accordingly
Both of the above points seem to be missing in your implementation. You are never listening to width property of HBox. Neither is your calculation for pos taking the center of Rectangle into account.
Here is an example which try to keep the Rectangle at the center no matter what the size of your HBox is.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class Main extends Application {
private static final int SIDE = 40;
private static final double DEFAULT_WIDTH = 200;
private static final double DEFAULT_POSITION = 100;
#Override
public void start(Stage primaryStage) {
Rectangle rectangle = new Rectangle(SIDE, SIDE);
HBox root = new HBox(rectangle);
root.setPrefWidth(DEFAULT_WIDTH);
rectangle.translateXProperty().bind(root.widthProperty().multiply(DEFAULT_POSITION/DEFAULT_WIDTH).subtract(SIDE/2));
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I would like to create a simple JavaFx class that shows the user a translucent rectangle (say an arbitrary 50% transparency) covering the users screen. It should simply allow me to get the Point of a mouse click event. This sounds trivial, but when I create transparent windows they always seem to be transparent to mouse events rather than just my requirement of semi-transparent visibility. The mouse event is never triggered.
I've used setMouseTransparent(false) on the rectangle and the root pane, but this makes no difference. I'd be really grateful if somebody could indicate any errors/misconceptions.
Here's the trivial test class I have created:
public class ClickScreen implements MouseListener {
private ClickScreenListener listener;
private Stage window;
private Point point;
public ClickScreen(ClickScreenListener listener) {
// Get screen size
Rectangle2D r = Screen.getPrimary().getBounds();
// Something to put stuff in
StackPane root = new StackPane();
// Translucent rectangle on the pane
Rectangle rectangle = new Rectangle(r.getWidth(), r.getHeight());
rectangle.setFill(Color.rgb(183, 183, 183, 0.5));
root.getChildren().add(rectangle);
Scene scene = new Scene(root, r.getWidth(), r.getHeight());
scene.setFill(null);
window = new Stage();
window.initStyle(StageStyle.TRANSPARENT);
window.setTitle("Click drop location");
window.setScene(scene);
this.listener = listener;
}
public Point getLocation(){
return point;
}
#Override
public void mouseClicked(MouseEvent e) {
point = e.getLocationOnScreen();
listener.screenClicked(point);
}
}
Edit:
A simpler example of the transparency issue I am experiencing is from this Hello World! example. When I mouse over the button, it's about 50:50 chance of clicking the button or just clicking "through" and giving focus to the underlying window (which is usually eclipse in my case). Would love you thoughts on this.
public class HelloWorld extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Hello World!");
Button btn = new Button();
btn.setText("Say 'Hello World'");
btn.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
});
StackPane root = new StackPane();
root.getChildren().add(btn);
Scene scene = new Scene(root, 300, 250);
scene.setFill(null);
primaryStage.setScene(scene);
primaryStage.initStyle(StageStyle.TRANSPARENT);
primaryStage.show();
}
}
Check your Imports
You are using some kind of weird setup where you are mixing AWT/Swing classes and JavaFX classes, which really isn't advised (and doesn't work at all in the combination and manner you have used). Just be careful in your JavaFX programs not to import any java.awt.* or javax.swing.* classes unless you really know what you are doing in mixing code for two different toolkits.
Sample Solution
Here is a sample solution which imports only JavaFX classes and utilizes JavaFX events, but otherwise tries to stick to the coding/callback style of the sample code in your question. (The sample could be further simplified through use of Java 8 lambdas).
import javafx.application.Application;
import javafx.event.*;
import javafx.geometry.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.*;
public class ClickListenerSample
extends Application
implements ClickScreenListener {
private Label clickFeedbackLabel = new Label("");
#Override public void start(Stage stage) {
Button listen = new Button("listen");
listen.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
new ClickScreen(ClickListenerSample.this);
}
});
VBox layout = new VBox(10);
layout.getChildren().setAll(
listen,
clickFeedbackLabel
);
layout.setPadding(new Insets(10));
stage.setScene(new Scene(layout, 100, 80));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
#Override public void screenClicked(Point2D point) {
clickFeedbackLabel.setText(point.getX() + ", " + point.getY());
}
}
interface ClickScreenListener {
void screenClicked(Point2D point);
}
class ClickScreen {
private ClickScreenListener listener;
private Stage window;
private Point2D point;
public ClickScreen(ClickScreenListener listener) {
// Get screen size
Rectangle2D r = Screen.getPrimary().getBounds();
// Something to put stuff in
StackPane root = new StackPane();
root.setStyle("-fx-background-color: null;");
// Translucent rectangle on the pane
Rectangle rectangle = new Rectangle(r.getWidth(), r.getHeight());
rectangle.setFill(Color.rgb(183, 183, 183, 0.5));
root.getChildren().add(rectangle);
Scene scene = new Scene(root, r.getWidth(), r.getHeight());
scene.setFill(null);
window = new Stage();
window.initStyle(StageStyle.TRANSPARENT);
window.setTitle("Click drop location");
window.setScene(scene);
scene.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
point = new Point2D(event.getScreenX(), event.getScreenY());
listener.screenClicked(point);
window.hide();
}
});
window.show();
this.listener = listener;
}
public Point2D getLocation(){
return point;
}
}
I'm using this to make a iOS-themed JavaFX2 (Java7) application with a frosted glass effect. The problem is that this code uses its effect on an ImageView. I'd like it to use its effect on whatever's behind the window, like this:
Is there anyway to do that? I'd also like that small drop-shadow effect you see around the above image.
To be clear, I don't want that slider or anything, just the effect of being able to see through the window and having that slight shadow around the edges. I want to use this iOS7-ish effect instead of aero, though.
This might be important: I'm using a modified version of Undecorator.
import javafx.animation.*;
import javafx.application.*;
import javafx.beans.property.*;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Insets;
import javafx.scene.*;
import javafx.scene.control.Label;
import javafx.scene.effect.*;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.image.*;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
public class FrostyTech extends Application {
private static final double BLUR_AMOUNT = 10;
private static final Effect frostEffect =
new BoxBlur(BLUR_AMOUNT, BLUR_AMOUNT, 3);
private static final ImageView background = new ImageView();
private static final StackPane layout = new StackPane();
#Override public void start(Stage stage) {
layout.getChildren().setAll(background, createContent());
layout.setStyle("-fx-background-color: null");
Scene scene = new Scene(
layout,
200, 300,
Color.TRANSPARENT
);
Platform.setImplicitExit(false);
scene.setOnMouseClicked(event -> {
if (event.getClickCount() == 2) Platform.exit();
});
makeSmoke(stage);
stage.initStyle(StageStyle.TRANSPARENT);
stage.setScene(scene);
stage.show();
background.setImage(copyBackground(stage));
background.setEffect(frostEffect);
makeDraggable(stage, layout);
}
// copy a background node to be frozen over.
private Image copyBackground(Stage stage) {
final int X = (int) stage.getX();
final int Y = (int) stage.getY();
final int W = (int) stage.getWidth();
final int H = (int) stage.getHeight();
try {
java.awt.Robot robot = new java.awt.Robot();
java.awt.image.BufferedImage image = robot.createScreenCapture(new java.awt.Rectangle(X, Y, W, H));
return SwingFXUtils.toFXImage(image, null);
} catch (java.awt.AWTException e) {
System.out.println("The robot of doom strikes!");
e.printStackTrace();
return null;
}
}
// create some content to be displayed on top of the frozen glass panel.
private Label createContent() {
Label label = new Label("Create a new question for drop shadow effects.\n\nDrag to move\n\nDouble click to close");
label.setPadding(new Insets(10));
label.setStyle("-fx-font-size: 15px; -fx-text-fill: green;");
label.setMaxWidth(250);
label.setWrapText(true);
return label;
}
// makes a stage draggable using a given node.
public void makeDraggable(final Stage stage, final Node byNode) {
final Delta dragDelta = new Delta();
byNode.setOnMousePressed(mouseEvent -> {
// record a delta distance for the drag and drop operation.
dragDelta.x = stage.getX() - mouseEvent.getScreenX();
dragDelta.y = stage.getY() - mouseEvent.getScreenY();
byNode.setCursor(Cursor.MOVE);
});
final BooleanProperty inDrag = new SimpleBooleanProperty(false);
byNode.setOnMouseReleased(mouseEvent -> {
byNode.setCursor(Cursor.HAND);
if (inDrag.get()) {
stage.hide();
Timeline pause = new Timeline(new KeyFrame(Duration.millis(50), event -> {
background.setImage(copyBackground(stage));
layout.getChildren().set(
0,
background
);
stage.show();
}));
pause.play();
}
inDrag.set(false);
});
byNode.setOnMouseDragged(mouseEvent -> {
stage.setX(mouseEvent.getScreenX() + dragDelta.x);
stage.setY(mouseEvent.getScreenY() + dragDelta.y);
layout.getChildren().set(
0,
makeSmoke(stage)
);
inDrag.set(true);
});
byNode.setOnMouseEntered(mouseEvent -> {
if (!mouseEvent.isPrimaryButtonDown()) {
byNode.setCursor(Cursor.HAND);
}
});
byNode.setOnMouseExited(mouseEvent -> {
if (!mouseEvent.isPrimaryButtonDown()) {
byNode.setCursor(Cursor.DEFAULT);
}
});
}
private javafx.scene.shape.Rectangle makeSmoke(Stage stage) {
return new javafx.scene.shape.Rectangle(
stage.getWidth(),
stage.getHeight(),
Color.WHITESMOKE.deriveColor(
0, 1, 1, 0.08
)
);
}
/** records relative x and y co-ordinates. */
private static class Delta {
double x, y;
}
public static void main(String[] args) {
launch(args);
}
}
Related Questions
Frosted Glass Effect in JavaFX?
How do I create a JavaFX transparent stage with shadows on only the border?
The visual effect that you want for OS dependent window decoration, can only be achieved through the APIs that OS provides. And thus was eliminated by StageStyle.TRANSPARENT below.
For JavaFX content itself, you can control the visuals of the stage > scene > root pane hierarchy. Stage and scene do not (and not aimed to) support advanced stylings so were eliminated by setting as transparent below.
#Override
public void start(Stage primaryStage) {
StackPane root = new StackPane();
root.setStyle("-fx-background-color: null;");
root.setPadding(new Insets(10));
DoubleProperty doubleProperty = new SimpleDoubleProperty(0);
Region region = new Region();
region.styleProperty().bind(Bindings
.concat("-fx-background-radius:20; -fx-background-color: rgba(56, 176, 209, ")
.concat(doubleProperty)
.concat(");"));
region.setEffect(new DropShadow(10, Color.GREY));
Slider slider = new Slider(0, 1, .3);
doubleProperty.bind(slider.valueProperty());
root.getChildren().addAll(region, slider);
primaryStage.initStyle(StageStyle.TRANSPARENT);
Scene scene = new Scene(root, 300, 250);
scene.setFill(Color.TRANSPARENT);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
However the drop shadow effect does not play well with alpha value of the background color. You can observe it by changing the shadow's color to another contrast one.
Output:
To expand on Jewlsea's answer .. And using the above example with JavaFX ONLY ..
While the classes are not public API, it does avoid the AWT stack completely.
Here is a non public example :
// copy a background node to be frozen over.
private Image copyBackground(Stage stage) {
final int X = (int) stage.getX();
final int Y = (int) stage.getY();
final int W = (int) stage.getWidth();
final int H = (int) stage.getHeight();
final Screen screen = Screen.getPrimary();
try {
Robot rbt = com.sun.glass.ui.Application.GetApplication().createRobot();
Pixels p = rbt.getScreenCapture(
(int)screen.getBounds().getMinX(),
(int)screen.getBounds().getMinY(),
(int)screen.getBounds().getWidth(),
(int)screen.getBounds().getHeight(),
true
);
WritableImage dskTop = new WritableImage((int)screen.getBounds().getWidth(), (int)screen.getBounds().getHeight());
dskTop.getPixelWriter().setPixels(
(int)screen.getBounds().getMinX(),
(int)screen.getBounds().getMinY(),
(int)screen.getBounds().getWidth(),
(int)screen.getBounds().getHeight(),
PixelFormat.getByteBgraPreInstance(),
p.asByteBuffer(),
(int)(screen.getBounds().getWidth() * 4)
);
WritableImage image = new WritableImage(W,H);
image.getPixelWriter().setPixels(0, 0, W, H, dskTop.getPixelReader(), X, Y);
return image;
} catch (Exception e) {
System.out.println("The robot of doom strikes!");
e.printStackTrace();
return null;
}
}
Results with a small dropshadow added:
DropShadow shdw = new DropShadow();
shdw.setBlurType(BlurType.GAUSSIAN);
shdw.setColor(Color.GAINSBORO);
shdw.setRadius(10);
shdw.setSpread(0.12);
shdw.setHeight(10);
shdw.setWidth(10);
layout.setEffect(shdw);
The opacity is a property of Node, which is the parent class in JavaFX for things that show up on the screen. http://docs.oracle.com/javafx/2/api/javafx/scene/Node.html#opacityProperty
So you can just set the opacity on the object that you want to have fade away. You then have to add some sort of way to change the opacity on the desired object. Using the slider from your image is one way, but there are others.
Drop shadows can be done using the DropShadow effect... http://docs.oracle.com/javafx/2/api/javafx/scene/effect/DropShadow.html. I have never used it. This is a little high level but if there are follow up questions in the comments I can help answer them.