Get a StackPane's width - javafx

I can't figure out how to get a StackPane's width :
If i refer to http://docs.oracle.com/javafx/2.0/api/javafx/scene/layout/StackPane.html, a StackPane's dimensions should adapt to it's content :
A stackpane's parent will resize the stackpane within the stackpane's resizable range during
layout. By default the stackpane computes this range based on its content as outlined in the
table below.
preferredWidth left/right insets plus the largest of the children's pref widths.
Syntax here is scala, but the issue concerns javafx :
import javafx.scene.layout.StackPane
import javafx.scene.shape.Rectangle
class Bubble() extends StackPane {
val rect = new Rectangle(100, 100)
getChildren.add(rect)
println(rect.getWidth)
println(this.getWidth)
}
Output :
>> 100.0
>> 0.0 <- Why isn't it 100 ?
Is this a bug or the awaited behaviour ? How can i get a StackPane's content width ?
Thanx

Layout managers (and actually all resizable objects) don't update their bounds until application being actually shown. Rectangle gives width 100 because it's default value with your constructor.
See next code:
#Override
public void start(Stage stage) throws Exception {
StackPane root = new StackPane();
Rectangle rect = new Rectangle(100, 100);
root.getChildren().add(rect);
// 100 - 0
System.out.println(rect.getWidth());
System.out.println(root.getWidth());
stage.setScene(new Scene(root));
// 100 - 0
System.out.println(rect.getWidth());
System.out.println(root.getWidth());
stage.show();
// 100 - 100
System.out.println(rect.getWidth());
System.out.println(root.getWidth());
}
So you need either wait StackPane to be shown. Or better rely on JavaFX on that matter and use binding. E.g.
stage.titleProperty().bind(root.widthProperty().asString());

Related

JavaFX - "Pointless" (CSS) Shadow Effect, Drastically decrices Graphics Performance

Hello, People [...]
🤔 Summary
Whenever i use Shadow-effect on my BorderPane or any Component/control/Element, the 3D Graphics performance (as seen, in the Preview section below) is getting way too low.
The "confusing" part is that, it even gets low performance when the effect is applied to something that really has nothing to do with my Tab, Subscene or even my moving Button, in a way [...]
I Use jdk-12.0.1.
👁️ Preview
⚠️ Recreating The Issue
Files Needed:
App.java | main.fxml | AnchorPane.css | MathUtils.java | SimpleFPSCamera.java
📝 General Code
(You can refer to Recreating The Issue Section for more Informations too)
AnchorPane.css
#BorderPane1 {
-fx-effect: dropshadow(three-pass-box, rgb(26, 26, 26), 50, 0.6, 0, 0); /* Comment it*/
}
App.java
public class App extends Application {
#FXML
public Parent root;
public TabPane TabPane1;
public BorderPane BorderPane1;
public static void main(String[] args) throws Exception {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("main.fxml"));
loader.setController(this);
root = loader.load();
Scene RootScene = new Scene(root, 1120, 540);
primaryStage.setScene(RootScene);
Thread t = new Thread() {
public void run() {
//Setting NewButton2
Button NewButton2 = new Button();
NewButton2.setId("Button2");
NewButton2.setText("test2");
NewButton2.setPrefWidth(150);
NewButton2.setPrefHeight(50);
NewButton2.setTranslateX(-75);
NewButton2.setTranslateY(-25);
NewButton2.setTranslateZ(900);
// Setting group
Group SubRootGroup = new Group(NewButton2);
SubRootGroup.setTranslateX(0);
SubRootGroup.setTranslateY(0);
SubRootGroup.setTranslateZ(0);
// Setting Scene
SubScene SubScene1 = new SubScene(SubRootGroup, 0, 0, true, SceneAntialiasing.BALANCED);
SubScene1.setId("SubScene1");
SubScene1.setFill(Color.WHITE);
SubScene1.heightProperty().bind(RootScene.heightProperty());
SubScene1.widthProperty().bind(RootScene.widthProperty());
// Initializing Camera
SimpleFPSCamera SimpleFPSCam = new SimpleFPSCamera();
// Setting Camera To The Scene
SubScene1.setCamera(SimpleFPSCam.getCamera());
// Adding Scene To Stage-TabPane.Tab(0)
TabPane1.getTabs().add(new Tab("Without Shadows"));
TabPane1.getTabs().get(0).setContent(SubScene1);
// Loading Mouse & Keyboard Events
SimpleFPSCam.loadControlsForSubScene(SubScene1);
}
};
t.setDaemon(true);
t.run();
primaryStage.show();
}
}
Things I 've Tried Until Now
javafx animation poor performance consumes all my cpu
setCache(true);
setCacheShape(true);
setCacheHint(CacheHint.SPEED);
(i have tried using it with all components without having any success [it might be my poor javaFX knowledge too , [using it in the wrong way?] ])
...
💛 Outro
Any Idea? Thanks In Advance, Any help will be highly appreciated, 💛 [...]
George.
Most probably, you will have figured this out by now, but since I was also banging my head about this same issue in the past, here is your answer:
The drop shadow effect is "expensive" and drawing it is slow. If you use it on a node with many descendants and move any of the descendants, it will cause the effect to be re-calculated on the parent, so the whole animation becomes slow (regardless if the parent itself is animated or not).
I solved this by using a StackPane as the top-most container, to which I added a Pane as a first child (which has the css drop-shadow effect) and the normal top-level container for the actual controls as a second child.
This way, the "shadow" pane is not updated when something is animated down the layout tree and, voila, you have a working drop-shadow effect without a performance hit :-)

