Canvas: hover over text output - javafx

I want to hover over a shape I created in Canvas (JavaFx) and when hovering over with the mouse I want a text output pop-up to display. Is there a built in function for this? I can't find how to do it anywhere...

Unlike with the scene graph, a Canvas has no notion of what it contains. It's nothing but a two-dimensional array of pixels and provides no further distinctions than that. If you want to know if and when the mouse hovers over a "shape" in the Canvas you'll have to keep track of where the "shape" is and do the necessary computations manually. Here's an example which shows a popup at the mouse's location only while within the drawn rectangle:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Label;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Popup;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage primaryStage) {
// used to test if mouse is within the rectangle
var bounds = new Rectangle2D(200, 100, 100, 100);
var canvas = new Canvas(500, 300);
// draw rectangle using above bounds
canvas.getGraphicsContext2D().setFill(Color.FIREBRICK);
canvas
.getGraphicsContext2D()
.fillRect(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight());
var popup = createPopup();
canvas.setOnMouseMoved(
e -> {
// test if local mouse coordinates are within rectangle
if (bounds.contains(e.getX(), e.getY())) {
// convert local coordinates to screen coordinates
var point = canvas.localToScreen(e.getX(), e.getY());
// show the popup at the mouse's location on the screen
popup.show(canvas, point.getX(), point.getY());
} else if (popup.isShowing()) {
// hide popup if showing and mouse no longer within rectangle
popup.hide();
}
});
primaryStage.setScene(new Scene(new Pane(canvas)));
primaryStage.show();
}
private Popup createPopup() {
var content = new StackPane(new Label("Hello, World!"));
content.setPadding(new Insets(10, 5, 10, 5));
content.setBackground(
new Background(new BackgroundFill(Color.WHITE, new CornerRadii(10), null)));
content.setEffect(new DropShadow());
var popup = new Popup();
popup.getContent().add(content);
return popup;
}
}
As you can see, this is relatively simple for a static image consisting of a single, rectangular shape. This can quickly become more complicated just by making the image dynamic, let alone by having to test the bounds of irregular shapes.
An easier approach would be to use the scene graph. Instead of drawing to a Canvas you would add a Rectangle to a layout. Then you can use the Node API to know when the mouse enters and exits the Rectangle (e.g. setOnMouseXXX, hover property, etc.). It also makes it easier to use something like a Tooltip, which can simply be "installed" on the Node.

Related

Why strange painting behavior in JavaFX

