JavaFX How Can I Center a Tooltip on a Node? - java

I'm currently working on a form that features validation upon changing focus from one node to another and wish to display a tooltip centered above a node containing an error if one exists. I'm using the tooltip's show(Node ownerNode, double anchorX, double anchorY) method signature to specify which node I would like to attach it to and where to position it.
I've tried the following code:
Tooltip tooltip = new Tooltip("Error");
tooltip.show(node, 0, 0);
double tooltipMiddle = tooltip.getWidth() / 2;
tooltip.hide();
double nodeMiddle = node.getWidth() / 2;
//Gets the node's x and y position within the window
Bounds bounds = node.localToScene(node.getBoundsInLocal());
//Tooltip's bottom-right corner is set to the anchor points, so I set x to
//the node's x coordinate plus half the width of the node plus half the width
//of the tooltip
tooltip.show(node,
bounds.getMinX() + nodeMiddle + tooltipMiddle, bounds.getMinY());
This has gotten me very close to the center, but it's still off. I've been all over the internet trying to find help, but I'm just not finding any, so I figured I'd ask here.
Any chance I could get some insight into why I'm not able to get this working how I'd like it?

Code which I bring provides correct positioning of tooltip but is far from being perfect. It would take a lot of work to bring comprehensive solution (if you want we can discuss it).
Going to the bottom I think you have a math problem and Tooltip class may not be the best option.
import javafx.application.Application;
import javafx.geometry.Bounds;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class TooltipApp extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
TextField textField = new TextField();
textField.setPrefWidth(100.);
Button button = new Button("Tooltip");
HBox hBox = new HBox(textField, button);
hBox.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
Label label = new Label("Empty!");
label.setVisible(false);
Pane tooltipPane = new Pane(label);
tooltipPane.setMouseTransparent(true);
StackPane stackPane = new StackPane(hBox, tooltipPane);
stackPane.setPrefSize(600., 400.);
Scene scene = new Scene(stackPane);
stage.setScene(scene);
stage.show();
button.setOnMouseClicked(event -> {
if (label.isVisible()) {
label.setVisible(false);
} else {
Bounds sceneBounds = textField.localToScene(textField.getBoundsInLocal());
label.setLayoutX((sceneBounds.getMinX() + (sceneBounds.getWidth() / 2)) - (label.getWidth() / 2));
label.setLayoutY(sceneBounds.getMinY() - label.getHeight());
label.setVisible(true);
}
});
}
}

First you should use Bounds bounds = node.localToScreen(node.getBoundsInLocal()); as it might not be clear what the relevant Scene is.
Then calling tooltip.show(node, bounds.getMinX(), bounds.getMinY()); should give you a tooltip with an upper left corner identical to the upper upper left corner of node.
Now, tooltip.show(node, bounds.getMinX() + nodeMiddle - tooltipMiddle, bounds.getMinY() - tooltipHeight); should produce the desired result and it does for
double tooltipMiddle = (tooltip.getWidth()-18) / 2;
double tooltipHeight = tooltip.getHeight()-18;
double nodeMiddle = getWidth() / 2;
The 18 came from a little experimenting and I'm not sure why the width and height are off by that amount, but it seems to be indpendent of the length or number of lines of the text in the tooltip.

Related

JavaFX LineChart with Values

