如何在运行时定义fx:include'd表单的控制器

时间:2018-02-07 03:41:58

标签: controller javafx-8 fxml subforms

我有一个主窗体(MainForm.fxml),它的控制器在fxml文件中定义。在这个相同的fxml文件中,我有2个子表单(Subform1.fxml和Subform2.fxml),我包含在fx:include中。 Subform1有一个具体的控制器。 Subform2是一个通用的“选择和编辑”表单,后面有抽象代码。我想根据上下文显示具有抽象代码的不同具体实现的Subform2。如果我在fxml中定义控制器,那么它将不再是通用的。

我只使用FXMLLoader来加载MainForm,我找不到任何方法来更改子窗体的控制器。我四处走动,尝试不同的事情。任何帮助将不胜感激。

更新我的问题 感谢James_D到目前为止的帮助。 fxml文件中我的Subform1的定义:

    <children>
         <!--<fx:include source="Subform1.fxml" />-->
         <!-- <Subform1 controller="${ISubform}" /> -->
         <Subform1 controller="${Subform1Controller}" />
         <!-- <Subform1 /> -->
    </children>

我创建了一个界面如下:

package testsubforms;

public interface ISubform {
}

这是我的控制者:

package testsubforms;

public class Subform1Controller implements ISubform {
    public Subform1Controller() {
        System.out.println("Inside Subform1Controller");
    }
}

以下是我的Subform1类:

package testsubforms;

import java.io.IOException;
import javafx.beans.NamedArg;
import javafx.fxml.FXMLLoader;
import javafx.scene.layout.GridPane;

public class Subform1 extends GridPane {
    private ObjectProperty controller;

    public ObjectProperty controllerProperty() {
        return this.controller;
    }

    public void setController(Subform1Controller controller) {
        this.controllerProperty().set(controller);
    }


    public Subform1(@NamedArg("controller") Subform1Controller controller) throws IOException {
        this.controller = new SimpleObjectProperty(this, "controller", controller);
        FXMLLoader loader = new FXMLLoader(getClass().getResource("Subform1.fxml"));
        loader.setRoot(this);
        loader.setController(controller);
        loader.load();
    }

    public Subform1() throws IOException {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("Subform1.fxml"));
        loader.setRoot(this);
        loader.setController(this);
        loader.load();
    }
}

我当前的问题是运行时错误“javafx.fxml.LoadException:无法绑定到无类型对象”,我在fxml文件中指定了Subform1。 任何有助于在拼图中获得最后一块作品的帮助将非常感激。一旦我得到最后一篇文章,我将发布完整的例子供其他人使用。

1 个答案:

答案 0 :(得分:0)

一种方法是将接口指定为Subform2.fxml中的控制器类。例如,定义

public interface Subform2Controller {

}

然后您可以将该接口指定为控制器“class”:

<GridPane xmlns="..." fx:controller="my.package.Subform2Controller">
    <!-- -->
</GridPane>

现在指定一个专门处理这种情况的控制器工厂:

Object subform2Controller = /* any controller implementation you like... */ ;
FXMLLoader loader = new FXMLLoader(getClass().getResource("path/to/main.fxml"));
loader.setControllerFactory(type -> {
    try {
        if (type == Subform2Controller.class) {
            return subform2Controller ;
        }
        // default implementation:
        return type.newInstance();
    } catch (Exception exc) {
        // this is pretty much fatal...
        throw new RuntimeException(exc);
    }
});
Parent root = loader.load();

这里的想法是控制器工厂是FXMLLoader用于将FXML文件中声明的类映射到特定对象的函数。 (默认情况下,它只调用指定的newInstance()上的Class。)当您使用FXML包含时,控制器工厂将向下传播以加载包含的文件。此实现只是拦截定义接口的特定情况,并返回您在代码中动态指定的任何对象。

据我所知,没有实际要求返回的对象是指定类的实例(虽然我想我从未测试过这个)。无论如何,如果确保控制器是实现fx:controller属性中声明的接口的类的实例,它可能有助于您的理智(这也使您有机会指定您期望该控制器的任何功能)提供)。

另一种方法是使用FXML "custom component" pattern。这实际上颠倒了FXML和控制器的创建角色,这意味着您不是加载一个或多或少静默创建控制器实例的FXML文件,而是创建一个充当控制器的Java对象,并负责加载FXML。

使用这种方法,您可以创建多个“自定义组件”,这些组件都加载相同的FXML文件。

因此,如果您的Subform2.fxml看起来像之前:

<!-- headers, etc -->

<GridPane xmlns="..." fx:controller="...">
    <!-- -->
</GridPane>

你要用:

替换根元素
<!-- headers, etc -->

<fx:root type="GridPane" xmlns="..." >
    <!-- -->
</fx:root>

请注意,此处不再指定控制器。

现在你可以创建一个类似控制器的类,它只需要扩展GridPane(或者更一般地说,它扩展了fx:root元素的“type”属性中指定的类)。在构造函数中,为FXML文件创建FXMLLoader,并将根和控制器都设置为当前对象:

public class Subform2 extends GridPane {

    @FXML
    private TextField someTextField ;

    // etc

    public Subform2() throws IOException {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("/path/to/Subform2.fxml"));
        loader.setRoot(this);
        loader.setController(this);
        loader.load();
    }

    @FXML
    public void handleSomeEvent(ActionEvent event) {
        // ...
    }

    // ...

}

要使用此功能,您只需使用

即可在Java中执行此操作
GridPane subform2 = new Subform2();

如果要在FXML中使用它,而不是使用<fx:include>,只需使用常规实例元素。当然,您可以像往常一样指定任何属性,无论它们是从GridPane继承还是在类本身中定义它们:

<Subform2 alignment="center">
    <padding>
        <Insets top="5" right="5" bottom="5" left="5"/>
    </padding>
</Subform2>

这可能会满足您的需求,因为您可以只定义一个FXML文件,但任意多个不同的类都可以加载单个FXML文件。

作为此的一个小变体,您可以将GridPane子类简单地作为FXML的根,并将另一个对象传递给其构造函数以充当控制器。因此,例如,如果您已经定义了表示FXML文件的控制器的接口(或抽象类):

public interface Subform2Controller {

    /* methods */

}

你可以做到

public class Subform2 extends GridPane {

    public Subform2(@NamedArg("controller") Subform2Controller controller) throws IOException {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("/path/to/Subform2.fxml"));
            loader.setRoot(this);
            loader.setController(controller);
            loader.load();
        }
    }
}

这允许您执行

之类的操作
<Subform2 controller="${subform2Controller}" />

再次允许您加载FXML文件并动态指定控制器。