Почему SceneBuilder так придирчив к пользовательским элементам управления?

Я надеялся, что кто-нибудь сможет объяснить мне, почему SceneBuilder настолько темпераментен, когда дело доходит до импорта пользовательских элементов управления.

Возьмем, к примеру, относительно простой пользовательский элемент управления (только в качестве примера):

public class HybridControl extends VBox{
    final private Controller ctrlr;
    public CustomComboBox(){
        this.ctrlr = this.Load();
    }

    private Controller Load(){
        final FXMLLoader loader = new FXMLLoader();
        loader.setRoot(this);
        loader.setClassLoader(this.getClass().getClassLoader());
        loader.setLocation(this.getClass().getResource("Hybrid.fxml"));
        try{
            final Object root = loader.load();
            assert root == this;
        } catch (IOException ex){
            throw new IllegalStateException(ex);
        }

        final Controller ctrlr = loader.getController();
        assert ctrlr != null;
        return ctrlr;
    }
    /*Custom Stuff Here*/
}

И тогда у вас есть класс Controller здесь:

public class Controller implements Initializable{
    /*FXML Variables Here*/
    @Override public void initialize(URL location, ResourceBundle resources){
        /*Initialization Stuff Here*/
    }
}

Это прекрасно работает. .jar прекрасно компилируется, SceneBuilder прекрасно читает .jar, он отлично импортирует элемент управления, и это здорово.

Меня раздражает то, что для этого требуется два отдельных класса, что не так уж важно, за исключением того, что я чувствую, что это должно быть выполнимо только с одним классом.

Теперь у меня это как указано выше, но я пробовал два других способа, которые оба терпят неудачу (SceneBuilder не найдет и не позволит мне импортировать элементы управления), и я надеялся, что кто-нибудь скажет мне, почему, чтобы я мог продолжать свою жизнь.

Во втором случае я попытался использовать один класс, который расширил VBox и реализовал Initializable:

public class Hybrid extends VBox implements Initializable{ /*In this case the FXML file Controller would be set to this class.*/
    /*FXML Variables Here*/
    public Hybrid(){
        this.Load();
    }
    private void Load(){
        final FXMLLoader loader = new FXMLLoader();
        loader.setRoot(this);
        loader.setClassLoader(this.getClass().getClassLoader());
        loader.setLocation(this.getClass().getResource("Hybrid.fxml"));
        try{
            final Object root = loader.load();
            assert root == this;
        } catch (IOException ex){
            throw new IllegalStateException(ex);
        }
        assert this == loader.getController();
    }
    @Override public void initialize(URL location, ResourceBundle resources){
        /*Initialization Stuff Here*/
    }

}

Это имеет ИДЕАЛЬНЫЙ смысл для меня. Это ДОЛЖНО работать, по крайней мере, в моей голове, но это не так. JAR-файл прекрасно компилируется, и я даже готов поспорить, что он будет отлично работать в программе, но когда я пытаюсь импортировать .jar в Scene Builder, он не работает. Его нет в списке импортируемых элементов управления.

Итак... Я пробовал кое-что другое. Я попытался вложить класс Controller в класс Control:

public class Hybrid extends VBox{ /*In this case the FXML Controller I had set to Package.Hybrid.Controller*/
    final private Controller ctrlr
    public Hybrid(){
        this.ctrlr = this.Load();
    }
    private Controller Load(){
        /*Load Code*/
    }
    public class Controller implements Initializable{
        /*Controller Code*/
    }
}

Это тоже не сработало. Я пробовал публичный, приватный, общедоступный статический, приватный статический, ни один из них не работал.

Так почему же это так? Почему SceneBuilder не может распознать пользовательский элемент управления, если класс Control и класс Controller не являются двумя отдельными объектами?

РЕДАКТИРОВАТЬ:

Благодаря James_D ниже я смог получить ответ И заставить пользовательские элементы управления работать так, как мне хотелось бы. Я также смог создать общий метод Load, который работает для всех пользовательских классов, если имя класса совпадает с именем файла FXML:

private void Load(){
    final FXMLLoader loader = new FXMLLoader();
    String[] classes = this.getClass().getTypeName().split("\\.");
    String loc = classes[classes.length - 1] + ".fxml";
    loader.setRoot(this);
    loader.setController(this);
    loader.setClassLoader(this.getClass().getClassLoader());
    loader.setLocation(this.getClass().getResource(loc));
    try{
        final Object root = loader.load();
        assert root == this;
    } catch (IOException ex){
        throw new IllegalStateException(ex);
    }
    assert this == loader.getController();
}

Просто подумал, что поделюсь этим. Обратите внимание, что это работает только в том случае, если, например, ваш класс был назван Hybrid, а ваш файл FXML был назван Hybrid.fxml.


person Will    schedule 07.10.2014    source источник


Ответы (1)


Ваша вторая (и третья) версия вообще не будет работать (SceneBuilder или без SceneBuilder), потому что утверждение

this == loader.getController()

не удастся. Когда вы вызываете loader.load(), FXMLLoader видит fx:controller="some.package.Hybrid" и создает новый экземпляр. Итак, теперь у вас есть два экземпляра класса Hybrid: тот, который вызывает load на FXMLLoader, и тот, который установлен в качестве контроллера загруженного FXML.

Вам нужно удалить атрибут fx:controller из файла FXML и установить контроллер непосредственно в коде, как в документация:

private void Load(){
    final FXMLLoader loader = new FXMLLoader();
    loader.setRoot(this);
    loader.setClassLoader(this.getClass().getClassLoader());
    loader.setLocation(this.getClass().getResource("Hybrid.fxml"));

    // add this line, and remove the fx:controller attribute from the fxml file:
    loader.setController(this);

    try{
        final Object root = loader.load();
        assert root == this;
    } catch (IOException ex){
        throw new IllegalStateException(ex);
    }
    assert this == loader.getController();
}

Экспериментируя со SceneBuilder, кажется, что он попытается создать пользовательские элементы управления, вызвав их конструктор без аргументов, и ожидает, что это завершится без создания исключения. Похоже, что он не может правильно обрабатывать ввод @FXML аннотированных значений в этом конкретном сценарии. Я бы порекомендовал для этого зарегистрировать ошибку на странице jira.

В качестве обходного пути вам, вероятно, придется написать свой код, чтобы выполнение конструктора без аргументов завершалось без создания исключения, даже если поля с аннотациями @FXML не вводятся. (Да, это боль.)

person James_D    schedule 08.10.2014
comment
Святое дерьмо... это сработало. хм вау. Я не могу в это поверить... Я думал, что пробовал это раньше, и это потерпело неудачу. На этот раз это не... вау, большое спасибо. А еще лучше SceneBuilder это увидел и подхватил. Серьезно сдулся. Это круто... - person Will; 08.10.2014
comment
Чтобы указать, мне не нужно было писать код, чтобы избежать генерации исключения. Я сделал то, что вы сказали, и удалил fx:controller = из FXML и поместил loader.setController(this) в метод Load. Это сработало. Это действительно сработало. - person Will; 08.10.2014