【问题标题】:How to subclass custom JFX components with FXML correctly?如何正确使用 FXML 子类化自定义 JFX 组件?
【发布时间】:2015-12-11 16:27:45
【问题描述】:

我想继承自定义 JFX 组件以更改/扩展它们的行为。作为一个真实的例子,我想扩展一个具有编辑功能的数据查看器组件。

考虑以下非常小的场景。 使用 Super 类可以完美运行。 但是在实例化子类Sub(在FXML 文件中)时,FXMLLoader 不再注入@FXML 字段label。 因此,在访问值为null 的字段时,调用initialize 会导致NullPointerException。我想FXMLLoader 不知何故需要信息来初始化SubSuper 子对象,使用Super.fxml

请注意initialize 方法在注入后会被FXMLLoader 自动调用。

我知道将超级组件嵌套在子组件中应该可以正常工作,但我仍然想知道这是否可以使用继承。

label 的可见性扩大到protected 显然并没有解决这个问题。在fx:root 中定义扩展点并结合@DefaultProperty(已提出此解决方案here)均无效。

感谢您的帮助。

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();
    }
}

更新

就像在这个question 中一样,要走的路似乎是为每个继承级别调用FXMLLoader(附加了一个FXML 文件)。问题归结为注入@FXML-annotated 字段,然后连接到调用initialize。意思是,如果我们希望这些字段被注入,initialize 会在之后为每个 load 调用。但是当initialize 被每个子类覆盖时,最具体的实现会被调用n 次(其中n 是继承级别的数量)。

类似

public void initialize() {
    if (getClass() == THISCLASS) {
        realInitialize();
    }
}

[Update]不会[/Update]解决这个问题,但对我来说似乎是个黑客。

考虑@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 发生在每次加载之后,因此NullPointerExceptions 发生在每次非叶初始化时。

完全不使用initialize 并自己调用一些init 函数时可以解决此问题。但是,这对我来说似乎很老套。

【问题讨论】:

    标签: inheritance javafx java-8 fxml fxmlloader


    【解决方案1】:

    我想我看到了问题所在。如果在Super()中不调用setController(),则没有注入label的地方,所以字段仍然是null。如果你确实在 super 中调用了setController(),那么Subinitialize() 实现会被调用两次——一次是在Super() 中调用load(),然后是在Sub 中调用load() .

    理论上,只要您防范Sub 中的 NPE,这应该可以工作。如果Sub#initialize() 被调用并且button 仍然是null,那么您知道您正在为Super 进行初始化,您应该委托给super.initialize()。当button 不是null 时,您不会调用super

    【讨论】:

    • 没错,就是这样。可以想象几种有条件的守卫。像你建议的那样检查null。计算调用并仅在初始化匹配的继承级别时执行自己的初始化代码。根本不使用initialize,并在加载后在构造函数中调用私有初始化函数(根本没有if)。但是所有这些“解决方案”都是hacky。没有正确和干净的方法吗?
    • 每个级别的私有标志也可以工作,而且看起来不像是“黑客”。但理想情况下,指定特定类型初始化器的能力似乎会有所帮助。
    • 我认为继承对于 JavaFX UI 组件来说是很自然的事情。因此,手动控制初始化流程对我来说似乎是错误的。我仍然希望有一个更干净的方法......我会等待一段时间,如果没有更好的出现,我会接受你的回答。感谢您的帮助!
    • 作为FXMLLoader 的最初作者,我可以向您保证,这不是最初编写该类时考虑的用例。 :-) 但是,我已经很长时间没有接触过该代码了,因此有较新经验的人可能能够提供更多见解。
    • 看来你是对的。我接受了你的回答。感谢您的耐心等待!
    【解决方案2】:

    您似乎没有在 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 中对getClass() 的调用会返回Sub.class。所以它加载了 Sub.xml,我猜这不是你想要的(看起来你正在尝试同时加载 Super.xmlSub.xml )。您可以通过在 Super 构造函数中显式加载 Super.xml 并在 Sub 构造函数中显式加载 Sub.xml 来做到这一点。

    【讨论】:

    • 是的,将标签也添加到子类确实有效。但这是我想避免的。想象一下Super 是一个巨大的组件。然后我将不得不复制(=复制)整个 FXML 规范。是的,我想加载两个 fxml 文件。第一个 Super.fxml 用于 Super 子对象,然后 Sub.fxml 用于Sub 子对象。但是FXMLLoader 在每次加载后运行initialize。因此,一个不完全注入的对象会运行。
    • 如果只在子类中调用 setController() 会怎样?
    • javafx.fxml.LoadException: Root hasn't been set. Use method setRoot() before load。我认为始终需要有效的根对象。
    • 控制器,不是 root。
    • 意思是在基类中调用setRoot()但不要调用setController()。
    【解决方案3】:

    我知道这篇文章有点老了,但我遇到了同样的问题,最后找到了一个解决方案,可以在继承并在子级和父级中进行注入和属性时正确初始化父级/子级。这是我正在使用的简单架构:

    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 中设置的属性也是如此。

    【讨论】:

      【解决方案4】:

      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);
      }
      

      这就是你所需要的

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-01-16
        • 1970-01-01
        • 1970-01-01
        • 2013-07-21
        相关资源
        最近更新 更多