javafx mouse events in transparent image - javafx

JavaFx ImageView doesn't trigger Mouse Events such as press or drag if you click or drag on a transparent pixel, is there anyway to work around this and detect mouse events from transparent areas ?
I have this image
that i added into this very simple JavaFX scene
using an ImageView named view and i want to move it with Mouse Drag events
so i wrote this code
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application{
double initMx, initMy,initX, initY;
#Override
public void start(Stage ps) throws Exception {
StackPane pane = new StackPane();
Image im = new Image("0.png");
ImageView view = new ImageView(im);
double fact = im.getWidth() / im.getHeight();
view.setFitHeight(300);
view.setFitWidth(300 * fact);
view.setOnMousePressed(e->{
initX = view.getTranslateX();
initY = view.getTranslateY();
initMx = e.getSceneX();
initMy = e.getSceneY();
});
view.setOnMouseDragged(e->{
double dx = initMx - e.getSceneX();
double dy = initMy - e.getSceneY();
double nx = initX - dx;
double ny = initY - dy;
view.setTranslateX(nx);
view.setTranslateY(ny);
});
pane.getChildren().add(view);
Scene scene = new Scene(pane, 500, 500);
ps.setScene(scene);
ps.show();
}
public static void main(String[] args) {
launch(args);
}
}
this code works fine so far,
but if you press or drag somewhere like under his ears ( or anywhere transparent ) nothing will happen ! how to fix this !

The more natural and easiest solution would have been to just set pick on bounds to true.
view.setPickOnBounds(true);

You can do so by setting this image as a graphic in a Button like so
button.setGraphics(new ImageView(im));
Note: You will need to remove style from the button after adding the ImageView by setting the button background with a transparent background color

