Get the Height of a Node in Javafx (Generate a Layout Pass)

Get the height of a node in JavaFX (generate a layout pass)

What you need to do is:

  1. Create the nodes which are to be placed in the VBoxes.
  2. Add them to some scene (it can just be a new dummy scene not attached to the stage but having the same CSS rules as your main scene).
  3. Call applyCss() and layout() on each of the nodes (or the dummy scene root).
  4. Measure the layout bounds of each of the nodes.
  5. Add the nodes to the VBoxes in your real scene according to your layout algorithm.

Related

To find out how to measure the size of a node, see the answer to:

  • How to calculate the pixel width of a String in JavaFX?

Background

JavaFX layout calculations work by applying CSS and a layout pass. Normally this occurs as part of a pulse (a kind of automated 60fps tick in the internal JavaFX system which checks for any dirty objects in the scene which need new css or layout applied to them). In most cases, you can just specify the changes you want to the scene and let the automated pulse layout mechanism handle the layout at that time; doing so is quite efficient as it means that any changes between a pulse are batched up and you don't need to manually trigger layout passes. However, when you need to actually get the size of things before the layout occurs (as in your case), then you need to manually trigger the CSS application and layout pass before you try to query the height and width extents of the node.

Documentation

Unfortunately the detailed Java 8u20 Javadoc for the node.applyCSS() method is currently broken, so I'll reproduce the sample which is in the Javadoc code here, so you can see the recommended usage in context. For the next Java feature release (8u40), the broken javadoc is fixed as part of RT-38330 Javadoc is missing for several methods of the Node class, so with that release, you will be able to find the following text in the JavaFX javadoc:

If required, apply styles to this Node and its children, if any. This method does not normally need to
be invoked directly but may be used in conjunction with layout() to size a Node before the
next pulse, or if the Scene is not in a Stage.

Provided that the Node's Scene is not null, CSS is applied to this Node regardless
of whether this Node's CSS state is clean. CSS styles are applied from the topmost parent
of this Node whose CSS state is other than clean, which may affect the styling of other nodes.
This method is a no-op if the Node is not in a Scene. The Scene does not have to be in a Stage.

This method does not invoke the layout() method. Typically, the caller will use the
following sequence of operations.

parentNode.applyCss();
parentNode.layout();

As a more complete example, the following code uses applyCss() and layout() to find
the width and height of the Button before the Stage has been shown. If either the call to applyCss()
or the call to layout() is commented out, the calls to getWidth() and getHeight()
will return zero (until some time after the Stage is shown).

public void start(Stage stage) throws Exception {
Group root = new Group();
Scene scene = new Scene(root);

Button button = new Button("Hello World");
root.getChildren().add(button);

root.applyCss();
root.layout();

double width = button.getWidth();
double height = button.getHeight();

System.out.println(width + ", " + height);

stage.setScene(scene);
stage.show();
}

layout() vs requestLayout()

Calling layout() will perform a layout pass immediately.

Calling requestLayout() will perform a layout pass in the future:

Requests a layout pass to be performed before the next scene is
rendered. This is batched up asynchronously to happen once per
"pulse", or frame of animation.

So requestLayout() informs the system that a layout needs to occur, but doesn't do the layout right away.

Usually, you don't need to call requestLayout() directly, because most of the time, the JavaFX system is able to internally determine that a region of the scene is dirty and needs to perform layout automatically when needed.

Alternative

The other option here is to override the layoutChildren() method of a parent node, which is "Invoked during the layout pass to layout the children in this Parent.". In doing so, it may also be necessary to override methods to computePrefHeight() as well as other computation methods for prefWidth and min & max height & width. A detailed description of using this approach is complicated and outside the scope of this answer.

How to get the actual size of the children during layout computation in JavaFX?

If you implement a custom Region (or indirect subclass), it's your classes' job to determine the size of the children.

computePrefWidth ect. need to consider the size bounds of the children and determine it's own preferred size based on those values. For these kinds of calculation you should use the minWidth, minHeight, prefWidth, prefHeight, maxWidth and maxHeight methods of Node.

Example

@Override
protected double computePrefWidth(double height) {
double size = 0;
for (Node child : getManagedChildren()) {
double pW = child.prefWidth(height);
if (pW > size) {
size = pW;
}
}

// TODO: consider insets ect...
return size;
}

The above way of determining the pref size would be suitable e.g. for something like a VBox.

JavaFX / FXML - Get Height of a VBox

There a several possible reasons, why getHeight() returns '0'.

  1. Your VBox doesn't contain any children.
  2. It's not in the SceneGraph yet.

VBox .getHeight() Not Changing until after .setOnAction Finishes

You can try adding a listener to the VBox's height property.

VBox vBoxContainer = new VBox();
vBoxContainer.setAlignment(Pos.CENTER);
vBoxContainer.setPrefSize(200, 200);

VBox vBox = new VBox();
vBox.setAlignment(Pos.CENTER);
vBoxContainer.getChildren().add(vBox);
vBox.heightProperty().addListener((observable, oldValue, newValue) -> {
System.out.println("Height changed to: " + newValue.doubleValue());
if(newValue.doubleValue() > 100)
{
//do something!
}
});
for (int i = 0; i < 5; i++) {
vBox.getChildren().add(new Label("newLabel"));
}
Button button = new Button("Add Label");
button.setOnAction(event -> {
vBox.getChildren().add(new Label("newLabel"));
});
Button checkButton = new Button("Print VBox Height");
checkButton.setOnAction(event -> System.out.println("VBox Height:" + vBox.getHeight()));

vBoxContainer.getChildren().addAll(button, checkButton);
stage.setScene(new Scene(vBoxContainer));
stage.show();

How to implement a JavaFX node so that it can return computed layoutBounds?

I found JDK-8156089 : Provide official API so subclasses can override layout bounds computation

So it seems I have to wait for an implementation of that feature request, which is not available as of Java 9.
Meanwhile a workaround is the following:

package stackoverflow.demo;

import java.lang.reflect.Field;

import javafx.application.Application;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

public class LayoutBoundsTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}

/**
* How to implement this component so that it can return
* its own computed layout bounds?
*/
public static class CenterPane extends StackPane
{
public CenterPane()
{
}

@Override
public boolean isResizable()
{
return false;
}

protected void addLayoutComponent( Node node )
{
Bounds bounds = node.getLayoutBounds();
setBounds( bounds );
getChildren().add( node );
}

/**
* Do it the hard way (and don't try that at home).
*/
private void setBounds( Bounds bounds )
{
try
{
// In Java 8 and 9 there's a private field in the
// Region that is lazy-initialized. We set this
// manually.
Field f = Region.class.getDeclaredField( "boundingBox" );
f.setAccessible( true );
f.set( this, bounds );
}
catch ( Exception e )
{
throw new RuntimeException( "No good.", e );
}
}
}

@Override
public void start(Stage stage) {
CenterPane centerPane = new CenterPane();
centerPane.addLayoutComponent( new Circle( 30, Color.YELLOW ) );

Node circle = new Circle( 30, Color.GREEN );

VBox root = new VBox();
root.getChildren().addAll(centerPane, circle);

Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Testing LayoutBounds");
stage.show();

// Both calls below should report the same layout bounds.
System.out.println("b0=" + centerPane.getLayoutBounds());
System.out.println("b2=" + circle.getLayoutBounds());
}
}


Related Topics



Leave a reply



Submit