I have a simple FX example with a simple component.
package fxtest;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage stage) {
var bp = new BorderPane();
var r = new Rectangle(0, 0, 200, 200);
r.setFill(Color.GREEN);
var sp = new StackPane(r);
bp.setCenter(sp);
bp.setTop(new XPane());
bp.setBottom(new XPane());
bp.setLeft(new XPane());
bp.setRight(new XPane());
var scene = new Scene(bp, 640, 480);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
package fxtest;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
public class XPane extends Region {
public XPane() {
setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
setMinSize(100, 100);
setPrefSize(100, 100);
widthProperty().addListener((o) -> {
populate();
});
heightProperty().addListener((o) -> {
populate();
});
populate();
}
private void populate() {
ObservableList<Node> children = getChildren();
Rectangle r = new Rectangle(getWidth(), getHeight());
r.setFill(Color.WHITE);
r.setStroke(Color.BLACK);
children.add(r);
Line line = new Line(0, 0, getWidth(), getHeight());
children.add(line);
line = new Line(0, getHeight(), getWidth(), 0);
children.add(line);
}
}
When run, it does what I expect:
When I grow the window, the X's grow.
But when I shrink the window, I get artifacts of the side panels.
I would have thought erasing the backgrounds would have fixed this, but I guess there's some ordering issue. But even still, when you drag the corner, all of the XPanes change size, and they all get repainted, but the artifacts remain.
I tried wrapping the XPanes in to a StackPane, but that didn't do anything (I didn't think it would, but tried it anyway).
How do I remedy this? This is JavaFX 13 on JDK 16 on macOS Big Sur.
Why you get artifacts
I think a different approach should be used rather than fixing the approach you have, but you could fix it if you want.
You are adding new rectangles and lines to your XPane in listeners. Every time the height or width changes, you add a new set of nodes, but the old set of nodes at the old height and widths remains. Eventually, if you resize enough, performance will drop or you will run out of memory or resources, making the program unusable.
A BorderPane paints its children (the center and the XPanes) in the order they were added without clipping, so these old lines will remain and the renderer will paint them over some panes as you resize. Similarly, some panes will paint over some lines because you are building up potentially lots of filled rectangles in the panes and they are partially overlapping lots of lines created.
To fix this, clear() the child node list in your populate() method before you add any new nodes.
private void populate() {
ObservableList<Node> children = getChildren();
children.clear();
// now you can add new nodes...
}
Alternate Solution
Change listeners on widths and heights aren't really the place to add content to a custom region, IMO.
I think that it is best to take advantage of the scene graph and let it handle the repainting and updating of existing nodes after you change the attributes of those nodes, instead of creating new nodes all the time.
Here is an example that subclasses Region and paints fine when a resize occurs.
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
public class XPane extends Region {
public XPane() {
super();
Rectangle border = new Rectangle();
Line topLeftToBottomRight = new Line();
Line bottomLeftToTopRight = new Line();
getChildren().addAll(
border,
topLeftToBottomRight,
bottomLeftToTopRight
);
border.setStroke(Color.BLACK);
border.setFill(Color.WHITE);
border.widthProperty().bind(
widthProperty()
);
border.heightProperty().bind(
heightProperty()
);
topLeftToBottomRight.endXProperty().bind(
widthProperty()
);
topLeftToBottomRight.endYProperty().bind(
heightProperty()
);
bottomLeftToTopRight.startYProperty().bind(
heightProperty()
);
bottomLeftToTopRight.endXProperty().bind(
widthProperty()
);
setMinSize(100, 100);
setPrefSize(100, 100);
}
}
On Region vs Pane
I'm not sure if you should be subclassing Pane or Region, the main difference between the two is that a Pane has a public accessor for a modifiable child list, but a Region does not. So it would depend on what you are trying to do. If it is just drawing X's like the example, then Region is appropriate.
On layoutChildren() vs binding
The Region documentation states:
By default a Region inherits the layout behavior of its superclass,
Parent, which means that it will resize any resizable child nodes to
their preferred size, but will not reposition them. If an application
needs more specific layout behavior, then it should use one of the
Region subclasses: StackPane, HBox, VBox, TilePane, FlowPane,
BorderPane, GridPane, or AnchorPane.
To implement a more custom layout, a Region subclass must override
computePrefWidth, computePrefHeight, and layoutChildren. Note that
layoutChildren is called automatically by the scene graph while
executing a top-down layout pass and it should not be invoked directly
by the region subclass.
Region subclasses which layout their children will position nodes by
setting layoutX/layoutY and do not alter translateX/translateY, which
are reserved for adjustments and animation.
I am not actually doing that here, instead, I am binding in the constructor rather than overriding layoutChildren(). You could implement an alternate solution that operates as the documentation discusses, overriding layoutChildren() rather than using binding, but it is more complicated and less well documented on how to do that.
It is uncommon to subclass Region and override layoutChildren(). Instead, usually, a combination of standard layout Panes will be used and constraints set on the panes and nodes to get the desired layout. This lets the layout engine do a lot of the work such as snapping to pixels, calculating margins and insets, respecting constraints, repositioning content, etc, a lot of which would need to be done manually for a layoutChildren() implementation.
One common approach is to bind the relevant geometric properties to the desired properties of the enclosing container. A related example is examined here, and others are collected here.
The variation below binds the vertices of several Shape instances to the Pane width and height properties. Resize the enclosing stage to see how the BorderPane children conform to entries in the BorderPane Resize Table. The example also adds a red Circle, which stays centered in each child, growing and shrinking in the center to fill the smaller of the width or height. The approach relies on the fluent arithmetic API available to properties that implement NumberExpression or methods defined in Bindings.
c.centerXProperty().bind(widthProperty().divide(2));
c.centerYProperty().bind(heightProperty().divide(2));
NumberBinding diameter = Bindings.min(widthProperty(), heightProperty());
c.radiusProperty().bind(diameter.divide(2));
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
/**
* #see https://stackoverflow.com/q/70311488/230513
*/
public class App extends Application {
#Override
public void start(Stage stage) {
var bp = new BorderPane(new XPane(), new XPane(),
new XPane(), new XPane(), new XPane());
stage.setScene(new Scene(bp, 640, 480));
stage.show();
}
private static class XPane extends Pane {
private final Rectangle r = new Rectangle();
private final Circle c = new Circle(8, Color.RED);
private final Line line1 = new Line();
private final Line line2 = new Line();
public XPane() {
setPrefSize(100, 100);
r.widthProperty().bind(this.widthProperty());
r.heightProperty().bind(this.heightProperty());
r.setFill(Color.WHITE);
r.setStroke(Color.BLACK);
getChildren().add(r);
line1.endXProperty().bind(widthProperty());
line1.endYProperty().bind(heightProperty());
getChildren().add(line1);
line2.startXProperty().bind(widthProperty());
line2.endYProperty().bind(heightProperty());
getChildren().add(line2);
c.centerXProperty().bind(widthProperty().divide(2));
c.centerYProperty().bind(heightProperty().divide(2));
NumberBinding diameter = Bindings.min(widthProperty(), heightProperty());
c.radiusProperty().bind(diameter.divide(2));
getChildren().add(c);
}
}
public static void main(String[] args) {
launch();
}
}