I want to show values on top of the linechart. I've seen this answer which is quite helpful but it changes the linechart nodes. What I want is same idea but showing values not on nodes, but near them (maybe right and above of them) something like:
53
O
/ \
/ \
/42 \ 21 21
O O------------O
EDIT:
I've tried to code below but sadly only lines appear, doesnt show nodes.
for (int index = 0; index < value.getData().size(); index++) {
XYChart.Data dataPoint = value.getData().get(index);
Node lineSymbol = dataPoint.getNode().lookup(".chart-line-symbol");
lineSymbol.setStyle("-fx-background-color: " + definedColor + " , white;");
Label label = new Label(value.getName());
label.toFront();
Pane nodeWithText = new Pane();
label.setStyle("-fx-font-size: 20; -fx-font-weight: bold;");
StackPane stackPane = new StackPane();
Group group = new Group(label);
group.getChildren().add(lineSymbol);
group.toFront();
group.setLayoutX(-10);
group.setLayoutY(-30);
StackPane.setAlignment(group, Pos.TOP_RIGHT);
StackPane.setMargin(group,new Insets(0,0,5,0));
nodeWithText.getChildren().add(group);
nodeWithText.getChildren().add(lineSymbol);
dataPoint.setNode(nodeWithText);
}
One option is to set a custom Node for each Data in your chart. Here's a crude example:
import javafx.application.Application;
import javafx.beans.binding.ObjectExpression;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
var chart = new LineChart<>(new NumberAxis(), new NumberAxis());
chart.getData().add(new Series<>(createData()));
primaryStage.setScene(new Scene(chart, 600, 400));
primaryStage.show();
}
private static ObservableList<Data<Number, Number>> createData() {
var list = FXCollections.<Data<Number, Number>>observableArrayList();
for (int x = 0; x < 10; x++) {
var data = new Data<Number, Number>(x, Math.pow(x, 2));
data.setNode(createDataNode(data.YValueProperty()));
list.add(data);
}
return list;
}
private static Node createDataNode(ObjectExpression<Number> value) {
var label = new Label();
label.textProperty().bind(value.asString("%,.2f"));
var pane = new Pane(label);
pane.setShape(new Circle(6.0));
pane.setScaleShape(false);
label.translateYProperty().bind(label.heightProperty().divide(-1.5));
return pane;
}
}
The above doesn't do anything complex when it comes to positioning the text. For instance, it won't take into account where the line is nor whether or not some of the text is cut off by the chart's clip. Basically, it's a proof-of-concept; the result looks like:
Some other possible options for adding text to the chart include:
Put the LineChart in a Group or Pane where the chart is the first child. Then, for each Data in your chart, you'd add a Label or Text to the parent and position it relative to the position of the Data's node (when it becomes available). You can calculate these positions using methods such as Node#localToScene and Node#sceneToLocal.
Subclass LineChart and add the Text or Label to the chart directly. As I've never subclassed a chart before, I'm not sure how to implement this option.
Something else I'm not thinking of.
Note that, no matter what you do, if your chart has a lot of data points very close to each other then displaying all the text in a visually pleasing way will be difficult—if not impossible.

Pane will not add all children unless I individually add text first in JAVA / Java FX

I'm working on a homework problem where I am making a cylinder using Java and Java FX. The cylinder is to re-size based on the size of the window.
I start by creating an ellipse at the top, two vertical lines, and two arc's at the bottom (one dashed). I have bound them to the pane, so they change as the window gets re-sized.
When I try to run it, the program compiles fine (in Intelli-J), the new Java window show up, but the program seems to hang there. I can't access the window, just see it in the program bar on my Mac.
For some reason, when I add text individually to the pane before I add all with my shapes, it works fine?
Code is below. Any help would be greatly appreciated!
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.Ellipse;
import javafx.scene.shape.Line;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class CylinderResize extends Application {
#Override
public void start(Stage primaryStage) {
//Create Pane
Pane pane = new Pane();
Scene scene = new Scene(pane);
//Create Elipse for top
Ellipse ellipse = new Ellipse();
//Make the Center X property half of the pane width
ellipse.centerXProperty().bind(pane.widthProperty().divide(2));
//Height starts 1/3 the way down
ellipse.centerYProperty().bind(pane.heightProperty().divide(3));
//X Radius is 1/4 the width property and y radius is 1/8 WIDTH property
ellipse.radiusXProperty().bind(pane.widthProperty().divide(4));
ellipse.radiusYProperty().bind(pane.heightProperty().divide(8));
ellipse.setStroke(Color.BLACK);
ellipse.setFill(Color.WHITE);
//Create Solid arch for bottom
Arc solidArc = new Arc();
solidArc.centerXProperty().bind(pane.widthProperty().divide(2));
solidArc.centerYProperty().bind(pane.heightProperty().multiply(2).divide(3));
solidArc.radiusXProperty().bind(pane.widthProperty().divide(4));
solidArc.radiusYProperty().bind(pane.heightProperty().divide(8));
solidArc.setStartAngle(180);
solidArc.setLength(180);
solidArc.setType(ArcType.OPEN);
solidArc.setStroke(Color.BLACK);
solidArc.setFill(Color.WHITE);
//Create dashed line for bottom
Arc dashedArc = new Arc();
dashedArc.centerXProperty().bind(pane.widthProperty().divide(2));
dashedArc.centerYProperty().bind(pane.heightProperty().multiply(2).divide(3));
dashedArc.radiusXProperty().bind(pane.widthProperty().divide(4));
dashedArc.radiusYProperty().bind(pane.heightProperty().divide(8));
dashedArc.setStartAngle(0);
dashedArc.setLength(180);
dashedArc.setType(ArcType.OPEN);
dashedArc.setFill(Color.WHITE);
dashedArc.setStroke(Color.BLACK);
dashedArc.getStrokeDashArray().addAll(6.0, 21.0);
//Create Vertical Lines for the sides
Line leftLine = new Line();
leftLine.startXProperty().bind(ellipse.centerXProperty().subtract(ellipse.radiusXProperty()));
leftLine.startYProperty().bind(ellipse.centerYProperty());
leftLine.endXProperty().bind(solidArc.centerXProperty().subtract(solidArc.radiusXProperty()));
leftLine.endYProperty().bind(solidArc.centerYProperty());
leftLine.setStroke(Color.BLACK);
Line rightLine = new Line();
rightLine.startXProperty().bind(ellipse.centerXProperty().add(ellipse.radiusXProperty()));
rightLine.startYProperty().bind(ellipse.centerYProperty());
rightLine.endXProperty().bind(solidArc.centerXProperty().add(solidArc.radiusXProperty()));
rightLine.endYProperty().bind(solidArc.centerYProperty());
rightLine.setStroke(Color.BLACK);
//Test with a text box
Text text = new Text(25, 25, "WHY IS THIS REQUIRED?????");
//Add the objects to the pane
pane.getChildren().add(text); //WHEN I TAKE THIS LINE OUT, THE PROGRAM JUST HANGS.........
pane.getChildren().addAll(ellipse, solidArc, dashedArc, leftLine, rightLine);
//Set Up Stage
primaryStage.setTitle("Ellipse that scales");
primaryStage.setScene(scene);
primaryStage.show();
}
}
Its not that your program is hanging but it is the stage is not sizing to anything you can fix that by setting a height and width like so
primaryStage.setWidth(200);
primaryStage.setHeight(200);
The reason the text was fixing it was because it was making a base size that you can visibly see when you remove it you can no longer see it because its so small
Also well done on the project I think its cool

