Sunflame
Sunflame

Reputation: 3186

JavaFx: Same Label in two(or more) places in the same view

I don't know if I miss something, but I cannot manage to achieve that, I have the same label twice or more on the same view. I don't really want to duplicate it just use the same label with the same text/tooltip/style.

A simple example:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.control.Label?>
<AnchorPane xmlns="http://javafx.com/javafx"
            xmlns:fx="http://javafx.com/fxml"
            fx:controller="stackoverflow.dummy.Controller">
        <Label fx:id="myLabel"/>
        <!--<Label fx:id="myLabel"/>-->  ofc this doesn't work.
        <!--<Label fx:id="myLabel"/>-->
</AnchorPane>

If I try with <fx:reference> it appears only once, if I try with <fx:copy> it says I need a copy constructor, so I think I simply miss some easy solution. It should exist.

Note: I don't really want want to duplicate the code like: myLabel1, myLabel2, ..., since all of them are having the same text/tooltip/style.

I also know I can create a separate .fxml file where I build the label and use it via <fx:include> but I would prefer solving it in the same .fxml since it is a really simple thing I think it doesn't worth creating a new .fxml only for this.

Upvotes: 1

Views: 1699

Answers (2)

Slaw
Slaw

Reputation: 46116

Problem

It is not possible to add the same Label multiple times to the scene graph. This is documented by Node (emphasis mine):

A node may occur at most once anywhere in the scene graph. Specifically, a node must appear no more than once in all of the following: as the root node of a Scene, the children ObservableList of a Parent, or as the clip of a Node.

The scene graph must not have cycles. A cycle would exist if a node is an ancestor of itself in the tree, considering the Group content ObservableList, Parent children ObservableList, and Node clip relationships mentioned above.

If a program adds a child node to a Parent (including Group, Region, etc) and that node is already a child of a different Parent or the root of a Scene, the node is automatically (and silently) removed from its former parent. If a program attempts to modify the scene graph in any other way that violates the above rules, an exception is thrown, the modification attempt is ignored and the scene graph is restored to its previous state.

The emphasized part explains why the Label only appears once when using <fx:reference>.

Solution

The only solution to your problem is to duplicate the Label. There are at least two options you can use to accomplish this.

Use <fx:copy>

One way to do this by using <fx:copy> (emphasis mine):

The <fx:copy> element creates a copy of an existing element. Like <fx:reference>, it is used with the fx:id attribute or a script variable. The element's "source" attribute specifies the name of the object that will be copied. The source type must define a copy constructor that will be used to construct the copy from the source value.

At the moment, no JavaFX platform classes provide such a copy constructor, so this element is provided primarily for use by application developers. This may change in a future release.

As stated in the documentation, using <fx:copy> requires the class to have a copy constructor. The documentation also states that none of the core JavaFX classes provide copy constructors. This means you'll have to subclass Label and provide the needed constructor, as shown in funkyjelly's answer. To ensure the properties stay up-to-date you can bind the properties inside the constructor:

public class CopyableLabel extends Label {

  public CopyableLabel(CopyableLabel label) {
    // You only mentioned the text, tooltip, and style properties
    // in your question. Bind more properties as needed.
    textProperty().bind(label.textProperty());
    tooltipProperty().bind(label.tooltipProperty());
    styleProperty().bind(label.styleProperty());
  }

}

That way you only need to inject the "master" Label and any updates to it will be propagated to all the copies. Using this design you must keep in mind that bound properties cannot be set directly; trying to set a bound property will result in exceptions. You might want to document the constructor will bind the properties, not just copy the values.

Use Expression Bindings

Another option is to bind the needed properties in the FXML file. You would still be creating multiple Labels but you would only need to inject the "master" Label, as before.

<VBox xmlns="http://javafx.com/javafx/" xmlns:fx="http://javafx.com/fxml/1">

  <Label fx:id="master"/>
  <Label text="${master.text}" tooltip="${master.tooltip}" style="${master.style}"/>
  <Label text="${master.text}" tooltip="${master.tooltip}" style="${master.style}"/>
  <Label text="${master.text}" tooltip="${master.tooltip}" style="${master.style}"/>
  <!-- repeat as needed -->

</VBox>

This uses the expression binding capabilities of JavaFX FXML. This option may clutter the FXML file but it doesn't require you to create a subclass of Label.

Upvotes: 3

nullPointer
nullPointer

Reputation: 4574

"I don't really want want to duplicate the code like: myLabel1, myLabel2, ..., since all of them are having the same text/tooltip/style." What if you just create multiple references pointing to the same label object ?

Or extend Label to include a copy constructor :

public MyLabel(MyLabel aLabel) {
this.property1 = aLabel.property1;
this.property2 = aLabel.property2;
...
}

Then in FXML using copy:

<MyLabel fx:id="myLabel1"/>
<fx:copy source="myLabel1"/>

Upvotes: 0

Related Questions