Pane cutting off when using light effects and perspective camera

I have encountered yet another problem with javaFX light effects. When using perspective camera to follow player in 2D world, everything works fine, until I add light effects!
The CYAN color represents the background.. When light effects are ON, the level pane does not render at the edges of the scene and the background can be seen instead. This makes the bottom and right side of my levels invisible and therefore unplayable. In the full screen mode it is even worse!
here is my demonstration code:
import javafx.application.Application;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.effect.Light;
import javafx.scene.effect.Lighting;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class dd extends Application {
PerspectiveCamera cam = new PerspectiveCamera(false);
public void start(Stage alku) throws Exception {
Pane testpane = new Pane();
Rectangle background = new Rectangle(500, 500);
background.setFill(Color.CYAN);
Rectangle rec = new Rectangle(500, 300);
Lighting lig = new Lighting();
Light.Point l = new Light.Point();
l.setX(200);
l.setY(150);
l.setZ(20);
l.setColor(Color.WHITE);
lig.setLight(l);
rec.setFill(Color.RED);
testpane.getChildren().addAll(background, rec);
Scene scene = new Scene(testpane, 300, 300);
cam.setTranslateX(30);
cam.setTranslateY(30);
alku.setTitle("TEST");
alku.setScene(scene);
rec.setEffect(lig);
scene.setCamera(cam);
alku.show();
}
public static void main(String[] args) {
launch(args);
}
}
If you remove the camera or the light, the "rec" renders normally again. Just adjust the window size to see the problem better.
So the main question I have:
Is this a bug? If not, how can this rendering issue be solved without sacrificing lights and camera?

why is my circle filled Black even though i haven't set it to be filled

