I am trying to create a Java application that uses JavaFX to allow the user to create shapes by selecting radio button options and then clicking and dragging in a BorderPane area to create their shapes.
I am on the right track so far. The problem I'm having is with getting them positioned correctly. I'm hoping that someone can help me figure out why the shapes aren't being created in the place that I expect them to.
Currently, the first shape I create gets placed in the upper left hand corner of the HBox that I have in the Center section of the BorderPane, regardless of where I click to start creating. Dragging the mouse seems to accurately size the box in accordance with the cursor.
Subsequent attempts to create shapes results in shapes created off-location of the cursor, and dragging will resize, but also not in correlation with the cursor.
Here is my code. I've taken out parts that aren't relevant to the issue at hand to hopefully make it more readable:
public class Main extends Application{
public static String shapeType = "";
public static String color = "";
static Rectangle customRectangle = null;
public void start(Stage mainStage) throws Exception{
Group root = new Group();
Scene scene = new Scene(root, 600, 400);
BorderPane borderPane = new BorderPane();
.....
HBox canvas = new HBox();
Group canvasGroup = new Group();
canvas.getChildren().add(canvasGroup);
canvas.setStyle("-fx-background-color: yellow");
borderPane.setTop(shapeOptions);
borderPane.setLeft(colorOptions);
borderPane.setCenter(canvas);
canvas.addEventFilter(MouseEvent.ANY, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if(event.getEventType() == MouseEvent.MOUSE_PRESSED && shapeType != ""){
switch(shapeType){
case "rectangle":
createCustomRectangle(event, color, canvasGroup);
}
}
if(event.getEventType() == MouseEvent.MOUSE_DRAGGED){
switch (shapeType){
case "rectangle":
editCustomRectangle(event);
}
}
}
});
root.getChildren().add(borderPane);
mainStage.setScene(scene);
mainStage.show();
}
public static void createCustomRectangle(MouseEvent event, String color, Group canvasGroup){
customRectangle = new Rectangle(0, 0, 10,10);
customRectangle.relocate(event.getX(), event.getY());
customRectangle.setFill(Color.RED);
canvasGroup.getChildren().add(customRectangle);
}
public static void editCustomRectangle(MouseEvent event){
customRectangle.setWidth(event.getX() - customRectangle.getTranslateX());
customRectangle.setHeight(event.getY() - customRectangle.getTranslateY());
}
public static void main(String[] args){
launch(args);
}
}
I also wanted to attach a couple of images to make my issue more clear. Here is attempting to create the first shape:
Clicking and dragging to create first shape
And here is trying to create a subsequent shape:
Clicking and dragging to create another shape
Hopefully the description, code, and images are enough to convey what's going on. Any help would be greatly appreciated.
Thanks!
Let's start fixing each problem at a time. First of all a friendly advice, try to name your variables in a way that helps the reader understand their meaning and their identity ( when I first saw the canvas variable I thought it was an actual Canvas ).
Now your layout is something like this :
BorderPane
TOP
- Something
CENTER
- HBox
- Group
- Rectangle
- Rectangle
- ...
Left
- Something
Bottom
- Something
The HBox takes all the available height and calculates it's width depending on its children. So in order to take all the
available space inside the BorderPane you need to actually specify it or bind its preferredWidthProperty with the
widthProperty of the BorderPane.
From the documentation of the Group class you can see that :
Any transform, effect, or state applied to a Group will be applied to
all children of that group. Such transforms and effects will NOT be
included in this Group's layout bounds, however, if transforms and
effects are set directly on children of this Group, those will be
included in this Group's layout bounds.
So when you relocate the Node ( the actual rectangle ) the method relocate() just set the translateX and translateY values and that transformation is applied to the Group's layout bounds as well. To fix that you could change the Group to an AnchorPane.
The way you resize the rectangle is not correct. You need to take the first mouse click coordinates when the first click event takes place and then on a drag event you will take the new coordinates, calculate the delta value of X and Y and just add that value to the width and height for the rectangle finally update the firstX and firstY variable on drag event listener :
deltaX = event.getX() - firstX;
deltaY = event.getY() - firstY;
customRectangle.setWidth(customRectangle.getWidth + deltaX);
customRectangle.setHeight(customRectangle.getHeight + deltaY);
Here is an example of the above :
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class Main extends Application {
public static String shapeType = "";
public static String color = "";
private static Rectangle customRectangle = null;
private double firstX = 0;
private double firstY = 0;
public void start(Stage mainStage) throws Exception {
BorderPane mainPane = new BorderPane();
HBox centerPane = new HBox();
centerPane.prefWidthProperty().bind(mainPane.widthProperty());
centerPane.prefHeightProperty().bind(mainPane.heightProperty());
AnchorPane anchorPane = new AnchorPane();
anchorPane.prefWidthProperty().bind(centerPane.widthProperty());
anchorPane.prefHeightProperty().bind(centerPane.heightProperty());
centerPane.getChildren().add(anchorPane);
centerPane.setStyle("-fx-background-color: yellow");
shapeType = "rectangle";
mainPane.setCenter(centerPane);
centerPane.addEventFilter(MouseEvent.ANY, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.getEventType() == MouseEvent.MOUSE_PRESSED && shapeType != "") {
switch (shapeType) {
case "rectangle":
firstX = event.getX();
firstY = event.getY();
createCustomRectangle(event, color, anchorPane);
}
}
if (event.getEventType() == MouseEvent.MOUSE_DRAGGED) {
switch (shapeType) {
case "rectangle":
editCustomRectangle(event);
firstX = event.getX();
firstY = event.getY();
}
}
}
});
Scene scene = new Scene(mainPane, 600, 400);
mainStage.setScene(scene);
mainStage.show();
}
public void createCustomRectangle(MouseEvent event, String color, AnchorPane canvasGroup) {
customRectangle = new Rectangle(0, 0, 10, 10); // or just set the actual X and Y from the start
customRectangle.relocate(event.getX(), event.getY());
customRectangle.setFill(Color.RED);
canvasGroup.getChildren().add(customRectangle);
}
public void editCustomRectangle(MouseEvent event) {
double deltaX = event.getX() - firstX;
double deltaY = event.getY() - firstY;
double width = customRectangle.getWidth() + deltaX;
double height = customRectangle.getHeight() + deltaY;
customRectangle.setWidth(width);
customRectangle.setHeight(height);
}
public static void main(String[] args) {
launch(args);
}
}
Well, first off, customRectangle.relocate(event.getX(), event.getY()); clearly isn't doing what it's supposed to do. It might be better to create the rectangle at the appropriate spot in the first place.
Instead of:
customRectangle = new Rectangle(0, 0, 10,10);
customRectangle.relocate(event.getX(), event.getY());
try:
customRectangle = new Rectangle(event.getX(), event.getY(), 10,10);
Secondly, it looks like customRectangle.getTranslateX() and customRectangle.getTranslateY() always return 0, so I'd take a look at those methods and see what's going on there as well. Good luck, hope this helped.
Edit:
instead of relocate maybe try using setTranslateX() and setTranslateY().
Thanks everyone for the suggestions! I found what I think is the most straight forward solution to my problem. One large part was that Group was not the kind of node that would allow my intended action. It appended every newly added Node/Object to the right of the previously created one. So, I switched to a StackPane and got better results. The problem there is that it creates everything on the center of the StackPane. My solution to that was to set the alignment for the shape as it's created. Largely, everything else remained the same.
Here is the pertinent code segments for anyone looking to perform a similar operation (my original post has more complete code):
StackPane stackCanvas = new StackPane();
stackCanvas.setStyle("-fx-background-color: yellow");
borderPane.setTop(shapeOptions);
borderPane.setLeft(colorOptions);
borderPane.setCenter(stackCanvas);
public static void createCustomRectangle(MouseEvent event, String color, StackPane stackCanvas){
customRectangle = new Rectangle();
StackPane.setAlignment(customRectangle, Pos.TOP_LEFT);
stackCanvas.getChildren().add(customRectangle);
customRectangle.setTranslateX(event.getX());
customRectangle.setTranslateY(event.getY());
customRectangle.setFill(Color.RED);
}
Related
I have an 4x4 gridpane object consists of Tiles, extends ImageView, and i want to create method for changing places of connected Tiles by mouse drag and drop. I've figured out how to take first element which drag started but I couldn't get the referances of the ImageView which in drag dropped.
Tile Class
public class Tile extends ImageView {
protected int xCoordinate;
protected int yCoordinate;
protected boolean isMoveable;
protected ArrayList<Point2D> points = new ArrayList<>();
public Tile(int xCoordinate, int yCoordinate) {
this.xCoordinate = xCoordinate;
this.yCoordinate = yCoordinate;
super.setFitHeight(125);
super.setFitWidth(125);
}}
GridPane codes
GridPane gridPane = new GridPane();
for (Tile tile : tiles){
gridPane.add(tile,tile.getXCoordinate(),tile.getYCoordinate());
}
StackPane centerPane = new StackPane();
centerPane.setStyle("-fx-background-color: white;");
centerPane.getChildren().add(gridPane);
centerPane.setPadding(new Insets(0,50,0,50));
I have tried this but I don't know how to get referance of connected Tile
gridPane.setOnMouseDragged(e->{
System.out.println(e.getTarget());
gridPane.setOnMouseReleased(e1->{
System.out.println(e1.getTarget());
});
});
I have created the codes for changing places but I should get the referance of the connected Tile when mouse released.
Oracle provides an excellent tutorial on using drag and drop in JavaFX.
You probably want to make use of the Dragboard, which is a special kind of Clipboard.
Points to note
You may not actually need to move the tiles that you have created. They are ImageViews.
You can place the image associated with the source in the dragboard and change the image in the target view when it is dropped.
You can set the dragView on the dragboard for visual feedback of the drag operation.
You can use a custom content type for the dragboard rather than an image, this is explained in the linked Oracle tutorial.
private static final DataFormat customFormat =
new DataFormat("helloworld.custom");
When putting a custom data onto a dragboard, specify the data type. Note that the data must be serializable.
When reading the data from the dragboard, a proper casting is needed.
Potential Approaches
There are two ways you can handle the tile display.
Tile view and model.
Create a separate tile view and tile model interface.
When the tiles change, don't change the view, only change the model instance backing the view. The view observes its model for changes and automatically updates itself. New nodes are not created and existing nodes are not moved.
That is the approach in the example below. The view is an ImageView and the model is an Image.
Encapsulate view and model together.
In this case, you place the information about the model in the view.
When a node is placed in the grid, you record it's grid position, for example by member values in the node or by setting user data on the node.
When a node is dragged to a new position, you query the source node for its position, then you swap the source and target nodes in the grid using:
GridPane.setConstraints(node, columnIndex, rowIndex)
This is essentially the approach you propose in your question.
I do not provide an implementation for this second potential approach.
Example
The images are images of the grid before and after manually dragging the tiles to reorder them.
The example is not going to be exactly what you want, it is purely provided as an example and you will need to adapt it if you wish to make use of some of the concepts in it.
The ImageViewFactory is just to create test images, you can ignore that portion.
Dragging specific stuff is in the DragUtil class.
Code uses Java 18 and newer Java language features, so to compile it you will need to enable the appropriate language level.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.Label;
import javafx.scene.effect.*;
import javafx.scene.image.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class TileDragApp extends Application {
private static final String TEXT = "abcdefghijklmnop";
private static final int GRID_SIZE = 4;
private static final int TILE_SIZE = 60;
#Override
public void start(Stage stage) {
ImageViewFactory imageViewFactory = new ImageViewFactory();
ImageView[] imageViews = imageViewFactory.makeImageViews(
TEXT,
TILE_SIZE
);
DragUtil dragUtil = new DragUtil();
GridPane grid = new GridPane();
for (int i = 0; i < TEXT.length(); i++) {
dragUtil.makeDraggable(imageViews[i]);
grid.add(imageViews[i], i % GRID_SIZE, i / GRID_SIZE);
}
grid.setGridLinesVisible(true);
grid.setPadding(new Insets(20));
stage.setScene(new Scene(grid));
stage.setResizable(false);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
class ImageViewFactory {
private static final String CSS =
"""
data:text/css,
""" +
// language=CSS
"""
.root {
-fx-background-color: azure;
}
.label {
-fx-font-size: 40px;
-fx-text-fill: navy;
}
""";
public ImageView[] makeImageViews(String text, int tileSize) {
List<Character> chars =
text.chars()
.mapToObj(
c -> (char) c
).collect(
Collectors.toList()
);
Collections.shuffle(chars);
return chars.stream()
.map(
c -> makeImageView(c, tileSize)
).toArray(
ImageView[]::new
);
}
private ImageView makeImageView(char c, int tileSize) {
Label label = new Label(Character.toString(c));
StackPane layout = new StackPane(label);
layout.setPrefSize(tileSize, tileSize);
Scene scene = new Scene(layout);
scene.getStylesheets().add(CSS);
SnapshotParameters snapshotParameters = new SnapshotParameters();
snapshotParameters.setFill(Color.AZURE);
Image image = layout.snapshot(snapshotParameters,null);
return new ImageView(image);
}
}
class DragUtil {
public void makeDraggable(ImageView imageView) {
Effect highlight = createHighlightEffect(imageView);
imageView.setOnDragDetected(e -> {
Dragboard db = imageView.startDragAndDrop(TransferMode.MOVE);
ClipboardContent content = new ClipboardContent();
content.putImage(imageView.getImage());
db.setContent(content);
db.setDragView(makeSmaller(imageView.getImage()));
e.consume();
});
imageView.setOnDragOver(e -> {
if (e.getGestureSource() != imageView
&& e.getDragboard().hasImage()
) {
e.acceptTransferModes(TransferMode.MOVE);
}
imageView.setEffect(highlight);
e.consume();
});
imageView.setOnDragExited(e -> {
imageView.setEffect(null);
e.consume();
});
imageView.setOnDragDropped(e -> {
Dragboard db = e.getDragboard();
boolean success = false;
if (db.hasImage() && e.getGestureSource() instanceof ImageView source) {
source.setImage(imageView.getImage());
imageView.setImage(db.getImage());
success = true;
}
e.setDropCompleted(success);
e.consume();
});
}
private Image makeSmaller(Image image) {
ImageView resizeView = new ImageView(image);
resizeView.setFitHeight(image.getHeight() * 3 / 4);
resizeView.setFitWidth(image.getWidth() * 3 / 4);
SnapshotParameters snapshotParameters = new SnapshotParameters();
return resizeView.snapshot(snapshotParameters, null);
}
private Effect createHighlightEffect(Node n) {
ColorAdjust monochrome = new ColorAdjust();
monochrome.setSaturation(-1.0);
return new Blend(
BlendMode.MULTIPLY,
monochrome,
new ColorInput(
0,
0,
n.getLayoutBounds().getWidth(),
n.getLayoutBounds().getHeight(),
Color.PALEGREEN
)
);
}
}
This seems like a simple question but I haven't yet been able to find a relevant answer.
I'm creating a simple Javafx application and creating nodes on click that can be dragged around.
I want to make sure a specific type of Node (Circle) is always drawn on top of the AnchorPane.
I think this is all the relevant code, but I can include the rest if needed.
How can I achieve this behavior?
#Override
public void start(Stage stage) throws Exception {
root = new AnchorPane();
scene = new Scene(root, 800, 800);
scene.setFill(Color.WHITE);
scene.setOnMouseDragged(mouseHandler);
scene.setOnMouseReleased(mouseHandler);
scene.setOnMousePressed(mouseHandler);
stage.setScene(scene);
stage.setTitle("Shapes");
stage.show();
}
Event Handler
EventHandler<MouseEvent> mouseHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
switch (eventName) {
case ("MOUSE_PRESSED"):
getCurrentShape(clickPoint);
if (!shapeSelected) {
if (clickType.equals("SECONDARY")) { // Draw a Square
ShapeComponent newShape = ShapeGenerator.generateRandomSquare(clickPoint);
components.add(newShape);
root.getChildren().add(newShape.getShape());
} else if (clickType.equals("PRIMARY")) { // Draw a Circle
ShapeComponent newShape = ShapeGenerator.generateCircle(clickPoint);
components.add(newShape);
/* MOVE NODE TO FRONT */
root.getChildren().add(newShape.getShape());
}
}
break;
Going to answer my own question here.
I thought of this a different way. Instead of always moving the circle to the front when it's drawn, I can always move the squares to the back when they are drawn.
This will add the new shape (square) to the AnchorPaneand then we can get the index of the most recent item (the square just drawn) and move that to the back.
root.getChildren().add(newShape.getShape());
root.getChildren().get(root.getChildren().size() - 1).toBack(); // Move Square to back of AnchorPane
Or, as mentioned in the comments.
root.getChildren().add(0, newShape.getShape());
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'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.
RESUME
Good day StackOverflow community.
I've been trying for some time to develop a program that enables users to put objects in an area, allowing this area to be moved by the mouse. For this type of program, I decided to use a ScrollPane, because the user can add various contents in the area which I call the canvas. For some reason, something strange is happening in my program.
EXPLANATION OF PROGRAM
What I basically did was create a group of objects, and define this group as the ScrollPane content. Within the group, there is a Rectangle object that was added to serve as canvas boundaries. This object has larger dimensions (such as 1500 x 1000, for example), and is used in calculations that prevent nodes from moving beyond its limits. This is just the logical behind the existing large rectangle in my program, but in reality, there is no Node object with the mouse movement. What exists is the random distribution of Shape objects by the rectangle area.
For ScrollPane has its scrollbars moved, I use the setHvalue setVvalue methods. Unfortunately for my purposes, this method does not change the position of the ScrollPane's viewport with pixel values, but values that are in a range between 0f and 1f. So I can move the viewport with the mouse, I used a equation known as Rule of 3 (here in my Country, as I know), which we equate values and cross multiply.
For example, say I want to move the viewport of the ScrollPane with the mouse horizontally, and that my canvas area has a width of 2000 pixels. Finding how far (in pixels) the mouse was dragged from one point to another, I need to know how this value represents in a range 0f to 1f. Suppose I have dragged the mouse in 3 pixels, I could find the representation of the 0f to 1f with the following comparison:
2000 px ---- 1f
3 px ---- xf
Multiplying crossed, I'll get the following result:
xf = 3 / 2000
xf = 0.0015
Note: I believe you all know that. I'm not teaching math to anyone,
just want to explain the logic of my problem.
SOURCE CODE
Here is my program class:
import testes.util.TestesUtil;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;
public class ScrollTest4 extends Application
{
// #########################################################################################################
// MAIN
// #########################################################################################################
public static void main(String[] args)
{
Application.launch(args);
}
// #########################################################################################################
// INSTÂNCIAS
// #########################################################################################################
// OUTSIDE
private BorderPane root;
private Button but_moreH;
private Button but_lessH;
private Button but_moreV;
private Button but_lessV;
// LOG
private VBox vbox_south;
private Label lab_hValue;
private Label lab_vValue;
private Label lab_viewport;
// INSIDE
private Rectangle rec_canvas;
private ScrollPane scroll;
private Group grp_objects;
// MOUSE
private double mouse_x = 0;
private double mouse_y = 0;
// MISC
private AnimationTimer timer;
// EDITED - 08/02/2014
private boolean moreH = false;
private boolean moreV = false; // Purposely unused.
private boolean lessH = false;
private boolean lessV = false; // Purposely unused.
// #########################################################################################################
// INÍCIO FX
// #########################################################################################################
#Override public void start(Stage estagio) throws Exception
{
this.iniFX();
this.confFX();
this.adFX();
this.evFX();
Scene cenario = new Scene(this.root , 640 , 480);
estagio.setScene(cenario);
estagio.setTitle("Programa JavaFX");
estagio.show();
}
protected void iniFX()
{
// OUTSIDE
this.root = new BorderPane();
this.but_moreH = new Button();
this.but_lessH = new Button();
this.but_moreV = new Button();
this.but_lessV = new Button();
// LOG
this.vbox_south = new VBox();
this.lab_hValue = new Label();
this.lab_vValue = new Label();
this.lab_viewport = new Label();
// INSIDE
this.scroll = new ScrollPane();
this.grp_objects = new Group();
this.rec_canvas = new Rectangle();
// MISC
this.timer = new AnimationTimer()
{
#Override public void handle(long now)
{
// EDITED - 08/02/2014
if(but_moreH.isArmed() || moreH)
{
// scroll.hvalueProperty().set(scroll.hvalueProperty().get() + 0.003f);
scroll.setHvalue(scroll.getHvalue() + 0.003f);
}
// EDITED - 08/02/2014
if(but_lessH.isArmed() || lessH)
{
// scroll.hvalueProperty().set(scroll.hvalueProperty().get() - 0.003f);
scroll.setHvalue(scroll.getHvalue() - 0.003f);
}
if(but_moreV.isArmed())
{
scroll.setVvalue(scroll.getVvalue() + 0.003f);
}
if(but_lessV.isArmed())
{
scroll.setVvalue(scroll.getVvalue() - 0.003f);
}
}
};
this.timer.start();
}
protected void confFX()
{
// OUTSIDE
this.but_moreH.setText("More H");
this.but_moreH.setMaxHeight(Double.MAX_VALUE);
this.but_lessH.setText("Less H");
this.but_lessH.setMaxHeight(Double.MAX_VALUE);
this.but_moreV.setText("More V");
this.but_moreV.setMaxWidth(Double.MAX_VALUE);
this.but_lessV.setText("Less V");
this.but_lessV.setMaxWidth(Double.MAX_VALUE);
// LOG
this.updateHvalue();
this.updateVvalue();
this.updateViewport();
// INSIDE
this.rec_canvas.setWidth(1200);
this.rec_canvas.setHeight(1000);
this.rec_canvas.setFill(Color.INDIANRED);
this.rec_canvas.setStroke(Color.RED);
this.rec_canvas.setStrokeType(StrokeType.INSIDE);
this.rec_canvas.setStrokeWidth(1);
}
protected void adFX()
{
// LOG
this.vbox_south.getChildren().add(this.but_moreV);
this.vbox_south.getChildren().addAll(this.lab_hValue , this.lab_vValue , this.lab_viewport);
// OUTSIDE
this.root.setCenter(this.scroll);
this.root.setTop(this.but_lessV);
this.root.setBottom(this.vbox_south);
this.root.setRight(this.but_moreH);
this.root.setLeft(this.but_lessH);
// INSIDE
this.grp_objects.getChildren().add(this.rec_canvas);
this.scroll.setContent(this.grp_objects);
// MISC
StrokeType[] strokes = {StrokeType.CENTERED , StrokeType.INSIDE , StrokeType.OUTSIDE};
for(int cont = 0 ; cont < 20 ; cont++)
{
Rectangle node = new Rectangle(Math.random() * 100 + 50 , Math.random() * 100 + 50);
node.setFill(TestesUtil.getCorAleatoria(false));
node.setStroke(TestesUtil.getCorAleatoria(false));
node.setStrokeType(strokes[(int) (Math.random() * 2)]);
node.setStrokeWidth(Math.random() * 9 + 1);
node.setRotate(Math.random() * 360);
node.setMouseTransparent(true);
// EDITED - 08/02/2014
TestsUtil.putRandomlyIn(
node ,
rec_canvas.getBoundsInParent().getMinY() ,
rec_canvas.getBoundsInParent().getMinY() + rec_canvas.getBoundsInParent().getHeight() ,
rec_canvas.getBoundsInParent().getMinX() + rec_canvas.getBoundsInParent().getWidth() ,
rec_canvas.getBoundsInParent().getMinX() );
this.grp_objects.getChildren().add(node);
}
}
protected void evFX()
{
// ##########################
// SCROLL PROPERTIES
// ##########################
this.scroll.hvalueProperty().addListener(new ChangeListener<Number>()
{
#Override public void changed(ObservableValue<? extends Number> observable,Number oldValue, Number newValue)
{
updateHvalue();
updateViewport();
}
});
this.scroll.vvalueProperty().addListener(new ChangeListener<Number>()
{
#Override public void changed(ObservableValue<? extends Number> observable,Number oldValue, Number newValue)
{
updateVvalue();
updateViewport();
}
});
this.scroll.setOnKeyPressed(new EventHandler<KeyEvent>()
{
#Override public void handle(KeyEvent e)
{
if(e.getCode() == KeyCode.RIGHT)
{
moreH = true;
}
else if(e.getCode() == KeyCode.LEFT)
{
lessH = true;
}
}
});
this.scroll.setOnKeyReleased(new EventHandler<KeyEvent>()
{
#Override public void handle(KeyEvent e)
{
if(e.getCode() == KeyCode.RIGHT)
{
moreH = false;
}
else if(e.getCode() == KeyCode.LEFT)
{
lessH = false;
}
}
});
// ##########################
// CANVAS
// ##########################
this.rec_canvas.setOnMousePressed(new EventHandler<MouseEvent>()
{
#Override public void handle(MouseEvent e)
{
// The XY distance from the upper left corner of the canvas.
mouse_x = e.getX();
mouse_y = e.getY();
}
});
this.rec_canvas.setOnMouseDragged(new EventHandler<MouseEvent>()
{
#Override public void handle(MouseEvent e)
{
// ##########################
// PIXELS
// ##########################
// The distance between mouse movements (drag events).
double xPixelsMoved = e.getX() - mouse_x;
// double yPixelsMoved = e.getY() - mouse_y;
// ##########################
// TO 1F
// ##########################
double h_of_1f = xPixelsMoved / rec_canvas.getBoundsInParent().getWidth();
double h_of_1f_inverted = h_of_1f * -1;
double currentH = scroll.getHvalue();
scroll.setHvalue(currentH + h_of_1f);
// scroll.hvalueProperty().set(scroll.getHvalue() + h_de_x);
// scroll.vvalueProperty().set(scroll.getVvalue() + v_de_y);
// ##########################
// DEBUG
// ##########################
System.out.printf("xPixelsMoved: %f , h_of_1f: %f , h_of_1f_inverted: %f %n",
xPixelsMoved , h_of_1f , h_of_1f_inverted);
// ##########################
// UPDATE FROM
// EVENT TO EVENT
// ##########################
// Writes last mouse position to update on new motion event.
mouse_x = e.getX();
mouse_y = e.getY();
}
});
}
// #########################################################################################################
// MISC.
// #########################################################################################################
protected void updateViewport()
{
Bounds vport = this.scroll.getViewportBounds();
this.lab_viewport.setText(String.format("Viewport - [X: %f , Y: %f , W: %f , H: %f]",
vport.getMinX() , vport.getMinY() , vport.getWidth() , vport.getHeight() ));
}
protected void updateHvalue()
{
this.lab_hValue.setText("H value: " + this.scroll.getHvalue() );
}
protected void updateVvalue()
{
this.lab_vValue.setText("V value: " + this.scroll.getVvalue() );
}
}
THE PROBLEM
Clicking the mouse button on the canvas area and drag it, you can see that the program moves the ScrollPane viewport horizontally. The program seems to work perfectly (or not). However, something goes wrong at the time when the mouse is dragged sometimes abruptly (...or not!). At certain times the ScrollPane Viewport is not visually updated. This is a strange behavior, because even if viewport is not visually updated, the scrollbars are still updated.
I put other ways to move the ScrollPane viewport horizontally using the same method, and for some reason, only the approach using the mouse makes it happen. I thought this could be solved by making a request for layout using requestLayout, also causing a request to a pulse, but it does not work.
THE TEST OUTPUT
The odd thing is that everything returns to normal when the window of my application is resized. Here's a video that shows what happens to my program:
VIDEO & MIRROR 1
I no longer know what else to do. Can anyone help me with this please?
EDIT (08/02/2014 10:08 AM GMT - 3:00)
The original source code of my application is found written in Portuguese, so you may be seeing something unknown. Basically TestesUtil is a utility class with static methods that define shortcuts to other client classes. I changed the call from my source code shown here previously and am now putting some methods of my class TestesUtil, translated into English as TestsUtil:
public static void putRandomlyIn(Node node , double northPoint , double southPoint , double eastPoint , double westPoint)
{
node.setLayoutX(Math.random() * pontoLeste);
node.setLayoutY(Math.random() * pontoSul);
fixEasternBoundary(node , eastPoint);
fixNorthernBoundary(node , northPoint);
fixWesternBoundary(node , westPoint);
fixSouthernBoundary(node , southPoint);
}
There is no mystery here. This method simply calculates a value from an interval, and defines the LayoutXY properties for the Node argument. Methods "fix ..." just check the boundsInParent bounds of the node compared to the point in the argument, and then adjust the layoutXYproperties from the Node object. Even if I remove the random distribution of objects, the problem still happens. So I'm sure this problem is not being caused by this.
The source code of the original post was changed with the addition of the ability to move the scroll bars with the arrow keys. Even if it is already an existing function of ScrollPane, adding that could reproduce the error seen with the mouse (now with arrows). Some things were also translated into English for better understanding by the community.
Please, I ask for help. I'm getting dizzy not knowing what to do. This type of situation could be happening because of some bug in JavaFX? Ahhrr... Please somebody help me in this. :'(
Thank you for your attention anyway.
EDIT (09/02/2014 10:50 AM GMT - 3:00)
Forgot to mention... My program was initially written and tested using JDK 8 b123. Currently I installed the JDK 8 b128 version and am still getting the same problem. My operating system is Windows 7 64x.
I am almost certain that this is a bug. Are you guys getting the same result as me? Or am I the only one to find this kind of problem? If this is a bug, which procedure should be taken?
Thank you for your attention.
EDIT (10/02/2014 09:45 AM GMT - 3:00)
A bounty was started.
UPDATE
This bug has now been fixed for JavaFX 8u20.
Bug description
This is a bug, that can be easily verified by executing the following code with JavaFx JRE 8:
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
final ScrollPane sp = new ScrollPane();
final Image[] images = new Image[5];
final ImageView[] pics = new ImageView[5];
final VBox vb = new VBox();
final Label fileName = new Label();
final String [] imageNames = new String [] {"fw1.jpg", "fw2.jpg",
"fw3.jpg", "fw4.jpg", "fw5.jpg"};
#Override
public void start(Stage stage) {
VBox box = new VBox();
Scene scene = new Scene(box, 180, 180);
stage.setScene(scene);
stage.setTitle("Scroll Pane");
box.getChildren().addAll(sp, fileName);
VBox.setVgrow(sp, Priority.ALWAYS);
fileName.setLayoutX(30);
fileName.setLayoutY(160);
for (int i = 0; i < 5; i++) {
images[i] = new Image(getClass().getResourceAsStream(imageNames[i]));
pics[i] = new ImageView(images[i]);
pics[i].setFitWidth(100);
pics[i].setPreserveRatio(true);
vb.getChildren().add(pics[i]);
}
sp.setVmax(440);
sp.setPrefSize(115, 150);
sp.setContent(vb);
sp.vvalueProperty().addListener(new ChangeListener<Number>() {
public void changed(ObservableValue<? extends Number> ov,
Number old_val, Number new_val) {
fileName.setText(imageNames[(new_val.intValue() - 1)/100]);
}
});
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
This code comes directly from the JavaFX ScrollPane Tutorial.
If one randomly moves the vertical scroll bar with the mouse very rapidly, then at some time the screen will freeze and no longer get updated. Although one is still able to move the scroll bar around, the displayed images will stay fixed. Only if one resizes the frame, the display of the images will be updated and the ScrollPane reverts to its previous state. Note, that this bug will only happen in JRE 8, it is not reproducible in JRE 7.
The only workaround for the problem, that I could find, is adding
sp.snapshot(new SnapshotParameters(), new WritableImage(1, 1));
to the listener:
sp.vvalueProperty().addListener(new ChangeListener<Number>() {
public void changed(ObservableValue<? extends Number> ov,
Number old_val, Number new_val) {
fileName.setText(imageNames[(new_val.intValue() - 1)/100]);
sp.snapshot(new SnapshotParameters(), new WritableImage(1, 1));
}
});
Calling snapshot on the ScrollPane seems to force the update every time the vvalueProperty changes. This seems to be a known workaround for several update problems with JavaFX - see here.