Я хотел бы создать подкласс пользовательских компонентов JFX, чтобы изменить/расширить их поведение. В качестве реального примера я хотел бы расширить компонент просмотра данных с помощью функций редактирования.
Рассмотрим следующий очень минимальный сценарий. Использование класса Super
работает отлично. Но при создании экземпляра подкласса Sub
(в файле FXML) FXMLLoader
больше не вводит поле @FXML
label
. Поэтому вызов initialize
приводит к NullPointerException
при доступе к полю со значением null
. Я полагаю, что FXMLLoader
каким-то образом нужна информация для инициализации подобъекта Super
объекта Sub
с помощью 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
переопределяется каждым подклассом, наиболее конкретная реализация вызывается n
раз (где n
— количество уровней наследования).
Что-то типа
public void initialize() {
if (getClass() == THISCLASS) {
realInitialize();
}
}
[Update]not[/Update] решит эту проблему, но мне кажется, что это хак.
Рассмотрим этот демонстрационный код от @mrak, который показывает загрузку на каждом уровне наследования. Когда мы реализуем методы initialize
на обоих уровнях, возникает проблема, описанная выше.
Вот более полный минимальный рабочий пример, основанный на коде mraks.
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
и самому вызвать какую-либо функцию инициализации. Но опять же, это кажется мне очень хакерским.