I am currently working on an assignment where i must print a circle in the center of the primary stage and 4 buttons on the bottom center area of the primary stage which move the circle, up, down, left, and right when clicked. when i run my code, my circle is filled in with the color black. I have set the stroke of the circle to be black but i have not set the circle to be filled black. I know i can just set my circle to be filled white and somewhat solve the problem, but i am wondering if anyone knows why this is happening. Also, i cannot get the Circle and the buttons to print into the same window. I can get either the circle to print by setting the primaryStage to the scene or print the buttons by setting the scene to hBox and then setting the primaryStage to the scene. How should i best change my code so that the buttons and the circle are both displayed?
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.geometry.Pos;
import javafx.event.EventHandler;
import javafx.event.ActionEvent;
public class Btest extends Application {
#Override // Override the start method in the Application class
public void start(Stage primaryStage) {
// Create a border pane
BorderPane pane = new BorderPane();
// create Hbox, set to bottom center
HBox hBox = new HBox();
hBox.setSpacing(10);
hBox.setAlignment(Pos.BOTTOM_CENTER);
Button btLeft = new Button("Left");
Button btDown = new Button("Down");
Button btUp = new Button("Up");
Button btRight = new Button("Right");
hBox.getChildren().addAll(btLeft, btDown, btUp, btRight);
// Lambda's
btLeft.setOnAction((e) -> {
System.out.println("Process Left");
});
btDown.setOnAction((e) -> {
System.out.println("Process Down");
});
btUp.setOnAction(e -> {
System.out.println("Process Up");
});
btRight.setOnAction((e) -> {
System.out.println("Process Right");
});
pane.setCenter(new CenteredCircle("Center"));
// Create a scene and place it in the stage
Scene scene = new Scene(pane, 300, 300);
//set stage and display
primaryStage.setTitle("ShowBorderPane"); // Set the stage title
primaryStage.setScene(scene); // Place the scene in the stage
primaryStage.show(); // Display the stage
}
public static void main(String[] args) {
Application.launch(args);
}
}
// create custom class for circle
class CenteredCircle extends StackPane {
public CenteredCircle(String title) {
setPadding(new Insets(11.5, 12.5, 13.5, 14.5));
Circle circle = new Circle();
circle.setStroke(Color.BLACK);
circle.setCenterX(50);
circle.setCenterY(50);
circle.setRadius(50);
getChildren().add(circle);
}
}
"Why is my circle filled Black even though i haven't set it to be filled?"
Because the default color is black. See the doc of Shape.setFill() method:
Defines parameters to fill the interior of an Shape using the settings
of the Paint context. The default value is Color.BLACK for all shapes
except Line, Polyline, and Path. The default value is null for those
shapes.
"... Also, i cannot get the Circle and the buttons to print into the same window."
Put the Hbox to the parent BorderPane, for instance into the bottom:
pane.setBottom( hBox );

Using javafx using circle.setCenterX and circle.setCenterY not working