Region width not refreshing automatically after modifying Grid Pane to which the region width is bound - JavaFX

I'm making a simple Java GUI app using JavaFX that has a Border Pane as the root node.
In the top section of the Border Pane, there is a Grid Pane with three columns (top Grid Pane from now on).
In the first column of the top Grid Pane, there is a Home Button, in the second column, there is an empty Region that only serves as spacer between the first and third column of the top Grid Pane, and in the third column, there is another GridPane (right Grid Pane from now on).
The right Grid Pane contains one Button (Log In Button) on start. However, when a user successfully logs into the app, two other Buttons and a Label are added to the right Grid Pane as part of the Log In Button click event.
The spacer maxWidthProperty and minWidthProperty are bound to the top Grid Pane (tgp) widthProperty and the right Grid Pane(rgp) widthProperty like this:
spacer.minWidthProperty().bind(tgp.widthProperty().subtract(80).subtract(rgp.widthProperty()).subtract(3));
spacer.maxWidthProperty().bind(tgp.widthProperty().subtract(80).subtract(rgp.widthProperty()).subtract(3));
which makes the right Grid Pane move nicely with its buttons staying on the right side of the scene when a user resizes the main stage.
However, a problem occurs when the user logs in and additional buttons are added to the right Grid Pane. The spacer somehow misses this change and its width stays the same, which makes the additional Buttons appear outside of the current stage width. The only way to refresh the spacer width is to interact with the stage somehow, by clicking minimize/maximize/restore or by clicking any button on the scene.
Is there a way to automatically refresh Region width after the nodes to which its width is bound to are modified? Or, is there a better approach to making a top Grid Pane with one button on the left and modifiable number of buttons (nodes) on the right?
Edit: Here is a demonstration of the problem with several screenshots stacked on one another:
Minimal reproducible example:
BorderPane root = new BorderPane();
GridPane tgp = new GridPane();
tgp.minWidthProperty().bind(root.widthProperty());
tgp.maxWidthProperty().bind(root.widthProperty());
tgp.setStyle("-fx-background-color: WHITE; -fx-border-color: LIGHTGREY;");
tgp.setMinHeight(37);
tgp.setMaxHeight(37);
root.setTop(tgp);
Button homeButton = new Button("Home"));
homeButton.setMinHeight(35);
homeButton.setMaxHeight(35);
homeButton.setMinWidth(80);
homeButton.setMaxWidth(80);
tgp.add(homeButton, 0, 0);
GridPane rgp = new GridPane(); // Right Grid Pane - holds User related nodes
rgp.setHgap(5);
tgp.add(rgp, 2, 0);
Label unl = new Label("My Profile");
unl.setFont(new Font("Calibri", 15));
unl.setTextFill(Color.RED);
unl.setMinWidth(Region.USE_PREF_SIZE);
Button wlButton = new Button("Watchlist");
wlButton.setMinHeight(35);
wlButton.setMaxHeight(35);
wlButton.setMinWidth(80);
wlButton.setMaxWidth(80);
Button cartButton = new Button("Cart");
cartButton.setMinHeight(35);
cartButton.setMaxHeight(35);
cartButton.setMinWidth(60);
cartButton.setMaxWidth(60);
Button logInOutButton = new Button("Log In");
logInOutButton.setMinHeight(35);
logInOutButton.setMaxHeight(35);
logInOutButton.setMinWidth(60);
logInOutButton.setMaxWidth(60);
rgp.add(logInOutButton, 3, 0);
logInOutButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
if (logInOutButton.getText().equals("Log In")) {
LogInStage lis = new LogInStage();
lis.initStage();
if (lis.username != null) {
logInOutButton.setText("Log Out");
rgp.add(unl, 0, 0);
rgp.add(wlButton, 1, 0);
rgp.add(cartButton, 2, 0);
}
} else if (logInOutButton.getText().equals("Log Out")) {
logInOutButton.setText("Log In");
rgp.getChildren().remove(unl);
rgp.getChildren().remove(wlButton);
rgp.getChildren().remove(cartButton);
}
}
});
Region spacer = new Region();
spacer.minWidthProperty().bind(tgp.widthProperty().subtract(80).subtract(rgp.widthProperty()).subtract(3));
spacer.maxWidthProperty().bind(tgp.widthProperty().subtract(80).subtract(rgp.widthProperty()).subtract(3));
tgp.add(spacer, 1, 0)
It's always a bad idea to use bindings, if you can avoid it. Any changes to the size constraints can lead to a new layout pass being scheduled, but during the layout pass they are assumed to be constant. If you now introduce a binding the following sequence of events could happen:
A layout pass is requested for the GridPane, setting a flag to indicate layout is required
A layout pass happens. During the layout pass the children are resized. This triggers an update of the constraints of the children with the bindings.
The flag is cleared, but the changes to the contraints already happened. The layout won't reflect this. The GridPane gets another reason to do a layout.
I don't know, how your scene is set up in detail, but I recommend using column constraints: Set the grow priorities for the outer ones to SOMETIMES and the one for the center to ALWAYS. If you require some spacing around the children, you could use GridPane.setMargin (or the padding of the GridPane itself, if you require the a distance to the edges for all children).
#Override
public void start(Stage primaryStage) {
Button[] rightContent = new Button[3];
for (int i = 0; i < rightContent.length; i++) {
Button btn = new Button(Integer.toString(i));
GridPane.setColumnIndex(btn, i);
rightContent[i] = btn;
}
Button cycle = new Button("cycle");
GridPane rgp = new GridPane(); // I would usually use a HBox here
// don't grow larger than needed
rgp.setMaxWidth(Region.USE_PREF_SIZE);
// cycle though 0 to 3 buttons on the right
cycle.setOnAction(new EventHandler<ActionEvent>() {
int nextIndex = 0;
#Override
public void handle(ActionEvent event) {
if (nextIndex >= rightContent.length) {
rgp.getChildren().clear();
nextIndex = 0;
} else {
rgp.getChildren().add(rightContent[nextIndex]);
nextIndex++;
}
}
});
ColumnConstraints sideConstraints = new ColumnConstraints();
sideConstraints.setHgrow(Priority.SOMETIMES);
ColumnConstraints centerConstraints = new ColumnConstraints();
centerConstraints.setHgrow(Priority.ALWAYS);
//prefer to grow the center part of the GridPane
GridPane root = new GridPane();
root.getColumnConstraints().addAll(sideConstraints, centerConstraints, sideConstraints);
root.add(cycle, 0, 0);
root.add(rgp, 2, 0);
// add something to visualize the center part
// you could simply leave this part out
Region center = new Region();
center.setStyle("-fx-border-radius: 10;-fx-border-width: 1;-fx-border-color:black;");
root.add(center, 1, 0);
Scene scene = new Scene(root, 400, 300);
primaryStage.setScene(scene);
primaryStage.show();
}
As mentioned in the comments, the center region is not actually needed.

