【问题标题】:Why is SceneBuilder so particular about Custom Controls?为什么 SceneBuilder 对自定义控件如此特别?
【发布时间】:2014-12-02 08:18:18
【问题描述】:

我希望有人能够准确地向我解释为什么 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*/
    }
}

这也不起作用。我试过公共的、私有的、公共的静态的、私有的静态的,它们都不起作用。

那么为什么会这样呢?为什么除非 Control 类和 Controller 类是两个独立的实体,否则 SceneBuilder 无法识别自定义控件?

编辑:

感谢下面的 James_D,我能够得到答案并让自定义控件按照我想要的方式工作。如果类的名称与 FXML 文件的名称相同,我还能够创建适用于所有自定义类的通用 Load 方法:

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。

【问题讨论】:

    标签: java class javafx scenebuilder


    【解决方案1】:

    您的第二个(和第三个)版本根本不起作用(SceneBuilder 或没有 SceneBuilder),因为断言

    this == loader.getController()
    

    会失败。当您调用 loader.load() 时,FXMLLoader 会看到 fx:controller="some.package.Hybrid"创建它的新实例。所以现在你有两个Hybrid 类的实例:一个在FXMLLoader 上调用load,另一个被设置为加载的FXML 的控制器。

    您需要从 FXML 文件中删除 fx:controller 属性,并直接在您的代码中设置控制器,如 documentation

    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-annotated 字段,也可以在不引发异常的情况下完成无参数构造函数的执行。 (是的,这很痛苦。)

    【讨论】:

    • 天哪……它奏效了。嗯哇。我简直不敢相信……我以为我以前尝试过,但失败了。这一次它没有......哇主要的荣誉。甚至更好的 SceneBuilder 看到它并把它捡起来。严重被吹走了。这太棒了...
    • 要指定,我不必编写代码来避免抛出异常。我按照您所说的做了,并从 FXML 中删除了 fx:controller = "" 并将 loader.setController(this) 放入 Load 方法中。有效。真的,真的很管用。
    猜你喜欢
    • 2014-08-21
    • 2016-08-21
    • 2015-12-08
    • 1970-01-01
    • 2011-07-12
    • 2021-08-17
    • 2010-11-22
    • 2010-10-22
    相关资源
    最近更新 更多