There are a few options here, but the more simple one is stated by @Slaw in a comment:
Use a String URL in the constructor instead of the Image
So this should work:
public ImageButton(@NamedArg("url") String url, @NamedArg("text") String text) {
setEffect(new ImageInput(new Image(url)));
setText(text);
}
with the FXML like:
<ImageButton fx:id="imageButton" text="Click Me!" url="@icon.png"/>
Let's explore now the use of <image />
combined with @DefaultProperty
.
ImageButton control
First of all, let's define our control. For the sake of simplicity (and also because these can't be overridden), I won't include width
and height
:
public class ImageButton extends Button {
public ImageButton(@NamedArg("image") Image image, @NamedArg("text") String text) {
setImage(image);
setText(text);
}
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image") {
@Override
protected void invalidated() {
// setGraphic(new ImageView(get()));
setEffect(new ImageInput(get()));
}
};
public final ObjectProperty<Image> imageProperty() {
return image;
}
public final Image getImage() {
return image.get();
}
public final void setImage(Image value) {
image.set(value);
}
}
And:
<ImageButton fx:id="imageButton" text="Click Me!">
<image>
<Image url="@icon.png"/>
</image>
</ImageButton>
will work perfectly fine. However, the purpose is to remove the <image>
tag.
DefaultProperty
The theory says you could do:
@DefaultProperty("image")
public class ImageButton extends Button {
...
}
and
<ImageButton fx:id="imageButton" text="Click Me!">
<Image url="@icon.png"/>
</ImageButton>
However, an exception is thrown:
Caused by: javafx.fxml.LoadException: Element does not define a default property.
For more details, why this exception happens, see the linked question.
Basically, as discussed within comments, @DefaultProperty
and @NamedArg
don't work together: In order to extend the FXML attributes of a given class, @NamedArg
provide new constructors to this class, which require the use of ProxyBuilder
, so FXMLLoader
will use instances of ProxyBuilder
instead, and these don't have included the @DefaultProperty
annotation.
Builders
Though builder design pattern was used in JavaFX 2.0, and it was deprecated a long time ago (in Java 8, removed in Java 9, link), there are still some builders in the current JavaFX code.
In fact, FXMLLoader
makes use of JavaFXBuilderFactory
, as default builder factory, that will call this ProxyBuilder
if NamedArg
annotations are found in the class constructor, among other builders like JavaFXImageBuilder
.
There is some description about builders here.
Builder implementation
How can we add our own builder factory? FXMLLoader
has a way: setBuilderFactory
.
Can we extend JavaFXBuilderFactory
? No, it's final, we can't extend it, we have to create one from the scratch.
ImageButtonBuilderFactory
Let's create it:
import javafx.util.Builder;
import javafx.util.BuilderFactory;
public class ImageButtonBuilderFactory implements BuilderFactory {
@Override
public Builder<?> getBuilder(Class<?> type) {
if (type == null) {
throw new NullPointerException();
}
if (type == ImageButton.class) {
return ImageButtonBuilder.create();
}
return null;
}
}
Now let's add the builder:
ImageButtonBuilder
import javafx.scene.image.Image;
import javafx.util.Builder;
import java.util.AbstractMap;
import java.util.HashSet;
import java.util.Set;
public class ImageButtonBuilder extends AbstractMap<String, Object> implements Builder<ImageButton> {
private String text = "";
private Image image;
private ImageButtonBuilder() {}
@Override
public Set<Entry<String, Object>> entrySet() {
return new HashSet<>();
}
public static ImageButtonBuilder create() {
return new ImageButtonBuilder();
}
@Override
public Object put(String key, Object value) {
if (value != null) {
String str = value.toString();
if ("image".equals(key)) {
image = (Image) value;
} else if ("text".equals(key)) {
text = str;
} else {
throw new IllegalArgumentException("Unknown property: " + key);
}
}
return null;
}
@Override
public ImageButton build() {
return new ImageButton(image, text);
}
}
Note that ImageButton
is the same class as above (without DefaultProperty
annotation).
Using the custom builder
Now we could use our custom builder:
FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource("imagebutton.fxml"));
loader.setBuilderFactory(new ImageButtonBuilderFactory());
Parent root = loader.load();
Scene scene = new Scene(root);
...
where the FXML is:
<StackPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11" xmlns:fx="http://javafx.com/fxml/1">
<ImageButton fx:id="imageButton" text="Click Me!">
<image>
<Image url="@icon.png"/>
</image>
</ImageButton>
</StackPane>
If we run this now, it should work. We have verified that our new builder works. If we comment out the setBuilderFactory
call, it will work as well (using NamedArg
and ProxyBuilder
). With the custom builder factory, it won't use ProxyBuilder
but our custom builder.
Final step
Finally, we can make use of DefaultProperty
to get rid of the <image>
tag.
And we'll add the annotation to the builder class, not to the control!
So now we have:
@DefaultProperty("image")
public class ImageButtonBuilder extends AbstractMap<String, Object> implements Builder<ImageButton> {
...
}
and finally we can remove the <image>
tag from the FXML file:
<StackPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11" xmlns:fx="http://javafx.com/fxml/1">
<ImageButton fx:id="imageButton" text="Click Me!">
<Image url="@icon.png"/>
</ImageButton>
</StackPane>
and it will work!