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

时间:2015-12-11 16:27:46

标签: inheritance javafx java-8 fxml fxmlloader

我想将自定义JFX组件子类化以更改/扩展其行为。作为一个真实世界的例子,我想扩展一个具有编辑功能的数据查看器组件。

考虑以下非常小的情况。 使用班级Super非常有效。 但是当实例化子类Sub(在FXML文件中)时,FXMLLoader不会再注入@FXML字段label。 因此,在访问值为initialize的字段时,调用NullPointerException会导致null。我想FXMLLoader以某种方式需要信息来使用 Super.fxml 初始化Super Sub子对象。

请注意,initialize方法会在注入后由FXMLLoader自动调用。

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

label的可见性扩展到protected显然无法解决此问题。在fx:root中结合@DefaultProperty定义一个扩展点(此解决方案已被提议here)既没有效果。

我感谢任何帮助。

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

更新

就像在这个question中一样,前进的方式似乎是为每个继承级别(附加了FXML文件)调用FXMLLoader。问题归结为注入@FXML - 注释字段随后与调用initialize相关联。这意味着,如果我们想要注入字段,那么initialize会在每个load之后被调用。但是当initialize被每个子类覆盖时,最具体的实现被称为n次(其中n是继承级别的数量)。

这样的东西
public void initialize() {
    if (getClass() == THISCLASS) {
        realInitialize();
    }
}

[更新]而非[/更新] 解决此问题,但对我来说就像是黑客攻击。

@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在每次加载后发生,因此在每次非叶子初始化时都会发生NullPointerException

完全不使用initialize并自己调用某些init函数时,可以解决问题。但同样,这对我来说似乎非常黑客。

4 个答案:

答案 0 :(得分:0)

看起来你没有在 Sub.xml 中定义标签,这可能就是为什么没有注入label字段的原因。尝试更新 Sub.xml 以包含以下内容:

<?import Super?>

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

这有用吗?

问题是,当您实例化getClass()时,Super中对Sub.class的调用会返回Sub。所以它加载了 Sub.xml ,我想这不是你想要的(看起来你正试图加载 Super.xml Sub.xml )。您可以通过在Super构造函数中显式加载 Super.xml 并在Sub构造函数中显式加载 Sub.xml 来实现。

答案 1 :(得分:0)

我想我看到了问题。如果您未在setController()中致电Super(),则无法注入label,因此该字段仍为null。如果您确实在超级电话中调用了setController(),那么Sub的{​​{1}}实施会被调用两次 - 一次调用initialize()中的load()并再次调用Super()中对load()的调用。

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

答案 2 :(得分:0)

我知道这篇文章有点陈旧但是我遇到了同样的问题,最后找到了一个解决方案,用于在继承和在子级和父级中都有注入和属性时正确初始化父/子。这是我正在使用的简单架构:

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

要注意的重要部分是最低级别的子级是调用fxml加载的子级。这样就可以在fxml加载开始使用反射注入数据之前运行所有级别的构造函数。如果父级加载fxml,则子级尚未创建类属性,从而导致反射注入失败。对于在FXML中设置的属性也是如此。

答案 3 :(得分:0)

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

这就是您所需要的