mentics
mentics

Reputation: 6999

How to reliably get the Bounds a JavaFX Label or ToolBar?

Example code below. This outputs all zeros for everything. For ToolBar, it also shows zeros. For Text, it outputs non-zero width and height. Is this a bug on Label, or deliberate? Am I supposed to do something to get Label to have its bounds? I have tried other threads, moving, various things.

Note: all the Nodes do show up as expected. It's just the getting of the Bounds that isn't working.

I saw something about maybe using a listener on the boundsInParent property. When I try that, the numbers bounce around some across many events, so there doesn't appear to be a way to figure out when I would have the right values.

import javafx.application.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.*;


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

    public void start(Stage stage) {
        Pane root = new Pane();
        Group g = new Group();
        root.getChildren().add(g);

        Scene scene = new Scene(root, 500, 500);
        stage.setScene(scene);
        stage.show();

        Platform.runLater(() -> {
//            Text text = new Text("test label");
//            ToolBar text = new ToolBar(new Button("test label"));
            Label text = new Label("test label");
            g.getChildren().add(text);
            System.out.println(text.getBoundsInParent());
            System.out.println(text.getBoundsInLocal());
            System.out.println(text.getLayoutBounds());
        });
    }
}

@sarcan's answer is a good start, but it's incomplete. Here is an example:

package other;

import javafx.application.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.*;


public class Main extends Application {

    @Override
    public void start(Stage stage) {
        Pane root = new Pane();
        final Group g = new Group();
        root.getChildren().add(g);

        Scene scene = new Scene(root, 500, 500);
        stage.setScene(scene);
        stage.show();

        Platform.runLater(() -> {
            final Label text = new Label("test label");
            g.getChildren().add(text);
            new Thread(() -> {
                Platform.runLater(() -> {
                    System.out.println("text.getWidth() = " + text.getWidth());
                    System.out.println(text.getBoundsInParent());
                    System.out.println(text.getBoundsInLocal());
                    System.out.println(text.getLayoutBounds());
                });
            }).start();
        });
    }

    public static void main(String[] args) {
        launch(args);
    }
}

You can remove the new Thread() part and it will still show the problem. I threw that in there just to show I was really trying to "defer" it as was recommended. The above example uses the runLater as recommended, but still demonstrates the problem. The bounds are all output as 0's.

The above example is very common--adding a Node when already in the FX thread and needing access to its bounds. But how does one reliably defer so you can get those bounds after the scene has been updated? Platform.runLater apparently does not do it.

Upvotes: 2

Views: 2874

Answers (1)

sarcan
sarcan

Reputation: 3165

The problem here is that you add the text to its parent, then immediately query its coordinates. The way JavaFX and most other toolkits work is a bit different from what you seem to expect: When you add the label, the scene graph is not updated immediately. Instead, it is marked as 'dirty'. When the JavaFX application thread exists your block, it will then perform the update, including laying out your group and setting the label's bounds accordingly.

There's a simple reason for doing it this way. If JavaFX would update the scene graph immediately, it would not only be wasteful, but also lead to strange results. Assume you're adding 20 labels, which trigger 20 immediate updates. As those would happen very fast, the ui would flicker. Instead, what happens is you add 20 labels which causes the ui to be marked as dirty 20 times (which is very cheap), and then all your changes are applied in a single update. Think of your runLater block as 'describing to JavaFX what you would like the ui to look like after the next atomic update'.

Now, that being said, if you want to get the bounds of your text, simply give JavaFX a chance for an update first. If you, for instance, move the addition of your text in front of the runLater block, the text will be added, the scenegraph will be updated and then you'll query your coordinates - which will yield proper results:

public void start(Stage stage) {
    Pane root = new Pane();
    final Group g = new Group();
    final Label text = new Label("test label");
    g.getChildren().add(text);
    root.getChildren().add(g);

    Scene scene = new Scene(root, 500, 500);
    stage.setScene(scene);
    stage.show();

    Platform.runLater(new Runnable() {
        public void run() {
            System.out.println("text.getWidth() = " + text.getWidth());
            System.out.println(text.getBoundsInParent());
            System.out.println(text.getBoundsInLocal());
            System.out.println(text.getLayoutBounds());
        }
    });
}

Upvotes: 7

Related Questions