JavaFX FXML 包含 fxml 导致 NullPointerException

JavaFX FXML include fxml causes NullPointerException

我想将一个按钮提取到一个新的 fxml 文件中,并用它更改主标签。无需提取即可完美运行。
main.fxml:

<VBox xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1"
      fx:controller="org.example.MainController">
   <Label fx:id="label" text="default"/>
   <Button onAction="#changeLabel" text="sayHello" />
</VBox>

主控制器:

public class MainController {
    @FXML
    private Label label;

    @FXML
    private void changeLabel() {
        label.setText("Changed");
    }
}

通过提取,我在 MainController.changeLabel() 中得到 NullPointerException
main.fxml 包含:

<VBox xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1"
      fx:controller="org.example.MainController">
   <Label fx:id="label" text="default"/>
   <fx:include source="button.fxml"/>
</VBox>


button.fxml:

<AnchorPane xmlns="http://javafx.com/javafx/11.0.1"
            xmlns:fx="http://javafx.com/fxml/1"
            fx:controller="org.example.MainController">
    <Button onAction="#changeLabel" text="sayHello" />
</AnchorPane>

什么会导致此 NPE?

您应该(几乎?)始终为不同的 FXML 文件使用不同的 class 控制器。 (我能想到的唯一例外是,如果您想定义不同的 FXML 文件来表示相同控件的不同布局。)

一种方法是将包含的 FXML 的控制器(“嵌套控制器”)注入主控制器。 (参见 documentation。)

public class MainController {
    @FXML
    private Label label;

    @FXML
    private ButtonController buttonController ;

    @FXML
    private void initialize() {    
        buttonController.setOnButtonPressed(this::changeLabel);
    }

    private void changeLabel() {
        label.setText("Changed");
    }
}
public class ButtonController {

    private Runnable onButtonPressed ;

    public void setOnButtonPressed(Runnable onButtonPressed) {
        this.onButtonPressed = onButtonPressed ;
    }

    public Runnable getOnButtonPressed() {
        return onButtonPressed ;
    }

    @FXML
    private void changeLabel() {
        if (onButtonPressed != null) {
            onButtonPressed.run();
        }
    }
}

然后 FXML 文件看起来像

<VBox xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1"
      fx:controller="org.example.MainController">
   <Label fx:id="label" text="default"/>
   <fx:include fx:id="button" source="button.fxml"/>
</VBox>

<VBox xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1"
      fx:controller="org.example.ButtonController">
   <Label fx:id="label" text="default"/>
   <fx:include source="button.fxml"/>
</VBox>

一般来说,控制器之间相互引用是个坏主意,因为它会破坏封装并增加不必要的依赖关系。更好的方法是使用 MVC 设计。

public class Model {

    private final StringProperty text = new SimpleStringProperty() ;

    public StringProperty textProperty() {
        return text ;
    }

    public final String getText() {
        return textProperty().get();
    }

    public final void setText(String text) {
        textProperty().set(text);
    }
}

现在你可以做

public class MainController {
    @FXML
    private Label label;

    private final Model model ;

    public MainController(Model model) {
        this.model = model ;
    }

    @FXML
    private void initialize() {    
        label.textProperty().bind(model.textProperty());
    }

}

public class ButtonController {

    private final Model model ;


    public ButtonController(Model model) {
        this.model = model ;
    }

    @FXML
    private void changeLabel() {
        model.setText("Changed");
    }
}

FXML文件如上,加载FXML时需要指定一个controller factory(以便通过将模型实例传递给constructors来实例化controller):

final Model model = new Model();

FXMLLoader loader = new FXMLLoader(getClass().getResource("/path/to/main.fxml");
loader.setControllerFactory(type -> {
    if (type.equals(MainController.class))   return new MainController(model);
    if (type.equals(ButtonController.class)) return new ButtonController(model);
    throw new IllegalArgumentException("Unexpected controller type: "+type);
});
Parent root = loader.load();
// ...