如何使用 FXML 正确地子类化自定义 JFX 组件?

How to subclass custom JFX components with FXML correctly?

我想子class 自定义 JFX 组件以 change/extend 它们的行为。作为一个真实世界的例子,我想扩展一个具有编辑功能的数据查看器组件。

考虑以下非常小的场景。 使用 class Super 效果很好。 但是当实例化 subclass Sub(在 FXML 文件中)时,FXMLLoader 不再注入 @FXML 字段 label。 因此,在访问值为 null 的字段时,调用 initialize 会导致 NullPointerException。我想 FXMLLoader 不知何故需要信息来初始化 SubSuper 子对象,使用 Super.fxml.

请注意方法 initialize 在注入后被 FXMLLoader 自动调用。

我知道将超级组件嵌套在子组件中应该可以正常工作,但我仍然想知道是否可以使用继承。

label 的可见性扩大到 protected 显然没有解决这个问题。在 fx:root 中结合 @DefaultProperty 定义扩展点(已提出此解决方案 ),但均无效。

感谢任何帮助。

fxml/Super.fxml

<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.*?>

<fx:root xmlns:fx="http://javafx.com/fxml/1" type="HBox">
    <Label fx:id="label"/>
</fx:root>

Super.java

import java.io.IOException;

import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;

public class Super extends HBox {

    @FXML
    protected Label label;

    public Super() {
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/" + getClass().getSimpleName() + ".fxml"));
        fxmlLoader.setRoot(this);
        fxmlLoader.setController(this);

        try {
            fxmlLoader.load();
        } catch (IOException exception) {
            throw new RuntimeException(exception);
        }
    }

    public void initialize() {
        label.setText("Super");
    }
}

fxml/Sub.fxml

<?import test.Super?>

<fx:root xmlns:fx="http://javafx.com/fxml/1" type="Super"></fx:root>

Sub.java

public class Sub extends Super {
    public Sub() {
        super();
    }
}

更新

就像在这个 中一样,要走的路似乎是为每个继承级别调用 FXMLLoader(附加了一个 FXML 文件)。问题归结为注入 @FXML 注释的字段被连接到调用 initialize 之后。意思是,如果我们希望字段被注入,initialize 之后会为每个 load 调用。但是当 initialize 被每个子 class 覆盖时,最具体的实现被调用 n 次(其中 n 是继承级别的数量)。

类似

public void initialize() {
    if (getClass() == THISCLASS) {
        realInitialize();
    }
}

[Update]不会[/Update] 解决这个问题,但对我来说似乎是 hack。

考虑一下@mrak 的 demo code,它显示了每个继承级别上的加载。当我们在两个级别中实现 initialize 方法时,就会出现上述问题。


这里是一个更完整的最小工作示例,基于 mraks code

Super.java

package test;

import java.io.IOException;
import java.net.URL;

import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;

public class Super extends HBox {

    @FXML
    private Label label;

    public Super() {
        super();
        loadFxml(Super.class.getResource("/fxml/Super.fxml"), this, Super.class);
    }

    public void initialize() {
        label.setText("initialized");
    }

    protected static void loadFxml(URL fxmlFile, Object rootController, Class<?> clazz) {
        FXMLLoader loader = new FXMLLoader(fxmlFile);
        if (clazz == rootController.getClass()) { // PROBLEM
            loader.setController(rootController);
        }
        loader.setRoot(rootController);
        try {
            loader.load();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

Sub.java

package test;

import javafx.fxml.FXML;
import javafx.scene.control.Button;

public class Sub extends Super {

    @FXML
    private Button button;

    public Sub() {
        super();
        loadFxml(Sub.class.getResource("/fxml/Sub.fxml"), this, Sub.class);
    }

    @Override
    public void initialize() {
        super.initialize();
        button.setText("initialized");
    }

}

Super.fxml

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

<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.*?>

<fx:root xmlns:fx="http://javafx.com/fxml/1" type="HBox">
    <Label fx:id="label" text="not initialized"/>
</fx:root>

Sub.fxml

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

<?import javafx.scene.control.*?>
<?import test.Super?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Button?>

<fx:root xmlns:fx="http://javafx.com/fxml/1" type="Super">
    <Button fx:id="button" text="not initialized"/>
</fx:root>

请参阅 Super.loadFxml 中的注释行。使用此条件会导致仅在叶中注入 @FXML 条目。但是 initialize 只被调用一次。不使用此条件会导致(理论上)注入所有 @FXML 条目。但是 initialize 发生在每次加载之后,因此 NullPointerExceptions 发生在每个非叶初始化上。

完全不使用 initialize 并自己调用一些初始化函数时,问题可以得到解决。但是,这对我来说似乎很老套。

您似乎没有在 Sub.xml 中定义标签,这可能是 label 字段中未注入任何内容的原因。尝试更新 Sub.xml 以包含以下内容:

<?import Super?>

<fx:root xmlns:fx="http://javafx.com/fxml/1" type="Super">
    <Label fx:id="label"/>
</fx:root>

行得通吗?

问题是当您实例化 Sub 时,在 Super returns Sub.class 中调用 getClass()。所以它加载 Sub.xml,我猜这不是你想要的(看起来你正在尝试同时加载 Super.xmlSub.xml)。您可以通过在 Super 构造函数中显式加载 Super.xml 并显式加载 Sub.xml[=31= 来做到这一点] 在 Sub 构造函数中。

我想我明白问题所在了。如果不在Super()中调用setController(),就没有地方可以注入label,所以该字段仍然是null。如果你在 super 中调用 setController(),那么 Subinitialize() 实现会被调用两次——一次是在 Super() 中调用 load(),另一次是Sub.

中对 load() 的调用

理论上,只要你在 Sub 中防范 NPE,这应该有效。如果 Sub#initialize() 被调用并且 button 仍然是 null,您知道您正在为 Super 初始化并且您应该委托给 super.initialize()。当 button 是非 null 时,你不会调用 super.

我知道这个 post 有点旧,但我 运行 遇到了同样的问题,并最终找到了在继承和同时具有注入和属性时正确初始化 parents/children 的解决方案child 和 parent。这是我使用的简单架构:

public class Parent extends HBox {

    @FXML
    private Label labelThatIsInBothFXMLs;

    public Parent() {
        this(true);
    }

    protected Parent(boolean doLoadFxml) {
        if (doLoadFxml) {
            loadFxml(Parent.class.getResource(...));
        }
    } 

    protected void loadFxml(URL fxmlFile) {
        FXMLLoader loader .... //Load the file
    }

    @Initialize
    protected void initialize() {
        // Do parent initialization.
        labelThatIsInBothFXMLs.setText("Works!");
    }

}

public class Child extends Parent {

    @FXML
    private Label labelOnlyInChildFXML;

    public Child() {
        super(false);
        loadFxml(Child.class.getResource(...));
    }

    @Override
    protected void initialize() {
        super.initialize();
        // Do child initialization.
        labelOnlyInChildFXML.setText("Works here too!");
    }
}

需要注意的重要部分是最低级别 child 是调用 fxml 加载的级别。这是为了在 fxml 加载开始使用反射注入数据之前,所有级别的构造函数都是 运行。如果 parent 加载 fxml,则 child 尚未创建 class 属性,导致反射注入失败。 FXML 中设置的属性也是如此。

Flipbed的回答有一个简单的方法

public class Super extends HBox {

@FXML
private Label label;

public Super() {
    super();
    if(getClass() == Super.class)
        loadFxml(Super.class.getResource("/fxml/Super.fxml"), this, Super.class);
}

这就是你所需要的