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());
Related
I have a code written using javafx and java 11. As I showed in the below code snippet, when I move my ImageView to right of the scene, The ImageView disappears.
At first:
and then after moving it using arrow keys with the help of scene event listener:
then:
and finally:
I don't use fxml. Here is my demo which has the problem too.
public class HelloApplication extends Application {
public String fetchResource(String path) {
return Objects.requireNonNull(getClass().getResource(path)).toString();
}
#Override
public void start(Stage stage) throws IOException {
Rectangle r = new Rectangle(100, 100);
ImageView spaceShip = new ImageView(fetchResource("spaceShip.png"));
spaceShip.setFitHeight(100);
spaceShip.setFitWidth(100);
spaceShip.setX(0);
spaceShip.setY(0);
EventHandler<KeyEvent> keyListener = event -> {
if (event.getCode() == KeyCode.RIGHT) {
spaceShip.setX(spaceShip.getX() + 20);
}
};
Group game = new Group(spaceShip);
Scene scene = new Scene(game, 1840, 1080);
scene.setOnKeyPressed(keyListener);
stage.setTitle("Hello!");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
I should mention that I create a rectangle and check the scenario with that, And there was not any problem. I think there is some problem with ImageView.
I couldn't reproduce the issue with your code, but I could with SceneBuilder. You may get the desired result if you wrap the Group in a Pane (at least it works in SceneBuilder).
Group game = new Group(spaceShip);
Pane pane = new Pane(game);
Scene scene = new Scene(pane, 1840, 1080);
stage.setScene(scene);
stage.show();
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);
}
I wonder if there is a possibility to perform a perspective rotation on a square in JavaFX. When I normally rotate it on the x-axis I just get a compressed square, however I wanted to simulate a real-life rotation with perspective. The result should look something like this
What it should look like
Of course depending on the degree. Is there any way to achieve this, best case where it is possible to specify a certain degree.
Here is what the code for the compression looks like:
public class HelloWorld extends Application {
#Override
public void start(Stage primaryStage) {
StackPane root = new StackPane();
Rectangle rectangle = new Rectangle();
rectangle.setWidth(500);
rectangle.setHeight(500);
rectangle.setRotationAxis(Rotate.X_AXIS);
rectangle.setRotate(50);
Scene scene = new Scene(root, 1000, 1000);
root.getChildren().addAll(rectangle);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
By default, a Scene uses a ParallelCamera, which does not define any perspective. Hence the coordinate mapping for x and y coordinates are effectively independent of the z coordinate.
In order to see a three-dimensional effect in the scene, specify a PerspectiveCamera:
public class HelloWorld extends Application {
#Override
public void start(Stage primaryStage) {
StackPane root = new StackPane();
Rectangle rectangle = new Rectangle();
rectangle.setWidth(500);
rectangle.setHeight(500);
rectangle.setRotationAxis(Rotate.X_AXIS);
rectangle.setRotate(50);
Scene scene = new Scene(root, 1000, 1000);
scene.setCamera(new PerspectiveCamera());
root.getChildren().addAll(rectangle);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
(As you can see, your angle is computed in the opposite direction to the angle you want.)
You can further configure properties of the PerspectiveCamera, such as the field of view, if needed. Refer to the documentation.
JavaFX supports real 3D so there is no need to mess around with this PerspectiveTransform which is just an Effect. You just have to set up a proper 3D Scene with a PerspectiveCamera and you can get exactly what you want.
Is there a way to add listener whenever I resize my splitpane?
I currently have
split.getDividers().get(0).positionProperty().addListener(new ChangeListener<Number>(){
public void changed(ObservableValue<? extends Number> observableValue, Number oldWindowWidth, Number newWindowWidth){
//code
}
});
which detects whenever the window or divider size is changed, but I only need to know when the divider position is changed, like when I drag it with the mouse. Is there a way to do that? Any help will be appreciated!
There is no clean way to do this but it is possible for example by using CSS lookup on the SplitPane.
As the dividers have the CSS class of split-pane-divider you can get the dividers from the scene-graph, and they are actually StackPane instances.
On these StackPane you can register a mouse pressed and a mouse release event listener, and update a class member that indicates that the divider is "in a drag" at the moment. And then in the position property listener you can check this class member: if it is true that means that the divider is being moved by the mouse, otherwise the change can be ignored.
Example:
public class Main extends Application {
// Indicates that the divider is currently dragged by the mouse
private boolean mouseDragOnDivider = false;
#Override
public void start(Stage primaryStage) throws Exception{
SplitPane sp = new SplitPane();
sp.getItems().addAll(new StackPane(), new StackPane());
sp.setDividerPositions(0.3f);
// Listen to the position property
sp.getDividers().get(0).positionProperty().addListener((obs, oldVal, newVal) -> {
if(mouseDragOnDivider)
System.out.println("It's a mouse drag to pos: " + newVal.doubleValue());
});
primaryStage.setScene(new Scene(sp, 300, 275));
sp.requestLayout();
sp.applyCss();
// For each divider register a mouse pressed and a released listener
for(Node node: sp.lookupAll(".split-pane-divider")) {
node.setOnMousePressed(evMousePressed -> mouseDragOnDivider = true);
node.setOnMouseReleased(evMouseReleased -> mouseDragOnDivider = false );
}
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Note:
As the lookup only works if the layout is created and the CSS is applied, hence it is important that the requestLayout() and the applyCss() methods are already executed and also that the SplitPane is already added to the scene-graph (attached to a Scene).
I have a TextField, and I would like to do something if the user clicks anywhere that is not the TextField itself.
Apparently, the onMouseClicked event won't trigger if you don't click the node itself, so that wouldn't work.
Listening the focusedProperty may have been a good idea, but the problem is that almost the entirety of my application is not focus traversable, so in many cases clicking outside the textfield won't unfocus it, so the listener won't be notified.
The only thing left in my mind is to put an event filter on the scene itself, intercept mouse clicks, get the click coordinates and determine if they fall within the bounds of the textfield. I find this a little bit overkill and I may be missing something more obvious.
Is there another way to determine if the user clicked anywhere outside my TextField node?
The only thing left in my mind is to put an event filter on the scene itself, intercept mouse clicks, get the click coordinates and determine if they fall within the bounds of the textfield. I find this a little bit overkill and I may be missing something more obvious.
In fact I consider this your best option, but with a variation:
Use the PickResult provided by the MouseEvent to get the target Node and check, if it's in the hierarchy of the Node.
(This can be necessary, if the Node has children; e.g. a TextField also contains a Text element.)
In fact this seems to be the only option that cannot be broken by consuming the event somewhere in the scene graph.
Example
This moves the focus to the root pane in case the user clicks somewhere except the TextField.
public static boolean inHierarchy(Node node, Node potentialHierarchyElement) {
if (potentialHierarchyElement == null) {
return true;
}
while (node != null) {
if (node == potentialHierarchyElement) {
return true;
}
node = node.getParent();
}
return false;
}
#Override
public void start(Stage primaryStage) {
TextField textField = new TextField();
textField.setMinSize(400, Region.USE_PREF_SIZE);
textField.setMaxWidth(400);
textField.setEditable(false);
textField.textProperty().bind(Bindings.when(textField.focusedProperty()).then("Got the Focus!").otherwise("Please give me the focus!"));
StackPane root = new StackPane();
root.getChildren().add(textField);
Scene scene = new Scene(root, 500, 200);
scene.addEventFilter(MouseEvent.MOUSE_CLICKED, evt -> {
if (!inHierarchy(evt.getPickResult().getIntersectedNode(), textField)) {
root.requestFocus();
}
});
primaryStage.setScene(scene);
primaryStage.show();
}
Assume you top level container of your app is Pane. You can apply mouse click event to that pane and inside it handle the mouse click event for the textfield. So this way, if the user clicks on the textfield or clicks somewhere else both events will be notified. Here's is the sample code.
Pane.setOnMouseClicked((MouseEvent evt) -> {
System.out.println("Click outside textfield");
textField.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
System.out.println("textfield clicked");
}
});
});
If you have any other controls in that Pane. This mouse click wont interfere with them, it will be only called when you click directly on the pane.