我想将自定义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函数时,可以解决问题。但同样,这对我来说似乎非常黑客。
答案 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);
}
这就是您所需要的