Try this, if you didn't yet:
view.setOnMouseDragged(e->{
double dx = initMx - e.getX();
double dy = initMy - e.getY();

Related

Javafx event handler overlap

I have a pane, inside the pane I have a circle and a label. Everytime mouse enter or exit the circle, the label will change to "enter" or "exit". And the label will move accordingly to the mouse position (mouse scenceX and sceneY).
The problem is when I enter the mouse inside the circle, the label will change to "enter" but will immediately change to "exit" then.
I think there is an overlap eventhandler somewhere in my code, but I don't know why. This is my code:
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception{
Pane pane = new Pane();
pane.setStyle("-fx-background-color: grey;");
pane.setPrefSize(300,300);
Circle circle = new Circle();
circle.setFill(Color.TRANSPARENT);
int radius = 50;
circle.setRadius(radius);
circle.setStroke(Color.BLACK);
circle.setStrokeWidth(2);
circle.setLayoutX(100);
circle.setLayoutY(60);
pane.getChildren().add(circle);
Label label = new Label("Mouse point is outside the circle");
pane.getChildren().add(label);
circle.addEventHandler(MouseEvent.MOUSE_EXITED, e -> {
label.setText("Mouse point is outside the circle");
System.out.println("exit");
});
circle.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> {
label.setText("Mouse point is inside the circle");
System.out.println("enter");
});
pane.addEventFilter(MouseEvent.MOUSE_MOVED, e -> {
label.setLayoutX(e.getSceneX());
label.setLayoutY(e.getSceneY());
});
primaryStage.setTitle("Hello World");
primaryStage.setScene(new Scene(pane));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I'm only guessing but I believe the problem is that when the event filter for pane is executed, the result is that the mouse pointer moves from being inside circle to inside label. Then when you move the mouse, it leaves label and enters circle which causes the MOUSE_ENTERED event handler to fire. After that handler fires, the event filter fires which moves the mouse pointer out of circle and into label which causes MOUSE_EXITED event handler to fire. That's why you see enter and exit repeatedly when you move the mouse pointer around inside circle. You don't see enter nor exit at all when you move the mouse pointer around pane when it is outside of circle.
In order to fix it, I removed MOUSE_ENTERED and MOUSE_EXITED and inside the event filter, I check whether the mouse pointer is inside the bounds of circle and set label text accordingly.
Here is the code.
(Note that I replaced addEventFilter with setOnMouseMoved.)
import javafx.application.Application;
import javafx.geometry.Bounds;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Pane pane = new Pane();
pane.setStyle("-fx-background-color: grey;");
pane.setPrefSize(300, 300);
Label label = new Label("Mouse point is outside the circle");
Circle circle = new Circle();
circle.setFill(Color.TRANSPARENT);
int radius = 50;
circle.setRadius(radius);
circle.setStroke(Color.BLACK);
circle.setStrokeWidth(2);
circle.setLayoutX(100);
circle.setLayoutY(60);
Bounds boundingBox = circle.getBoundsInParent();
double maxX = boundingBox.getMaxX();
double minX = boundingBox.getMinX();
double maxY = boundingBox.getMaxY();
double minY = boundingBox.getMinY();
pane.getChildren().add(circle);
pane.getChildren().add(label);
pane.setOnMouseMoved(e -> {
double x = e.getSceneX();
double y = e.getSceneY();
if (minX < x && x < maxX && minY < y && y < maxY) {
label.setText("Mouse point is inside the circle");
}
else {
label.setText("Mouse point is outside the circle");
}
label.setLayoutX(e.getSceneX());
label.setLayoutY(e.getSceneY());
});
primaryStage.setTitle("Hello World");
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
As explained in another answer, when the label moves, it moves so that the mouse is now inside the label; this means it is no longer hovering directly over the circle, so an "exited" event is fired on the circle.
The simplest fix is just to make the label ignore the mouse, which can be done using the mouseTransparent property:
Label label = new Label("Mouse point is outside the circle");
label.setMouseTransparent(true);
pane.getChildren().add(label);

Java FX nested event handler I want to block the parent handler when child handler is called

I have a fxml Mouse event handler method where every time I click a Pane a pop up dialog box appears on the screen asking for information. When OK is clicked on the dialog, a circle with text is added to a stack pane and the stack pane is then added to the pane where the user clicked.
However I am trying to implement a event handler where I can move the stack pane around by dragging the stack pane with the mouse. My event handler works but every time I finish dragging the stack pane the pop up dialog pops up. I do not want the pop up dialog to pop up when moving the circle how would I change my code to do this is there a way of blocking the pop up handler?
Thank you.
My Handler method:
#FXML
private void handleAddVertex(MouseEvent event) {
boolean okClicked = main.showAddVertexPopUp(this);
if(okClicked) {
String vertexText = "";
if(getSelectedDataChoice().equals("Integer")) {
vertexText = dataModel.getListOfIntVertices().get(dataModel.getListOfIntVertices().size() - 1).toString();
}else if(getSelectedDataChoice().equals("Double")){
vertexText = dataModel.getListOfDoubleVertices().get(dataModel.getListOfDoubleVertices().size() - 1).toString();
}else {
vertexText = dataModel.getListOfStringVertices().get(dataModel.getListOfStringVertices().size() - 1).toString();
}
EventHandler<MouseEvent> circleOnMousePressedEventHandler =
new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
orgSceneX = t.getSceneX();
orgSceneY = t.getSceneY();
orgTranslateX = ((StackPane)(t.getSource())).getTranslateX();
orgTranslateY = ((StackPane)(t.getSource())).getTranslateY();
}
};
EventHandler<MouseEvent> circleOnMouseDraggedEventHandler =
new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
double offsetX = t.getSceneX() - orgSceneX;
double offsetY = t.getSceneY() - orgSceneY;
double newTranslateX = orgTranslateX + offsetX;
double newTranslateY = orgTranslateY + offsetY;
((StackPane)(t.getSource())).setTranslateX(newTranslateX);
((StackPane)(t.getSource())).setTranslateY(newTranslateY);
}
};
double x = event.getX();
double y = event.getY();
Circle vertex = new Circle(x, y, 20, Color.WHITE);
vertex.setStroke(Color.BLACK);
Text text = new Text (vertexText);
StackPane stack = new StackPane();
stack.getChildren().addAll(vertex, text);
stack.setLayoutX(x);
stack.setLayoutY(y);
stack.setOnMousePressed(circleOnMousePressedEventHandler);
stack.setOnMouseDragged(circleOnMouseDraggedEventHandler);
centerPane.getChildren().add(stack);
}
}
To stop an Event from propagating you use Event.consume().
Marks this Event as consumed. This stops its further propagation.
From your description, it appears handleAddVertex is a MOUSE_CLICKED handler. You'll have to add another EventHandler to the newly created StackPane that consumes MOUSE_CLICKED events.
stack.setOnMouseClicked(Event::consume);
This will stop the MOUSE_CLICKED event from bubbling up to the Node which has the handleAddVertex handler.
For more information on event processing in JavaFX, see JavaFX: Handling Events.
Here's a small example where you can see the difference between consuming and not consuming the event:
import java.util.function.Predicate;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class Main extends Application {
private CheckBox consumeClickEventsCheckBox;
#Override
public void start(Stage primaryStage) throws Exception {
consumeClickEventsCheckBox = new CheckBox("Consume click events");
consumeClickEventsCheckBox.setSelected(true);
HBox top = new HBox(consumeClickEventsCheckBox);
top.setPadding(new Insets(10));
top.setAlignment(Pos.CENTER);
Pane center = new Pane();
center.setBorder(new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, null, null)));
center.setOnMouseClicked(this::handleAddCircle);
Rectangle clip = new Rectangle();
clip.widthProperty().bind(center.widthProperty());
clip.heightProperty().bind(center.heightProperty());
center.setClip(clip);
BorderPane root = new BorderPane(center);
root.setTop(top);
Scene scene = new Scene(root, 600, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
private void handleAddCircle(MouseEvent event) {
event.consume();
Alert confirm = new Alert(AlertType.CONFIRMATION);
confirm.initOwner(((Node) event.getSource()).getScene().getWindow());
confirm.setHeaderText(null);
confirm.setContentText("Do you want to add a circle here?");
if (confirm.showAndWait().filter(Predicate.isEqual(ButtonType.OK)).isPresent()) {
Circle circle = new Circle(event.getX(), event.getY(), 25);
circle.setOnMousePressed(this::handleCirclePressed);
circle.setOnMouseDragged(this::handleCircleDragged);
circle.setOnMouseClicked(this::handleCircleClicked);
((Pane) event.getSource()).getChildren().add(circle);
}
}
private Point2D origin;
private void handleCirclePressed(MouseEvent event) {
event.consume();
origin = new Point2D(event.getX(), event.getY());
}
private void handleCircleDragged(MouseEvent event) {
event.consume();
Circle circle = (Circle) event.getSource();
circle.setTranslateX(circle.getTranslateX() + event.getX() - origin.getX());
circle.setTranslateY(circle.getTranslateY() + event.getY() - origin.getY());
}
/*
* Will consume the MOUSE_CLICKED event only if the CheckBox is selected. You can test
* the behavior of consuming the event by toggling the CheckBox.
*/
private void handleCircleClicked(MouseEvent event) {
if (consumeClickEventsCheckBox.isSelected()) {
event.consume();
}
}
}