Get GridPane consisting of ImageViews to fit in Margin of BorderPane and resizing

I have a question concerning a Uni-project I'm working on.
We have to create a game, where we have a board that consists of tiles and you should be able to rotate these tiles with clicks. The Tiles should always fit in the Grid and the Grid should resize with the Window.
Now I made a class "TileView" which extends ImageView to get the pictures matching the pattern of the tile. Then I have TileGrid, which extends GridPane, which should consist of width*height tileviews. And this GridPane is in a BorderPane and this makes the scene.
Unfortunately I can't really put Code in here, because we have strict rules of plagiarism (and if someone sees my code and copies it, I'm going down too).
So, I make 2 for loops for the width and height and create a new TileView and set PreserveRatio on true and then do this:
setRowIndex(temp, i);
setColumnIndex(temp, j);
super.getChildren().add(temp);
this is in the class TileGrid.
Then I add it to the BorderPane with margin insets 100 and this is what happens:
https://imgur.com/a/OYSwER2
https://imgur.com/a/YUsZX09
But now I have the problem of fitting the TileViews to the size of the grid and make them resize with the window. I tried ("temp" is the just created ImageView as I put them in the TileGrid):
temp.fitWidthProperty().bind(prefWidthProperty());
temp.fitHeightProperty().bind(prefHeightProperty());
but then my single ImageViews are gigantic.
https://imgur.com/a/zaCz3OB
This is filling my whole screen.
I've tried numerous things, searched for hours and never achieved what I wanted so I really hope someone can help me or give me a tip even without my code.
Thanks in advance!
Here is a MCVE that demos what you need.
Centers the GridPane content
centerRoot.setAlignment(Pos.CENTER);
Binds the ImageViews fitHeight and fitWidth properties with the GridPanes height and width properties - 100(due to setMargins) / 10(the number of rows/columns)
for (int i = 0; i < 10; i++) {
for (int t = 0; t < 10; t++) {
Image image = new Image("https://s3.amazonaws.com/media.eremedia.com/uploads/2012/08/24111405/stackoverflow-logo-700x467.png");
ImageView imageView = new ImageView(image);
imageView.fitHeightProperty().bind(centerRoot.heightProperty().subtract(100).divide(10));
imageView.fitWidthProperty().bind(centerRoot.widthProperty().subtract(100).divide(10));
imageView.setPreserveRatio(false);
centerRoot.add(imageView, i, t);
}
}
Full Code:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
/**
*
* #author blj0011
*/
public class JavaFXApplication241 extends Application
{
#Override
public void start(Stage primaryStage)
{
BorderPane root = new BorderPane();
GridPane centerRoot = new GridPane();
centerRoot.setAlignment(Pos.CENTER);
GridPane.setMargin(centerRoot, new Insets(50, 50, 50, 50));
root.setCenter(centerRoot);
Scene scene = new Scene(root, 500, 500);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
for (int i = 0; i < 10; i++) {
for (int t = 0; t < 10; t++) {
Image image = new Image("https://s3.amazonaws.com/media.eremedia.com/uploads/2012/08/24111405/stackoverflow-logo-700x467.png");
ImageView imageView = new ImageView(image);
imageView.fitHeightProperty().bind(centerRoot.heightProperty().subtract(100).divide(10));
imageView.fitWidthProperty().bind(centerRoot.widthProperty().subtract(100).divide(10));
imageView.setPreserveRatio(false);
centerRoot.add(imageView, i, t);
}
}
}
/**
* #param args the command line arguments
*/
public static void main(String[] args)
{
launch(args);
}
}
As you can't provide any code I'll try to give you steps to work with I hope it might help
First as the BorderPane and GridPane will adjust their sizes to the window you have one problem which is adjusting the sizes of the ImageViews to make it resizable you have to bind their sizes to the size of the scene and take into consideration the number of the rows and columns you wanted for the grid
let's say you have 2 columns so image is 50% of the scene
imageView.fitWidthProperty().bind(scene.getWidthProperty().divide(2));
Note: I'm not sure about the syntax of this line I kinda busy sorry

