.setLayoutX() not affecting position in FlowPane - javafx

For some reason or another textExampleTwo.setLayoutX(40) does not actually result in the Text moving at all to the right. Is this a bug or have I missed something important here?
public void start(Stage stage) throws Exception {
FlowPane flowPane = new FlowPane();
flowPane.setOrientation(Orientation.VERTICAL);
Text textExampleOne = new Text("An example - 1");
Text textExampleTwo = new Text("An example - 2");
textExampleTwo.setLayoutX(40.0);
flowPane.getChildren().addAll(textExampleOne, textExampleTwo);
Scene applicationScene = new Scene(flowPane);
stage.setHeight(400.0);
stage.setWidth(400.0);
stage.setScene(applicationScene);
stage.show();
}

You've missed something important here:
Many Panes including FlowPane determine the position of their children on their own. For positioning the layoutX and layoutY properties are used. If you assign a new value to one of them and the Node is a child of a layout that positions it's children itself, this just leads to the position to be changed back during the next layout pass.
The exception to this are Nodes with the managed property set to false. This leads to neither layoutX nor layoutY being assigned however.
In your case you seem to want a combination of the two.
In this case the desired effect can be achieved by setting a margin.
// set all insets except the left one to 0
FlowPane.setMargin(textExampleOne, new Insets(0, 0, 0, 40));
Note however that this does not set the x position to 40, but it keeps a space of size 40 at the left of the Node. If you add enough children before this node to move it to the second column, this spacing would be used to calculate the distance to the beginning of the column.

Related

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.

SetPadding of one single elemet in a layout whith many childrens

There is a way to set a Padding of one single element in a VBox having other childrens?
this.layout_p = new VBox();
this.txta_p = new TextArea();
this.m_p = new Button("m");
this.o_p = new Button("o");
this.c_p = new Button("c");
this.oa_p = new Button("oa");
this.np_p = new Button("np'");
layout_p.getChildren().addAll(txta_p, m_p,
o_p, c_p, oa_p, np_p);
layout_p.setAlignment(Pos.CENTER_RIGHT);
I would like to have the button: np_p with a top padding of 50 from others buttons.
So having a separation between oa_p and np_p.
(Without using another VBox for that button and so, setPadding(...))
Setting a padding for a single node does not make sense. Padding is the room between the bounds of a node and it's content, see also the CSS Box Model.
You're actually trying to add a margin here, i.e. some space around the node. This can be done using VBox.setMargin.
VBox.setMargin(np_p, new Insets(50, 0, 0, 0));

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

Strange behaviour when having a group in vbox

Can someone please explain me why this strange behaviour exists? When I have a group in a vbox every item in a child appears to modify the siblings to.
Following strange behaviour happens:
everything normal here
here too everything normal
whoops, why did the searchbar move???
First of the structure of the application I have:
root (VBox) //vbox so the menubar has its own space
├───menubar (MenuBar)
└───contentroot (Group)
├───searchbar (TextField) //searchbar should always be on the top left
└───nodeRoot (Group)
├───circle1 (Nodes)
└───circle2 (Nodes)
The root is a vbox so the menubar has its own unchallenged space. The searchbar should always be top left, directly under the menubar.
The nodeRoot should contain every other node. Kinda like a drawing board which I should be able to drag around.
Code:
public void start(Stage primaryStage) throws Exception{
VBox root = new VBox();
MenuBar menuBar = new MenuBar(new Menu("File"));
Group contentRoot = new Group();
contentRoot.getChildren().add(new TextField("SearchBar"));
Group nodeRoot = new Group();
contentRoot.getChildren().add(nodeRoot);
root.getChildren().addAll(menuBar, contentRoot);
Circle circle = new Circle(30, Color.RED);
nodeRoot.getChildren().add(circle);
Scene scene = new Scene(root, 300, 275);
primaryStage.setScene(scene);
primaryStage.show();
scene.setOnMousePressed(event -> {
circle.setTranslateX(event.getSceneX() - 15);
circle.setTranslateY(event.getSceneY() - 15);
});
}
My guess why this happens:
The problem started to appear after I added the menubar and put everything into a VBox. This is when the siblings of the nodeRoot get changed too. My guess is that because VBox is a Region the behaviour is different than a normal group which expands. But then I dont understand why it only happens if the item moves to the left or top.
Can somebody please explain why this happens and how I can fix it?
From the javadocs for Group:
A Group will take on the collective bounds of its children and is not
directly resizable.
When you click near the top or left of the scene, the circle's bounds include negative values. Since the group takes on those bounds, it also takes on negative values. The TextField never has any layout bounds set, so the Group positions it at (0,0). Hence the text field can end up below or to the right of the circle. The vbox positions the group in order to try and contain it entirely, so it shifts it right if it contains negative x-value bounds and down if it contains negative y-value bounds.
If you use a Pane to contain the circle, instead of a Group:
Pane contentRoot = new Pane();
it behaves more intuitively: the Pane does not take on the union of the bounds of its child nodes, so if the circle has negative bounds, it just moves left and/or above the pane's visible area.

Resources