Create a draggable selection box for a sketching program in JavaFX

I'm trying to create a draggable selection box for a sketching program in JavaFX, one like this:
I'm only not sure how to do it. I initially wanted to do it like this: capture the mouse coordinates when the mouse is pressed and do it again at the end of a drag, then calculate the height and width and make a transparent button with a black border with these properties.
But, then I realized that when I do it like this, it is not possible to see the button while you are scaling the plane, unless you draw and delete a lot of buttons.
So, I wondered if there is a better way to do something like this or is my reasoning above right? Thanks
I would use a Rectangle instead of a Button. Just do what you describe, but update the size (and position) of the rectangle on mouse drag, instead of only adding it when the mouse is released.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class SelectionRectangle extends Application {
private double mouseDownX ;
private double mouseDownY ;
#Override
public void start(Stage primaryStage) {
Rectangle selectionRectangle = new Rectangle();
selectionRectangle.setStroke(Color.BLACK);
selectionRectangle.setFill(Color.TRANSPARENT);
selectionRectangle.getStrokeDashArray().addAll(5.0, 5.0);
Pane pane = new Pane();
pane.setMinSize(600, 600);
pane.getChildren().add(selectionRectangle);
pane.setOnMousePressed(e -> {
mouseDownX = e.getX();
mouseDownY = e.getY();
selectionRectangle.setX(mouseDownX);
selectionRectangle.setY(mouseDownY);
selectionRectangle.setWidth(0);
selectionRectangle.setHeight(0);
});
pane.setOnMouseDragged(e -> {
selectionRectangle.setX(Math.min(e.getX(), mouseDownX));
selectionRectangle.setWidth(Math.abs(e.getX() - mouseDownX));
selectionRectangle.setY(Math.min(e.getY(), mouseDownY));
selectionRectangle.setHeight(Math.abs(e.getY() - mouseDownY));
});
primaryStage.setScene(new Scene(pane));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
You can use a mouse released handler to figure out what's selected, by looking at the x, y, width, and height properties of the rectangle, as needed.

Moving a Rectangle to the position where a mouse click happen

I am trying to create a project where the user clicks on the screen and then a rectangle will move to the position where the clicked occurred. My intention was to get the center of the rectangle to move the exact location of the click but my code only moves the rectangle into the general area of where the click occurred. My question is how do I get the center of the rectangle to move the exact location of where a mouse click occurs?
public void start(Stage primaryStage) {
BorderPane root = new BorderPane();
Scene scene = new Scene(root,400,400);
Rectangle rec = new Rectangle(50,50,50,50);
rec.setLayoutX(200);
rec.setLayoutY(200);
TranslateTransition transition = new TranslateTransition(Duration.seconds(0.50), rec);
transition.setOnFinished(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent t) {
rec.setLayoutX(rec.getTranslateX() + rec.getLayoutX());
rec.setLayoutY(rec.getTranslateY() + rec.getLayoutY());
rec.setTranslateX(0);
rec.setTranslateY(0);
}
});
scene.setOnMousePressed(e->{
transition.setToX(e.getSceneX() - rec.getLayoutX());
transition.setToY(e.getSceneY() - rec.getLayoutY());
transition.playFromStart();
});
root.getChildren().add(rec);
primaryStage.setScene(scene);
primaryStage.show();
}
You ignore the x/y properties of the Rectangle which also shift the position where the Rectangle is drawn. Furthermore for the center to be moved to this position, you need to also subtract half the width/height from the position to move to...
Also I recommend using a Pane instead of BorderPane, is you want to set the layoutX/layoutY properties yourself. Nonetheless in this case it should work too with some small adjustments:
scene.setOnMousePressed(e -> {
transition.setToX(e.getSceneX() - rec.getLayoutX() - rec.getWidth() / 2 - rec.getX());
transition.setToY(e.getSceneY() - rec.getLayoutY() - rec.getHeight() / 2 - rec.getY());
transition.playFromStart();
});
Here a complete sample using a Pane as suggested by fabian.
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Translator extends Application {
private static final double W = 400, H = 400;
private static final double S = 50;
public void start(Stage stage) {
Rectangle rec = new Rectangle(W / 2 - S / 2,H / 2 - S / 2,S,S);
TranslateTransition transition = new TranslateTransition(Duration.millis(500), rec);
transition.setOnFinished(t -> {
rec.setX(rec.getTranslateX() + rec.getX());
rec.setY(rec.getTranslateY() + rec.getY());
rec.setTranslateX(0);
rec.setTranslateY(0);
});
Pane root = new Pane(rec);
Scene scene = new Scene(root,W,H);
root.setOnMousePressed(e -> {
transition.stop();
transition.setToX(e.getX() - S / 2 - rec.getX());
transition.setToY(e.getY() - S / 2 - rec.getY());
transition.playFromStart();
});
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}

Getting a MouseEvent to target ImageViews inside a TilePane

I have many ImageViews inside a TilePane, that is inside a StackPane and then a ScrollPane. There is no border, padding, or margin between the children of the TilePane so there is no chance that I'm not clicking on an ImageView. When I click on an image, I want the target of the MouseEvent to be the ImageViews, but instead it is the TilePane.
How can I get the event chain to end on an ImageView instead of ending early on the TilePane?
Otherwise, is there a way I can get the ImageView using other information? Perhaps using the coordinates of the event?
The usual way I do this is just to register the mouse listener with the node in which I am interested; in your case this means register a mouse listener with each ImageView. It's easy then to have each mouse listener have a reference to the particular image view with which it's registered, or to other data (e.g. a filename) if you need.
One thing that might be happening: if your images have transparent pixels, then mouse clicks on that part of the image will by default "drop through" to the node below. You can change this behavior by calling imageView.setPickOnBounds(true); on the image views.
Some test code. If you run this you'll see some numbered images with different colored backgrounds. About 1 in 4 have transparent backgrounds (they appear white). If you click on these (but not on the actual text of the number), you'll see the mouse handlers registered with the scroll pane and stack pane have the tile pane as the target, and the handler registered with the ImageView is not even invoked. For those without the transparent background, the target is always the ImageView. If you select the check box, so pickOnBounds is true for all the ImageViews, both transparent and opaque images behave as you want.
import java.util.Random;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.TilePane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class ImageViewClickTest extends Application {
private static final Random RNG = new Random();
#Override
public void start(Stage primaryStage) {
TilePane tilePane = new TilePane();
CheckBox pickOnBounds = new CheckBox("Pick on bounds");
pickOnBounds.setPadding(new Insets(16));
for (int i=1; i<=200; i++) {
ImageView imageView = createImageView(i);
imageView.pickOnBoundsProperty().bind(pickOnBounds.selectedProperty());
// mouse handler directly on image view:
// can access image-view specific data...
String message = "Clicked on Image "+i ;
imageView.setOnMouseClicked(e ->
System.out.println("From handler on ImageView: "+message));
tilePane.getChildren().add(imageView);
}
StackPane stack = new StackPane(tilePane);
stack.setOnMouseClicked(e -> {
// source will be the stack pane
// target will be the top-most node
// (i.e. the ImageView, in most cases)
System.out.println("From handler on stack pane: Source: "+e.getSource());
System.out.println("From handler on stack pane: Target: "+e.getTarget());
});
ScrollPane scroller = new ScrollPane(stack);
scroller.setFitToWidth(true);
scroller.setOnMouseClicked(e -> {
// source will be the scroll pane
// target will be the top-most node
// (i.e. the ImageView, in most cases)
System.out.println("From handler on scroller: Source: "+e.getSource());
System.out.println("From handler on scroller: Target: "+e.getTarget());
});
BorderPane root = new BorderPane(scroller, pickOnBounds, null, null, null);
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
private ImageView createImageView(int index) {
Label label = new Label(Integer.toString(index));
label.setAlignment(Pos.CENTER);
label.setMinSize(48, 48);
label.setStyle(randomStyle());
Image image = new Scene(label, Color.TRANSPARENT).snapshot(null);
ImageView imageView = new ImageView(image);
return imageView ;
}
private String randomStyle() {
StringBuilder style = new StringBuilder();
style.append("-fx-background-color: -fx-background;");
style.append("-fx-background: ");
if (RNG.nextDouble() < 0.25) {
style.append( "transparent;");
style.append(" -fx-text-fill: black;") ;
} else {
String bg = String.format("#%02x%02x%02x;",
RNG.nextInt(256), RNG.nextInt(256), RNG.nextInt(256));
style.append(bg);
}
return style.toString();
}
public static void main(String[] args) {
launch(args);
}
}

Resources