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 ImageView
if 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:
- A menu can be added to a
MenuBar
, which is itself an ancestor ofParent
and can therefore be added anywhere in the scene - Menu items can be added as a
ContextMenu
to anyControl
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