Как правильно подклассифицировать пользовательские компоненты JFX с помощью FXML?

Я хотел бы создать подкласс пользовательских компонентов 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 и самому вызвать какую-либо функцию инициализации. Но опять же, это кажется мне очень хакерским.


person JD3    schedule 11.12.2015    source источник


Ответы (4)


Я думаю, что вижу проблему. Если вы не вызываете setController() в Super(), вводить label некуда, поэтому поле остается null. Если вы вызываете setController() в super, то реализация initialize() в Sub вызывается дважды: один раз при вызове load() в Super() и еще раз при вызове load() в Sub.

Теоретически это должно работать, пока вы защищаете от NPE в Sub. Если вызывается Sub#initialize(), а button по-прежнему null, вы знаете, что вы инициализируетесь для Super, и вам следует делегировать super.initialize(). Когда button не является null, вы не должны вызывать super.

person Greg Brown    schedule 12.12.2015
comment
Точно, такая ситуация. Можно представить себе несколько видов условных стражей. Проверка на null, как вы предложили. Подсчет вызовов и выполнение собственного кода инициализации только при инициализации соответствующего уровня наследования. Не использовать initialize вообще и вызывать приватные init-функции в конструкторе после загрузки (вообще без if). Но все эти решения хакерские. Разве нет правильного и чистого подхода к этому? - person JD3; 12.12.2015
comment
Частный флаг на каждом уровне также будет работать, и это не похоже на взлом. Но в идеале кажется, что возможность указать инициализатор для конкретного типа была бы полезной. - person Greg Brown; 12.12.2015
comment
Я думаю, что наследование является чем-то естественным для компонентов пользовательского интерфейса JavaFX. Следовательно, ручное управление потоком инициализации кажется мне неправильным. Я все еще надеюсь, что к этому есть более чистый подход ... Я подожду некоторое время, и если ничего лучше не появится, я приму ваш ответ. Спасибо за помощь! - person JD3; 12.12.2015
comment
Как первоначальный автор FXMLLoader, я могу заверить вас, что это не вариант использования, который рассматривался при первоначальном написании класса. :-) Однако я давно не прикасался к этому коду, поэтому кто-то с более свежим опытом работы с ним может предложить больше информации. - person Greg Brown; 12.12.2015
comment
Похоже, ты прав. Я принял твой ответ. Спасибо за ваше терпение! - person JD3; 15.12.2015

Не похоже, что вы определяете метку в 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.xml в конструкторе Super И явно загрузив Sub.xml в конструкторе Sub.

person Greg Brown    schedule 11.12.2015
comment
Да, добавление метки также к подклассу работает. Но именно этого я хотел бы избежать. Представьте, что Super — это огромный компонент. Тогда мне пришлось бы воспроизвести (= скопировать) всю спецификацию FXML. Да, я хотел бы загрузить оба файла fxml. Сначала Super.fxml для подобъекта Super, а затем Sub.fxml для подобъекта Sub. Но FXMLLoader запускает initialize после каждой загрузки. Следовательно, запускаются не полностью внедренные объекты. - person JD3; 11.12.2015
comment
Что, если вы вызываете setController() только в подклассе? - person Greg Brown; 12.12.2015
comment
javafx.fxml.LoadException: Root hasn't been set. Use method setRoot() before load. Я думаю, что всегда требуется действительный корневой объект. - person JD3; 12.12.2015
comment
Контроллер, не root. - person Greg Brown; 12.12.2015
comment
Это означает, что вызовите setRoot() в базовом классе, но не вызывайте setController(). - person Greg Brown; 12.12.2015
comment
Д'о. Теперь я проверил следующее: загрузите Super.fxml без контроллера, затем загрузите Sub.fxml с контроллером. Оба с одним корнем. При второй загрузке происходит NullPointerException. У меня такое чувство, особенно при чтении этой ссылки (stackoverflow.com/questions/31569299/), снова необходимо использовать какой-то другой механизм, такой как это расширение. - person JD3; 12.12.2015
comment
У вас есть доступ к исходному коду FXMLLoader? Любая идея, что вызывает NPE? - person Greg Brown; 12.12.2015
comment
Вы обновили конструкторы для загрузки явных файлов FXML (без использования имени класса)? - person Greg Brown; 12.12.2015
comment
Да, значение null — это поле метки, которое не было введено. См. тестовый код здесь paste.ofcode.org/344rbL888eLxVfk2vnDeJpq - person JD3; 12.12.2015
comment
Насколько я помню, initialize() следует вызывать только тогда, когда контроллер загрузчика не нулевой. Разве это не так? - person Greg Brown; 12.12.2015
comment
Да, но внедрение @FXML-полей также происходит только тогда, когда контроллер не нулевой. - person JD3; 12.12.2015
comment
Правильный. Вызывая только load() в листовом классе, вы должны быть уверены, что initialize() вызывается только один раз, а внедрение происходит только один раз. Пока конечный класс (т.е. Sub) устанавливает ненулевой контроллер, кажется, что это должно работать нормально. - person Greg Brown; 12.12.2015
comment
Да, но без настройки контроллера при загрузке Super.fxml @FXML записи туда не вводятся. В моем исходном примере это приводит к тому, что поле label становится null после завершения загрузки Sub.fxml. - person JD3; 12.12.2015
comment
Полный пример см. на странице paste.ofcode.org/7Wy2HaZf25axqMjF8Bv5Cw. Обратите внимание на комментарий в Super.java. - person JD3; 12.12.2015
comment
Да, никогда не выбирайте случайного хостера для своего кода. Кажется, удалить содержимое в течение часа. Я адаптировал вопрос, чтобы он содержал код. - person JD3; 12.12.2015
comment
Я видел это. Только что опубликовал новый ответ. - person Greg Brown; 12.12.2015

Я знаю, что этот пост немного устарел, но я столкнулся с той же проблемой и, наконец, нашел решение для правильной инициализации родителей/детей при наследовании и наличии инъекций и свойств как в дочернем, так и в родительском. Вот простая архитектура, которую я использую:

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.

person Flipbed    schedule 22.03.2017

Есть простой способ ответить 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);
}

Это все, что вам нужно

person Eric Chan    schedule 12.11.2018