What is the preferred size of a control? - java

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).

Related

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

JavaFX How Can I Center a Tooltip on a Node?

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.

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.

Scaling a button's text automatically with the button in JavaFX

I made a grid of buttons in JavaFX.
When I resize the window with the grid inside, the buttons resize within the grid as well.
The problem is that the text on those buttons doesn't resize along with them: it stays the same size all the time, so when the buttons grow big, there's a lot of empty space on a button and then a tiny little text in the middle, which looks terrible.
I would rather like the text to automatically resize along with these buttons to fit the empty space, so that when the entire user interface gets bigger, the text on the buttons gets bigger as well.
How can I accomplish that?
I tried setting the -fx-font-size in the CSS stylesheet to percentage values, but it doesn't seem to work the same way as for websites: the text doesn't scale as a percentage of its container, but as a percentage of some predefined text size.
Edit
This is not a duplicate! Stop marking each question out there as duplicate! If it has been answered, I wouldn't have asked it in the first place!
From what I see, the first of those threads was about a situation where someone wanted to set the size/style of the text for newly-created buttons to account for the current size of their container etc. This is not what I need, because I want the buttons which has been already created as well to automatically resize their texts when these buttons resize inside their container in some way.
The other thread was about scaling the text along with the root container / window with a preset font size. This is also different from what I need, because I don't want the text to be scaled with the window, but with the sizes of the buttons themselves. And it has to be scaled in a certain way: to always fit the size of the button. You know: the text stays the same, but stretches so that it always fits the inside of the button (with a little padding, not a huge empty area around the text).
It is the button's size which is to determine the size of the text on it, not the window or container or something else, and it needs to be done automatically by the button itself (either the built-in one or a subclassed one), not manually by its encompassing container iterating over all these buttons and updating their text's sizes (which would be dumb way to do it).
This is, liked the linked questions, something of a hack: but consider scaling the text node inside the button instead of changing the font size. This seems to work ok:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.stage.Stage;
public class ScaledButtons extends Application {
#Override
public void start(Stage primaryStage) {
GridPane root = new GridPane();
root.setHgap(5);
root.setVgap(5);
for (int i = 1; i <= 9 ; i++) {
root.add(createScaledButton(Integer.toString(i)), (i-1) % 3, (i-1) / 3);
}
root.add(createScaledButton("#"), 0, 3);
root.add(createScaledButton("0"), 1, 3);
root.add(createScaledButton("*"), 2, 3);
primaryStage.setScene(new Scene(root, 250, 400));
primaryStage.show();
}
private Button createScaledButton(String text) {
Button button = new Button(text);
GridPane.setFillHeight(button, true);
GridPane.setFillWidth(button, true);
GridPane.setHgrow(button, Priority.ALWAYS);
GridPane.setVgrow(button, Priority.ALWAYS);
button.layoutBoundsProperty().addListener((obs, oldBounds, newBounds) ->
scaleButton(button));
button.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
return button ;
}
private void scaleButton(Button button) {
double w = button.getWidth();
double h = button.getHeight();
double bw = button.prefWidth(-1);
double bh = button.prefHeight(-1);
if (w == 0 || h == 0 || bw == 0 || bh == 0) return ;
double hScale = w / bw ;
double vScale = h / bw ;
double scale = Math.min(hScale, vScale);
button.lookup(".text").setScaleX(scale);
button.lookup(".text").setScaleY(scale);
}
public static void main(String[] args) {
launch(args);
}
}
An alternate approach to get a similar effect could be to subclass com.sun.javafx.scene.control.skin.ButtonSkin and override the layoutLabelInArea(double x, double y, double w, double h, Pos alignment) method from the skin's parent (LabeledSkinBase). You can then explicitly assign the updated skin to your button (either via CSS or via Java API calls).
Doing so would requires the subclassing of com.sun APIs which could change without notice in subsequent JavaFX releases. Also layoutLabelInArea is reasonably complex in its operation so changing the layout logic could be a little tricky. Certainly, James's suggestion of applying a text rescaling operation based upon a listener to the layout bounds property is simpler in this particular case.
I'm not necessarily advocating this approach, just providing a route to something that you could create that would satisfy your goal of: "It is the button's size which is to determine the size of the text on it, not the window or container or something else, and it needs to be done automatically by the button itself".

Categories

Resources