如何使用 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
不知何故需要信息来初始化 Sub
的 Super
子对象,使用 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
发生在每次加载之后,因此 NullPointerException
s 发生在每个非叶初始化上。
完全不使用 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.xml 和 Sub.xml)。您可以通过在 Super
构造函数中显式加载 Super.xml 并显式加载 Sub.xml[=31= 来做到这一点] 在 Sub
构造函数中。
我想我明白问题所在了。如果不在Super()
中调用setController()
,就没有地方可以注入label
,所以该字段仍然是null
。如果你在 super 中调用 setController()
,那么 Sub
的 initialize()
实现会被调用两次——一次是在 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);
}
这就是你所需要的
我想子class 自定义 JFX 组件以 change/extend 它们的行为。作为一个真实世界的例子,我想扩展一个具有编辑功能的数据查看器组件。
考虑以下非常小的场景。
使用 class Super
效果很好。
但是当实例化 subclass Sub
(在 FXML 文件中)时,FXMLLoader
不再注入 @FXML
字段 label
。
因此,在访问值为 null
的字段时,调用 initialize
会导致 NullPointerException
。我想 FXMLLoader
不知何故需要信息来初始化 Sub
的 Super
子对象,使用 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
发生在每次加载之后,因此 NullPointerException
s 发生在每个非叶初始化上。
完全不使用 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.xml 和 Sub.xml)。您可以通过在 Super
构造函数中显式加载 Super.xml 并显式加载 Sub.xml[=31= 来做到这一点] 在 Sub
构造函数中。
我想我明白问题所在了。如果不在Super()
中调用setController()
,就没有地方可以注入label
,所以该字段仍然是null
。如果你在 super 中调用 setController()
,那么 Sub
的 initialize()
实现会被调用两次——一次是在 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);
}
这就是你所需要的