JavaFX - Get Coordinates of Node Relative to its Parent

I am making a simple graphical interface for saving previously generated images. All images come to me square but I want to allow for some cropping functionality (more precisely cutting off equal parts from the bottom and top of the image). I want to do this by allowing the user to drag a shaded region over the image which will tell the user that this region will be cropped out. See the below image for details. To enable this drag functionality I have added small triangles that I want the user to drag which in turn will move the shaded regions about. However the coordinates for the triangles are all weird and seem nonsensical. Therefor I was wondering what the best way is to get the coordinates of the triangles in relation to the ImageView (or their first common parent node) in terms of ImageView-side-lengths. So if the triangle is in the center its coordinates are [0.5, 0.5] for instance.
The Image view will be moving around inside the scene and will also be changing size so it is vital that I can get the coordinates relative to not only the ImageView but also to the size of the ImageView.
Here is also the surrounding hierarchy of nodes if that helps. The Polygons are the triangles and the regions are the rectangles.
Thanks for all forms of help!
Node.getBoundsInParent returns the bounds of a node in it's parent coordinates. E.g. polygon.getBoundsInParent() would return the bounds in the VBox.
If you need to "go up" one additional step, you can use parent.localToParent to do this. vBox.localToParent(boundsInVbox) returns the bounds in the coordinate system of the AnchorPane.
To get values relative to the size of the image, you simply need to divide by it's size.
The following example only allows you to move the cover regions to in one direction and does not check, if the regions intersect, but it should be sufficient to demonstrate the approach.
The interesting part is the event handler of the button. It restricts the viewport of the second image to the part of the first image that isn't covered.
private static void setSideAnchors(Node node) {
AnchorPane.setLeftAnchor(node, 0d);
AnchorPane.setRightAnchor(node, 0d);
}
#Override
public void start(Stage primaryStage) {
// create covering area
Region topRegion = new Region();
topRegion.setStyle("-fx-background-color: white;");
Polygon topArrow = new Polygon(0, 0, 20, 0, 10, 20);
topArrow.setFill(Color.WHITE);
VBox top = new VBox(topRegion, topArrow);
top.setAlignment(Pos.TOP_CENTER);
topArrow.setOnMouseClicked(evt -> {
topRegion.setPrefHeight(topRegion.getPrefHeight() + 10);
});
// create bottom covering area
Region bottomRegion = new Region();
bottomRegion.setStyle("-fx-background-color: white;");
Polygon bottomArrow = new Polygon(0, 20, 20, 20, 10, 0);
bottomArrow.setFill(Color.WHITE);
VBox bottom = new VBox(bottomArrow, bottomRegion);
bottom.setAlignment(Pos.BOTTOM_CENTER);
bottomArrow.setOnMouseClicked(evt -> {
bottomRegion.setPrefHeight(bottomRegion.getPrefHeight() + 10);
});
Image image = new Image("https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg/402px-Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg");
ImageView imageView = new ImageView(image);
setSideAnchors(top);
setSideAnchors(bottom);
setSideAnchors(imageView);
AnchorPane.setTopAnchor(top, 0d);
AnchorPane.setBottomAnchor(bottom, 0d);
AnchorPane.setTopAnchor(imageView, 0d);
AnchorPane.setBottomAnchor(imageView, 0d);
AnchorPane container = new AnchorPane(imageView, top, bottom);
ImageView imageViewRestricted = new ImageView(image);
Button button = new Button("restrict");
button.setOnAction(evt -> {
// determine bouns of Regions in AnchorPane
Bounds topBounds = top.localToParent(topRegion.getBoundsInParent());
Bounds bottomBounds = bottom.localToParent(bottomRegion.getBoundsInParent());
// set viewport accordingly
imageViewRestricted.setViewport(new Rectangle2D(
0,
topBounds.getMaxY(),
image.getWidth(),
bottomBounds.getMinY() - topBounds.getMaxY()));
});
HBox root = new HBox(container, button, imageViewRestricted);
root.setFillHeight(false);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}