Node child of HBox/VBox not following a drag, or moving in a weird way

First time asking something here, sorry for my broken English + I'm here because I'm getting upset by my code's behavior, which won't help my english.
Using javaFX (with java only, no FXML) I am trying to make a Node looking like this
(So multiples HBox in 2 VBox in one HBox)
And whenever I move it (on Drag), it moves on the screen, but if I use ChangeListener on the child's properties I can see they are not moving at all in reality (layoutX or translateX stay at starting values), which cause me a lot of trouble since I want to connect the nodes's cell (and clicking on things not there is quite the fun).
And when I tried to force it by binding the translateXProperty and all those things my Node's content juste goes away and the code tell me it is where it's supposed to be, but on screen it's juste gone (see pic)
(There's only an offset on the X value because I used bind() only on translateX to show the problem).
Sorry this is some bad paint right there but I don't know how to show otherwise, and I don't think giving my code would be relevant since I respected the first picture organization. I have tried using Group and Pane and subclasses but it doesn't work, and the HBox + VBox system organize my content just how I want it to be.
I've been looking at the doc and because of my English there must be something I didn't understand at all but well, I don't know, so if you have any idea, if possible where I can keep this organization, thanks for your help.
EDIT :
As some people asked here is what it's looks like in code, with some Listener to see that the content doesn't move, and because I need to be able to make a link between the HBox's circles I need them to have the right translate values.
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
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 double mouseX;
private double mouseY;
private double sourceX;
private double sourceY;
public static void main(String[] args)
{
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception
{
HBox root = new HBox(10);
// Making the first col (see first image)
VBox left = new VBox(5
, new HBox(10, new Circle(10, Color.WHEAT), new Rectangle(50, 20, Color.CORAL))
, new HBox(10, new Circle(10, Color.WHEAT), new Rectangle(50, 20, Color.CORAL))
);
// Making the second col (see first image)
VBox right = new VBox(5);
right.translateXProperty().addListener((observable, oldValue, newValue) -> System.out.println("right VBox " +
"translate to " + newValue)); // Isn't called
root.getChildren().add(right);
// Making the first row of the right col (see first image)
HBox row0Right = new HBox(10);
row0Right.translateXProperty().addListener((observable, oldValue, newValue) -> System.out.println("row0Right " +
"HBox translate to " + newValue)); // Isn't called
row0Right.getChildren().add(new Rectangle(50, 20, Color.CYAN));
// Making a Circle with a translate Listener
Circle circle = new Circle(10, Color.CRIMSON);
circle.translateXProperty().addListener((observable, oldValue, newValue) -> System.out.println("circle " +
"row0col1 translate to " + newValue)); // Isn't called
row0Right.getChildren().add(circle);
// Making the second row of the right col (see first image)
HBox row1Right = new HBox(10);
row1Right.translateXProperty().addListener((observable, oldValue, newValue) -> System.out.println("row1Right " +
"HBox translate to " + newValue)); // Isn't called
row1Right.getChildren().add(new Rectangle(50, 20, Color.CYAN));
row1Right.getChildren().add(new Circle(10, Color.CRIMSON));
right.getChildren().addAll(row0Right, row1Right);
root.setOnMousePressed(event ->
{
mouseX = event.getSceneX();
mouseY = event.getSceneY();
sourceX = ((Node) (event.getSource())).getTranslateX();
sourceY = ((Node) (event.getSource())).getTranslateY();
});
root.setOnMouseDragged(event ->
{
double newX = sourceX + event.getSceneX() - mouseX;
double newY = sourceY + event.getSceneY() - mouseY;
((Node) (event.getSource())).setTranslateX(newX);
((Node) (event.getSource())).setTranslateY(newY);
});
root.translateXProperty().addListener((observable, oldValue, newValue) -> System.out.println("root translate " +
"to " + newValue)); // Is the only one called
primaryStage.setTitle("Hello World");
primaryStage.setScene(new Scene(root, 720, 720));
primaryStage.show();
}
}
EDIT 2 :
I noticed that if I use a Rectangle as the moving root, and bind shapes translates to it, everything is fine, but I prefer the way the HBox/VBox place the children as I don't want to set every single px in x, y, width and height for every shapes bind to my core rectangle (which might become a lot of shapes) when I can use those Box pattern.

What is the preferred size of a control?

My first attempt at playing with JavaFX and I'm trying to understand a little bit about layout management. How do we access the preferred size of a control?
In the example below I am attempting to set the maximum width 200 pixels greater than the preferred width. That is, I want to allow the button to grow (up to a maximum) as the width of the frame is increased.
However when I run the code the preferred width is -1, so adding 200 to the preferred width gives a maximum width of 199.
import javafx.application.Application;
import javafx.event.*;
import javafx.stage.Stage;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.geometry.Insets;
public class BorderPaneSSCCE extends Application
{
#Override
public void start(Stage primaryStage)
{
Button button = new Button( "Button at PreferredSize" );
button.setMaxWidth( button.getPrefWidth() + 200 );
System.out.println(button.prefWidth(-1) + " : " + button.getPrefWidth());
button.setOnAction(new EventHandler<ActionEvent>()
{
#Override
public void handle(ActionEvent event)
{
System.out.println("Width: " + button.getWidth());
}
});
HBox root = new HBox();
HBox.setHgrow(button, Priority.ALWAYS);
root.getChildren().add(button);
Scene scene = new Scene(root);
primaryStage.setTitle("Java FX");
primaryStage.setScene(scene);
primaryStage.show();
System.out.println(button.prefWidth(-1) + " : " + button.getPrefWidth());
}
public static void main(String[] args)
{
launch(args);
}
}
When I run the code and click on the button I see: Width: 139.0
After I resize the width of the frame so the button is as large as possible and I click on the button I see: Width: 199.0
I was hoping to see Width: 339.0 (ie. 139 + 200)
So, how do we access the preferred size/width of a control?
getPrefWidth() returns the USE_COMPUTED_SIZE flag (which is -1) by default.
You can use prefWidth(-1) to get the internally computed preferred width. However the preferred width will not be calculated until the node is being layed out by the layout pane (HBox in your example). This happens the first time when the stage is shown.
You have multiple options, if you want the maximum width to depend on the preferred width. One is to set the preferred width to a fixed value with setPrefWidth() prior to setting the maximum width.
Another is to implement a custom layout algorithm - either on the node or on a layout pane. Here is an example using a custom button.
// button whose maxWidth is always prefWidth + 200
Button button = new Button() {
#Override
protected double computeMaxWidth(double height)
{
return this.prefWidth(height) + 200.0;
}
};
-1 is the value of Region.USE_COMPUTED_SIZE, which tells the component to figure out it's own best size. This works because every Node has a method called prefWidth(double height) and it's double prefHeight(double width) (the same for min/max size) that layout managers use for calculating the layout of their children. To get the pref size you need to use
button.prefWidth([-1 or Region.USE_COMPUTED_SIZE])
(docs)
getPrefWidth() only works after the Node has been shown / rendered.
You can either show the Button first or try to use Node.prefWidth(-1).

Categories

Resources