I am working through some coursework and am running into an odd issue. I'm working with javafx learning how to build shapes and work with alignment. Anyway my circle object will not respond to setCenterX or setCenterY commands (the radius definition statement does work) in the original definition statements nor in the commands issued by my event handlers which should be redefining these set x and set y values. I cannot figure out why. Please see my code below. When working correctly my code would allow me to move the circle object around the screen with the buttons and event handlers I've created. If I can figure out why the setCenterX and setCenterY don't work, I'm sure I can get the rest. Thanks for your help in advance.
package bravo15;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.scene.Scene;
public class FifteenDotThreeVersionThree extends Application {
#Override // Override the start method in the Application class
public void start(Stage primaryStage) {
Circle circle = new Circle();
circle.setCenterX(300);
circle.setCenterY(300);
circle.setRadius(50);
// Hold four buttons in an HBox
// Define hbox
HBox hBox = new HBox();
hBox.setSpacing(10);
hBox.setAlignment(Pos.CENTER);
// define buttons
Button btLeft = new Button("Left");
Button btRight = new Button("Right");
Button btUp = new Button("Up");
Button btDown = new Button("Down");
// add defined buttons into the hbox
hBox.getChildren().add(btLeft);
hBox.getChildren().add(btRight);
hBox.getChildren().add(btUp);
hBox.getChildren().add(btDown);
// Create and register the handlers for the four buttons
btLeft.setOnAction(e -> circle.setCenterX(circle.getCenterX() - 10));
btRight.setOnAction(e -> circle.setCenterX(circle.getCenterX() + 10));
btUp.setOnAction(e -> circle.setCenterY(circle.getCenterY() + 10));
btDown.setOnAction(e -> circle.setCenterY(circle.getCenterY() - 10));
BorderPane borderPane = new BorderPane();
borderPane.setTop(circle);
borderPane.setBottom(hBox);
BorderPane.setAlignment(hBox, Pos.CENTER);
// Create a scene and place it in the stage
Scene scene = new Scene(borderPane, 200, 200);
primaryStage.setTitle("ControlCircle Version 3"); // Set the stage title
primaryStage.setScene(scene); // Place the scene in the stage
primaryStage.show(); // Display the stage
}
/**
* The main method is only needed for the IDE with limited
* JavaFX support. Not needed for running from the command line.
*/
public static void main(String[] args) {
launch(args);
}
}
A BorderPane manages the layout of its components, so it positions the circle for you by setting its layoutX and layoutY properties so that it appears at the top left.
Wrap it in a Pane, which performs no layout, and place the Pane in the top of the border pane:
borderPane.setTop(new Pane(circle));
Note that you have things set up so that it is initially off-screen. You probably want to increase the size of the scene:
Scene scene = new Scene(borderPane, 600, 600);
You can do it this way to shift the circle:
btLeft.setOnAction(e -> circle.setTranslateX(circle.getTranslateX() - 10));
btRight.setOnAction(e -> circle.setTranslateX(circle.getTranslateX() + 10));
btUp.setOnAction(e -> circle.setTranslateY(circle.getTranslateY() - 10));
btDown.setOnAction(e -> circle.setTranslateY(circle.getTranslateY() + 10));
setTranslateX():
Defines the x coordinate of the translation that is added to this
Node's transform. The node's final translation will be computed as
layoutX + translateX, where layoutX establishes the node's stable
position and translateX optionally makes dynamic adjustments to that
position. This variable can be used to alter the location of a node
without disturbing its layoutBounds, which makes it useful for
animating a node's location.
And it looks better with borderPane.setCenter(circle); than borderPane.setTop(circle);.
I have also removed the following lines:
circle.setCenterX(300);
circle.setCenterY(300);

onMouseEntered doesn't work properly JavaFX

I am drawing a Circle and a Text on a Pane and what I want is to have a text appear next to the cursor that says "Mouse is over the circle" when it is over the circle and "Mouse is outside the circle" when its outside. What happens instead is the text always says "Mouse is outside the circle" except in some locations over the circle (and even then it tends to flash back to the wrong one). I also tried setting the text directly from the mouseEntered and mouseExited events and its even worse. What am I doing wrong? Better yet, is there another way of determining whether the cursor is over a certain node? Also if you could explain to me why I get a "variable used in lambda expression should be effectively final" when I move the definition of s inside the start method, it would be great :)
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class Ex1512 extends Application {
String s="";
#Override
public void start(Stage primaryStage) throws Exception {
Text text = new Text();
Pane pane = new Pane();
Circle circle = new Circle(100, 100, 50);
circle.setFill(Color.WHITE);
circle.setStroke(Color.BLACK);
circle.setOnMouseEntered(e -> s = "Mouse is over the circle");
circle.setOnMouseExited(e -> s = "Mouse is outside the circle");
pane.setOnMouseMoved(e -> {
text.setText(s);
text.setX(e.getX());
text.setY(e.getY());
});
pane.getChildren().addAll(circle,text);
Scene scene = new Scene(pane,300,300);
primaryStage.setScene(scene);
primaryStage.show();
}
}
You are putting the text over the circle. So the text receives the event, then the circle again. Just relocate the text away from the mouse cursor.
This solves your problem:
text.setX(e.getX()+20);
Or you could use setMouseTransparent on the text.

Resources