Issue
I've recently encountered problem, where I needed to get specific node (preferably by id) from nodes used in my window. Im using FXMLLoader, so first idea was to search tree structure FXMLLoader returns.
root|
|
|--some_container |- child1
| |- child2
|
|--other_container |-child3
...
Further research got me to method lookup in Scene class, but official documentation (https://docs.oracle.com/javase/8/javafx/api/javafx/scene/Scene.html#lookup-java.lang.String-) is a lacking description how this method works and is inaccurate in my opinion (I struggled quite a bit to make things work).
As mentioned here (How to find an element with an ID in JavaFX?) invocation of method lookup must be after method show, why?
Second question is why node id has to be preceded by '#' (hash sign)? while official documentation states it should be "...CSS selector..."? (Im a bit confused here, because class Node contains only one id-like property)
Edi1 - simple example for @kleopatra although problem isn't strictly code, its about understanding.
Example fxml file with scene contents:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.canvas.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.2" xmlns:fx="http://javafx.com/fxml/1" fx:controller="display.windows.ConsoleForBoard">
<children>
<SplitPane fx:id="mainSplitPane" dividerPositions="0.7" layoutX="277.0" layoutY="100.0" prefHeight="160.0" prefWidth="200.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<items>
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="398.0" prefWidth="404.0">
<children>
<SplitPane dividerPositions="0.8" layoutX="150.0" layoutY="53.0" orientation="VERTICAL" prefHeight="200.0" prefWidth="160.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<items>
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="100.0" prefWidth="160.0">
<children>
<Canvas fx:id="drawBoard" height="313.0" layoutX="125.0" layoutY="43.0" width="413.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
</children>
</AnchorPane>
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="100.0" prefWidth="160.0">
<children>
<TextArea layoutX="107.0" layoutY="-86.0" prefHeight="200.0" prefWidth="200.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
</children>
</AnchorPane>
</items>
</SplitPane>
</children>
</AnchorPane>
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="100.0">
<children>
<ToggleButton layoutX="35.0" layoutY="62.0" mnemonicParsing="false" text="ToggleButton" />
<TextField alignment="CENTER" layoutX="2.0" layoutY="14.0" promptText="Main Menu" text="Main Menu" />
</children>
</AnchorPane>
</items>
</SplitPane>
</children>
</AnchorPane>
Note: Canvas has fx:id="drawBoard"
example class that creates this scene:
public class BoardWithMenu extends Application {
Scene mainScene;
AnchorPane root;
FXMLLoader loader;
public static void start() {launch("start");}
@Override
public void start(Stage stage) throws Exception {
loader = new FXMLLoader(this.getClass().getResource("scene_definition.fxml"));
root = loader.load();
mainScene = new Scene(root, 500, 500);
stage.setScene(mainScene);
stage.setTitle("Space map");
stage.show();
Canvas board = (Canvas) mainScene.lookup("drawBoard");
System.out.println(board.getId());
}
}
Problem:
Line
Canvas board = (Canvas) mainScene.lookup("#drawBoard");
Cannot be invoked before
stage.show();
It will produce null pointer error. Why?
Solution
This answer is supplemental to kleopatra's answer.
Use Java variable references instead of lookups unless you really need lookups
My advice is to avoid the CSS-based lookup methods if you can (in favor of a direct typed reference in Java).
This advice holds particularly when using FXML. FXML has the ability to assign an fx:id
to a node, which can be mapped to a named, typed field in a Controller (using the @FXML
annotation). In general, that is a preferred method of referencing a node by ID when FXML is involved. It is preferred for numerous reasons:
- When using a Java variable reference rather than a lookup, you have typed information and compile-time checks, rather than untyped runtime checks.
- Lookups may be time-sensitive: correct results may require a layout pass and/or CSS application.
- During the layout pass and rendering phases, the JavaFX framework implementation for skins on controls may add or remove various nodes which would change the result of lookups.
- Using lookups you may find nodes that have been added to the scene by private skin implementations. By then writing code that relies on those found nodes, you break encapsulation. A later JavaFX release might change the private skin implementation which changes the structure or nodes which are added by the skin and then breaks your code.
The above points can make it easier to code errors when using lookups rather than relying on Java variable references.
Understand the difference between an id and an fx:id
Do not confuse an fx:id
used in FXML with a CSS ID, they are different things, though you might often want to assign them to be the same value, you can do that in the definition of the element in FXML, for example:
<Button fx:id="myButton" id="myButton">
The fx:id
value will be used to map to a variable reference in a controller which has an @FXML
annotation (or via reflection on public members if no annotation is provided):
@FXML Button myButton;
The id
value will map to the id stored in the node and associated with it. The associated documentation describes it like this:
The id of this Node. This simple string identifier is useful for finding a specific Node within the scene graph. While the id of a Node should be unique within the scene graph, this uniqueness is not enforced. This is analogous to the "id" attribute on an HTML element (CSS ID Specification).
Lookups are based on CSS selectors
Here is an example lookup by ID from the node lookup documentation:
- For example, if a Node is given the id of "myId", then the lookup method can be used to find this node as follows:
scene.lookup("#myId");
.
Lookups can be called on any node, or on the scene.
The node lookup(selector)
methods work based on the node id
, not the fx:id
because the lookup string is based on css. The lookup uses a css selector. So lookup based on an ID will have a #
prefix in front of the ID because that is the syntax for selecting a node via an ID. You aren't restricted to only looking up by ID. The CSS selector syntax is powerful and pretty much a language unto itself so you can have complicated expressions for querying the scene graph node tree and extracting matching nodes if you wish.
Be aware that skin implementations can modify the scene graph
If you do want to do a jquery style find (which is kind of what the JavaFX method lookup
methods are), then be super careful when and how you do it due to kleopatra's second point on nodes inserted by the skin:
- all nodes that are inserted by the skin are available only after the skin is attached, that is after showing (f.i. "#drawBoard" which is injected to the splitPane's items)
Also, note that skins can be applied in css via the fx-skin
attribute of Controls. So, even the application of CSS can change the skin, which can change the scene graph which can alter the results of a lookup.
How to ensure that expected nodes will be found on lookup
To ensure that all expected nodes are in the scene to be looked up and with the expected attributes applied (e.g. size, color), etc., before doing the lookup:
First, add the required nodes to a scene.
Second, do one of the following:
- Generate a layout pass OR
- show the related stage (which will cause a layout pass) OR
- Do the lookup in a
Platform.runLater()
block.- This will wait for the next pulse, so the layout for the current pulse is complete before execution. Though the
Platform.runLater()
specification doesn't guarantee this, practically, that is how it works.
- This will wait for the next pulse, so the layout for the current pulse is complete before execution. Though the
In most cases, my preferred solution would be to generate the layout pass.
To generate a layout pass:
- Add all required nodes to a scene with appropriate CSS styles AND
- Invoke the
applyCss()
andlayout()
methods on the scene root or an appropriate parent node.
Then invoke your desired lookup methods to find nodes.
Lookups without explicit layout passes (be careful)
If you know that the elements you want to lookup are already in the scene, then you can directly lookup()
the nodes and they will be found. For example, if you just added them to the scene explicitly in a prior piece of code, so you know that they don't need to be created by a Skin on a subsequent pulse.
However, the looked-up nodes may not return the correct initialized values for some attributes such as height or width until after a layout has been performed. Similarly, any attributes of the looked-up nodes which are set by CSS might be incorrect until CSS has been applied to the nodes. So for safety's sake me advice would be to err on the side of caution and ensure that layout and CSS are applied to the nodes you want to look up before you try to look them up.
Summary
As might be derived from this answer the use of the JavaFX lookup()
methods come with some warning and caveats. Which is the reason for the primary recommendation to avoid their use unless they add great benefit to your particular application.
And note, lookups may be of great benefit: they are very powerful, it is quite incredible what some jquery developers achieve using similar functionality, just be careful how you use them.
Answered By - jewelsea
Answer Checked By - Willingham (JavaFixing Volunteer)