如何正确地将 @DefaultProperty 注释添加到使用 @NamedArg 的自定义 class

How to properly add @DefaultProperty annotation to custom class that uses @NamedArg

我创建了一个自定义 class ImageButton 来删除一些样板代码

open class ImageButton(@NamedArg("image") image: Image,
                       @NamedArg("tooltipText") tooltipText: String,
                       @NamedArg("width") width: Double, 
                       @NamedArg("height") height: Double) : Button() {
    init {
        prefWidth = width
        minWidth = NEGATIVE_INFINITY
        maxWidth = NEGATIVE_INFINITY

        prefHeight = height
        minHeight = NEGATIVE_INFINITY
        maxHeight = NEGATIVE_INFINITY

        cursor = ImageCursor.HAND
        effect = ImageInput(image)
        tooltip = Tooltip(tooltipText)
    }
}

现在,而不是这个:

<Button fx:id="deleteButton" prefWidth="32.0" prefHeight="32.0" 
        minHeight="-Infinity" maxHeight="-Infinity"
        minWidth="-Infinity" maxWidth="-Infinity" 
        onMouseClicked="#deleteThePiece">
    <cursor>
        <ImageCursor fx:constant="HAND"/>
    </cursor>
    <tooltip>
        <Tooltip text="Delete Current Piece"/>
    </tooltip>
    <effect>
        <ImageInput>
            <source>
                <Image url="@/icons/delete.png"/>
            </source>
        </ImageInput>
    </effect>
</Button>

我可以这样写:

<ImageButton fx:id="deleteButton" width="32.0" height="32.0"
        onMouseClicked="#deleteThePiece" tooltipText="Delete Current Piece">
    <image>
        <Image url="@/icons/dice.png"/>
    </image>
</ImageButton>

不过,我希望能够进一步缩短它,像这样:

<ImageButton fx:id="deleteButton" width="32.0" height="32.0"
        onMouseClicked="#deleteThePiece" tooltipText="Delete Current Piece">
    <Image url="@/icons/dice.png"/>
</ImageButton>

我有很多这样的对象,所以如果能够使 fxml 标签尽可能短就好了。

我知道有一个注解 @DefaultProperty 可用于解包默认标签(例如,您可以省略 <Pane> 标签内的 <children> 标签,因为它具有@DefaultProperty("children")注解)所以我用了:

@DefaultProperty("image")
open class ImageButton(...) {...}

但是当加载 fxml 文件时出现以下错误:

javafx.fxml.LoadException: Element does not define a default property.

我做了一些研究并发现了这个:

但是,它不包含解决方案。它只是说明了问题。

所以我的问题是:

是否可以在使用 @NamedArg 注释的自定义 class 上使用 @DefaultProperty 注释?

如果是,我该如何实现?

如果不是,我是否应该尝试以不同的方式构造我的 ImageButton 对象?例如使用 <fx:factory>?

这里有几个选项,但@Slaw 在评论中指出了更简单的选项:

Use a String URL in the constructor instead of the Image

所以这应该有效:

public ImageButton(@NamedArg("url") String url, @NamedArg("text") String text) {
    setEffect(new ImageInput(new Image(url)));
    setText(text);
}

FXML 如:

<ImageButton fx:id="imageButton" text="Click Me!" url="@icon.png"/>

现在让我们探讨 <image />@DefaultProperty 的结合使用。

ImageButton 控件

首先,让我们定义我们的控件。为了简单起见(也因为这些不能被覆盖),我不会包括 widthheight:

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);
    }
}

并且:

<ImageButton fx:id="imageButton" text="Click Me!">
     <image>
         <Image url="@icon.png"/>
     </image>
</ImageButton>

将完美运行。但是,目的是删除 <image> 标记。

默认属性

理论说你可以做到:

@DefaultProperty("image")
public class ImageButton extends Button {
...
}

<ImageButton fx:id="imageButton" text="Click Me!">
     <Image url="@icon.png"/>
</ImageButton>

但是抛出异常:

Caused by: javafx.fxml.LoadException: Element does not define a default property.

有关更多详细信息,为什么会发生此异常,请参阅链接

基本上,正如评论中所讨论的,@DefaultProperty@NamedArg 不能一起工作:为了扩展给定 class、@NamedArg 的 FXML 属性为此 class 提供新的构造函数,这需要使用 ProxyBuilder,因此 FXMLLoader 将使用 ProxyBuilder 的实例,而这些不包括 @DefaultProperty注解.

建设者

尽管在 JavaFX 2.0 中使用了构建器设计模式,但很久以前就已弃用(在 Java 8 中,在 Java 9 中删除,link), 当前 JavaFX 代码中还有一些 builders.

事实上,FXMLLoader 使用 JavaFXBuilderFactory 作为默认的构建器工厂,如果在 [=151] 中找到 NamedArg 注释,它将调用此 ProxyBuilder =] 构造函数,以及 JavaFXImageBuilder.

等其他构建器

有一些关于建造者的描述here

生成器实现

我们如何添加自己的构建器工厂? FXMLLoader 有办法:setBuilderFactory.

我们可以延长 JavaFXBuilderFactory 吗?不,它是最终的,我们不能扩展它,我们必须从头开始创建一个。

ImageButtonBuilderFactory

让我们创建它:

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;
    }
}

现在让我们添加构建器:

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);
    }

}

请注意 ImageButton 与上面的 class 相同(没有 DefaultProperty 注释)。

使用自定义生成器

现在我们可以使用自定义构建器了:

FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource("imagebutton.fxml"));
loader.setBuilderFactory(new ImageButtonBuilderFactory());
Parent root = loader.load();
Scene scene = new Scene(root);
...

FXML 所在位置:

<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>

如果我们 运行 现在这样做,它应该可以工作。我们已经验证了我们的新生成器可以正常工作。如果我们注释掉 setBuilderFactory 调用,它也会起作用(使用 NamedArgProxyBuilder)。对于自定义构建器工厂,它不会使用 ProxyBuilder,而是我们的自定义构建器。

最后一步

最后,我们可以利用 DefaultProperty 去掉 <image> 标签。

并且我们会将注释添加到构建器 class,而不是控件!

所以现在我们有:


@DefaultProperty("image")
public class ImageButtonBuilder extends AbstractMap<String, Object> implements Builder<ImageButton> {
...
}

最后我们可以从 FXML 文件中删除 <image> 标签:

<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>

它会起作用的!