JavaFX binding only applied after resizing the window

When I run the following code in the start method of my Main (JavaFX) class I get weird results. The window gets displayed but pane (with a green border) has a width of 0. It is supposed to have the same width as the container's height since I binded prefWidth to the height property.
Then, when I resize the window, the binding comes into effect and the pane becomes a square. Notice that if I maximize the window it also doesn't apply the bindings.
Thank you!
//Create a pane with a min width of 10 and a green border to be able to see it
Pane pane = new Pane();
pane.setStyle("-fx-border-color: green; -fx-border-width: 2");
//Bind the pane's preferred width to the pane's height
pane.prefWidthProperty().bind(pane.heightProperty());
//Put the pane in a vbox that does not fill the stage's width and make the pane grow in the vbox
VBox container = new VBox(pane);
container.setFillWidth(false);
VBox.setVgrow(pane, Priority.SOMETIMES);
//Show the vbox
primaryStage.setScene(new Scene(container, 600, 400));
primaryStage.show();
The problem you are running into here is that when the container is laid out, it has no reasonable information as to the order in which it should compute the width and the height of the pane. So essentially what happens is it computes the width, which (since it's empty), is zero; then computes the height (which fills the container, since you told the VBox to do that). After that, the prefWidth property is changed, but by then the actual width has already been set, so it's essentially too late. The next time a layout pass occurs, the new pref width is taken into account.
I haven't checked the actual layout code, but (since the default content bias is null) most likely the layout code for the vbox is going to do something equivalent to the following pseudocode:
protected void layoutChildren() {
// content bias is null:
double prefWidth = pane.prefWidth(-1);
double prefHeight = pane.prefHeight(-1);
// no fill width:
double paneWidth = Math.max(this.getWidth(), prefWidth);
// vgrow, so ignore preferred height and size to height of the vbox:
double paneHeight = this.getHeight();
pane.resizeRelocate(0, 0, paneWidth, paneHeight);
}
The last call actually causes the height of the pane to change, which then causes the prefWidth to change via the binding. Of course, that's too late for the current layout pass, which has already set the width based on the previous preferred width calculation.
Basically, relying on bindings to manage layout like this is not a reliable way of doing things, because you are changing properties (such as prefWidth in this example) during the layout pass, when it may be already too late to resize the component.
The reliable way to manage layout for a pane like this is to override the appropriate layout methods, which are invoked by the layout pass in order to size the component.
For this example, since the width depends on the height, you should return VERTICAL for the contentBias, and you should override computePrefWidth(double height) to return the height (so the width is set to the height):
#Override
public void start(Stage primaryStage) {
Pane pane = new Pane() {
#Override
public Orientation getContentBias() {
return Orientation.VERTICAL ;
}
#Override
public double computePrefWidth(double height) {
return height ;
}
};
pane.setStyle("-fx-border-color: green; -fx-border-width: 2");
//Bind the pane's preferred width to the pane's height
// pane.prefWidthProperty().bind(pane.heightProperty());
//Put the pane in a vbox that does not fill the stage's width and make the pane grow in the vbox
VBox container = new VBox(pane);
container.setFillWidth(false);
VBox.setVgrow(pane, Priority.SOMETIMES);
//Show the vbox
primaryStage.setScene(new Scene(container, 600, 400));
primaryStage.show();
}

JavaFX: How can I horizontally center an ImageView inside a ScrollPane?

viewScroll.setContent(new ImageView(bigimg));
double w = viewScroll.getContent().getBoundsInLocal().getWidth();
double vw = viewScroll.getViewportBounds().getWidth();
viewScroll.getContent().setTranslateX((vw/2)-(w/2));
viewScroll.toFront();
I set an ImageView with some Image inside the ScrollPane but the ImageView always goes to the far left corner. Here I'm trying to manually offset the difference, but it doesn't work well. The ImageView goes too far to the right plus it only updates once because it's inside the eventhandler for a button.
Here is an example using a label without the need for listeners:
public void start(Stage primaryStage) throws Exception {
ScrollPane scrollPane = new ScrollPane();
Label label = new Label("Hello!");
label.translateXProperty().bind(scrollPane.widthProperty().subtract(label.widthProperty()).divide(2));
scrollPane.setContent(label);
Scene scene = new Scene(scrollPane);
primaryStage.setScene(scene);
primaryStage.setWidth(200);
primaryStage.setHeight(200);
primaryStage.show();
}
I am not sure if you are familiar with properties or not, but in the code above, when I bind the translateXProperty to the formula, every time one of the dependencies changes, such as the ScrollPane's widthProperty or the Label's widthProperty, the formula is recalculated and translateXProperty is set to the result of the formula.
I am not sure, but in your previous code, it appears that the calculation code would be in a resize listener. This is not required when dealing with properties as they update whenever dependencies changed (note the bind() and not set()).

Resources