Walking the JavaFX Scene Graph

Update

I am keeping this post up for historical reasons. But I warn you: It’s incomplete and might be deceptive!

Why? Because when I tested this, I only created a scene graph with a root node, but I never added the root to a Scene or showed the scene in a Stage. If you do this then everything changes. Most importantly: Parent.getChildrenUnmodifiable will return nodes that are created by the skin!

For example, the MenuBar will not return an emtpy list, but will return the HBox container used in the skin. The same is true for a ToolBar. The TabPane will return a TabPaneSkin$TabContentRegion node for every open Tab. And a TitledPane will give you TitledPaneSkin$1 and TitledPaneSkin$TitleRegion. A regular node like Button (which is also derived from Parent) will return the Text node used when you display a text (and probably the ImageViewif you show an image, but I haven’t tested this).

You can use the practice below if you have just loaded an FXML scene graph and have not yet added the result to a Scene (as is the case in our project).

So … continue at your own risk 😉

Walking the Scene Graph

In this post I want to explore how to walk through a JavaFX scene graph. Usually you will need this kind of processing, when you are searching for a specific node in your scene, i.e. with some userData or with a certain id.

If you look for information on this topic, you will find articles (i.e. Working with the JavaFX Scene Graph) which basically tell you, that:

  • Node ist the abstract base class for all scene graph nodes.
  • Parent is the abstract base class for all branch nodes.

Looking at the API you quickly find Parent.getChildrenUnmodifiable which will return a list of children of this Parent as a read-only list. This is a good starting point to implement the visitor pattern with something like this:

parent.getChildrenUnmodifiable().forEach(node -> visitor.visit(node));

The Tab – a Node or not?

When you look more closely at the members of the scene graph you discover that there is a class that does not follow this paradigm: The Tab. A Tab can be added to a TabPane which itself extends Parent but does not include the tabs in its children. One simple reason being, that the Tab is not derived from Node, but directly inherits from Object. They are related to some extent, as they share the Styleable interface with some common properties like id, style, styleClass and they also both offer getUserData and setUserData methods.

Since the Tab can have a Node as content it makes sense to visit this content when we are looking for some node deep down in an unknown hierarchie.

The MenuItem

Depending on your use case, you might also want to visit menu items when walking through the scene graph. In JavaFX menus can be used in two ways:

  1. A menu can be added to a MenuBar, which is itself an ancestor of Parent and can therefore be added anywhere in the scene
  2. Menu items can be added as a ContextMenu to any Control

The class MenuItem shares the same similarities to Node as the class Tab (see above) and it also directly extends Object, so you won’t find menu items or menus in the children list.

One Adapter to use them all

So let’s implement an adapter to cover these base classes and implement our own version of getChildrenUnmodifiable().

public class NodeAdapter {

    public static NodeAdapter adapt(Object fx) {
        if (fx instanceof Node) {
            return new NodeAdapter((Node)fx);
        } else if (fx instanceof MenuItem) {
            return new NodeAdapter((MenuItem)fx);
        } else if (fx instanceof Tab) {
            return new NodeAdapter((Tab)fx);
        }
        return null;
    }

    private Object fxObject;

    public NodeAdapter(Node node) {
        this.fxObject = node;
    }

    public NodeAdapter(MenuItem menuItem) {
        this.fxObject = menuItem;
    }

    public NodeAdapter(Tab tab) {
        this.fxObject = tab;
    }


    public List<NodeAdapter> getChildrenUnmodifiable() {
        ...
    }

    public void accept(INodeVisitor visitor) {
        boolean result = visitor.visit(this);
        if (!result)
            return;
        getChildrenUnmodifiable().forEach(nodeAdapter -> nodeAdapter.accept(visitor));
    }

}

This adapter can also be used to have a single entry point for things like using the idProperty or calling getUserData(). The source code with some implemented delegation methods can be found here.

Other exceptions to the rule

There are some classes that are ancestors of Parent but don’t expose their subnodes (panes or items) in getChildrenUnmodifiable:

  • Accordion => Accordion.getPanes()
  • SplitPane => SplitPane.getItems()
  • ToolBar => ToolBar.getItems()
  • ButtonBar => ButtonBar.getButtons()

And we also want to cover nodes that can only have one child as content when we walk the graph:

  • TitledPane => TitledPane.getContent()
  • ScrollPane => ScrollPane.getContent()

So the final method looks like this:

public List<NodeAdapter> getChildrenUnmodifiable() {
    List<NodeAdapter> result = new ArrayList<NodeAdapter>();

    // primary parent type derived from the root type
    if (fxObject instanceof Parent) {
        ((Parent)fxObject).getChildrenUnmodifiable().forEach(node -> result.add(new NodeAdapter(node)));
    } else if (fxObject instanceof Menu) {
        ((Menu)fxObject).getItems().forEach(item -> result.add(new NodeAdapter(item)));
    } else if (fxObject instanceof Tab) {
        Node content = ((Tab)fxObject).getContent();
        if (content != null)
            result.add(new NodeAdapter(content));
    } 

    // extended parent types
    if (fxObject instanceof MenuBar) {
        ((MenuBar)fxObject).getMenus().forEach(menu -> result.add(new NodeAdapter(menu)));
    } else if (fxObject instanceof TabPane) {
        ((TabPane)fxObject).getTabs().forEach(tab -> result.add(new NodeAdapter(tab)));
    } else if (fxObject instanceof TitledPane) {
        Node content = ((TitledPane)fxObject).getContent();
        if (content != null)
            result.add(new NodeAdapter(content));
    } else if (fxObject instanceof ScrollPane) {
        Node content = ((ScrollPane)fxObject).getContent();
        if (content != null)
            result.add(new NodeAdapter(content));
    } else if (fxObject instanceof Accordion) {
        ((Accordion)fxObject).getPanes().forEach(pane -> result.add(new NodeAdapter(pane)));
    } else if (fxObject instanceof SplitPane) {
        ((SplitPane)fxObject).getItems().forEach(item -> result.add(new NodeAdapter(item)));
    } else if (fxObject instanceof ToolBar) {
        ((ToolBar)fxObject).getItems().forEach(item -> result.add(new NodeAdapter(item)));
    } else if (fxObject instanceof ButtonBar) {
        ((ButtonBar)fxObject).getButtons().forEach(button -> result.add(new NodeAdapter(button)));
    }

    // context menu
    if (fxObject instanceof Control) {
        ContextMenu contextMenu = ((Control)fxObject).getContextMenu();
        if (contextMenu != null) {
            contextMenu.getItems().forEach(item -> result.add(new NodeAdapter(item)));
        }
    }

    return Collections.unmodifiableList(result);
}

Conclusion

This approach is obviously volatile. It depends on implementation details of JavaFX that might change. It also might need to be updated when new parent types are introduced. I’m not even sure I covered all of the possible angles! So, for most cases, I would not advise to use this in production level code, but to try to find a different approach to solve the problem. But of course, it might come in handy from time to time …

For anyone looking to extend this, here is the JUnit test code used to check the implementation based on 8u40: TestNodeAdapter

3 thoughts on “Walking the JavaFX Scene Graph

  1. Pingback: Java desktop links of the week, March 16 « Jonathan Giles

  2. Hallo Herr Keimel
    Habe gerade ihr Beispiel gesehen, ist mir äußerst hilfreich. Habe dazu aber eine Anmerkung: Ich denke ButtonBar mit getButtons() bräuchte ebenfalls eine separate Behandlung? :S
    Beste Grüsse

    • Hallo Herr Meyer, danke für den Kommentar. Sie haben vollkommen recht! Ich habe die ButtonBar mit ButtonBar.getButtons() in den Artikel aufgenommen.

Please leave a comment