如何使用 FXrouter 或其他方式手动实例化 FXML 控制器?

How to manually instantiated FXML controller with FXrouter, or another way?

大家下午好。我正在尝试将控制器启动到 FXML 页面,但不确定如何启动。我目前正在使用 FXRouter GitHub API.

我想将控制器页面绑定到 FXML 文件外部的 FXML 页面。我下面有一个“LoadNewRoute”方法,目前它正在执行此操作,但我想改进它,因此它将控制器分配给外部的 FXML 页面。最好的方法是什么?

加载页面的方法

private static void loadNewRoute(RouteScene route) throws IOException {
        // get Main Class package name to get correct files path

        String pathRef = mainRef.getClass().getPackage().getName();


        // set FXRouter current route reference
        currentRoute = route;

        // create correct file path.  "/" doesn't affect any OS
        String scenePath = "/" + pathRef + "/" + route.scenePath;


        // load .fxml resource
        FXMLLoader loader = new FXMLLoader(new Object() { }.getClass().getResource(scenePath));
        Parent resource = loader.load();




      //  resource.setController(BuyController);

        // set window title from route settings or default setting
        window.setTitle(route.windowTitle);
        // set new route scene
        window.setScene(new Scene(resource, route.sceneWidth, route.sceneHeight));
        // show the window
        window.show();

        // set scene animation
        routeAnimation(resource);
    }

FXML 页面-

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.RadioButton?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.text.Font?>

<AnchorPane xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.gradle.Controllers.BuyController" >

    <MenuBar layoutX="676.0" layoutY="2.0">
        <Menu mnemonicParsing="false" text="Pages">
            <MenuItem mnemonicParsing="false" onAction="#SwitchToMainScene" text="Home" />
            <MenuItem mnemonicParsing="false" onAction="#SwitchToBuyScene" text="Buy" />
            <MenuItem mnemonicParsing="false" onAction="#SwitchToSellScene" text="Sell" />
        </Menu>
        <Menu mnemonicParsing="false" text="Options">
            <MenuItem mnemonicParsing="false" onAction="#CloseApp" text="Exit" />
            <MenuItem mnemonicParsing="false" onAction="#SwitchToProfileScene" text="Profile" />
        </Menu>
    </MenuBar>
    <Pane layoutX="14.0" layoutY="67.0" prefHeight="200.0" prefWidth="200.0">
        <CheckBox layoutX="7.0" layoutY="116.0" mnemonicParsing="false" prefHeight="17.0" prefWidth="82.0" text="DogeCoin" />
        <CheckBox layoutX="7.0" layoutY="153.0" mnemonicParsing="false" prefHeight="17.0" prefWidth="72.0" text="Bitcoin" />
        <CheckBox layoutX="7.0" layoutY="83.0" mnemonicParsing="false" text="Etherium" />
        <Label layoutX="27.0" layoutY="26.0" text="Select your CryptoCurrency" />
    </Pane>
    <Pane layoutX="528.0" layoutY="-1.0" prefHeight="37.0" prefWidth="272.0">
        <Button layoutX="222.0" layoutY="-1.0" mnemonicParsing="false" onAction="#CloseApp" prefHeight="30.0" prefWidth="50.0" text="Exit" />
        <Button layoutX="166.0" layoutY="-1.0" mnemonicParsing="false" onAction="#SwitchToProfileScene" prefHeight="30.0" prefWidth="56.0" text="Profile" />
        <Button layoutX="118.0" layoutY="-1.0" mnemonicParsing="false" onAction="#SwitchToSellScene" prefHeight="30.0" prefWidth="48.0" text="Sell" />
        <Button layoutX="70.0" layoutY="-1.0" mnemonicParsing="false" onAction="#SwitchToBuyScene" prefHeight="30.0" prefWidth="48.0" text="Buy" />
        <Button layoutX="14.0" layoutY="-1.0" mnemonicParsing="false" onAction="#SwitchToMainScene" prefHeight="30.0" prefWidth="56.0" text="Home" />
    </Pane>
    <Pane layoutX="300.0" layoutY="88.0" prefHeight="200.0" prefWidth="200.0">
        <RadioButton layoutX="13.0" layoutY="38.0" mnemonicParsing="false" text="Amount in coin" />
        <RadioButton layoutX="13.0" layoutY="67.0" mnemonicParsing="false" text="Amount in cash" />
        <TextField layoutX="13.0" layoutY="139.0" />
        <Label layoutX="-1.0" prefHeight="34.0" prefWidth="200.0" text="Choose type of buying, and enter amount" wrapText="true" />
    </Pane>
    <Button layoutY="-1.0" mnemonicParsing="false" onAction="#BackButton" prefHeight="30.0" prefWidth="64.0" text="Back" />
    <Button layoutX="237.0" layoutY="447.0" mnemonicParsing="false" onAction="#BuyButton" prefHeight="82.0" prefWidth="278.0" text="Buy">
        <font>
          <Font size="31.0" />
        </font>
     </Button>
</AnchorPane>

控制器-

   package com.gradle.Controllers;
   import com.gradle.Controller;
  import com.gradle.FXRouter;
  import javafx.event.ActionEvent;
  import javafx.fxml.FXMLLoader;

  import java.io.IOException;

  public class BuyController extends com.gradle.Controller {

  public void example () {

  }

    

家庭控制器

public class HomeController extends com.gradle.Controller {


    public void SwitchToMainScene (ActionEvent event) throws IOException {
        FXRouter.goTo("Home", new HomeController());}

    public void SwitchToBuyScene (ActionEvent event) throws IOException {
        FXRouter.goTo("Buy", new BuyController());}

    public void SwitchToSellScene (ActionEvent event) throws IOException {
        FXRouter.goTo("Sell", new SellController());}

    public void SwitchToProfileScene (ActionEvent event) throws IOException {
        FXRouter.goTo("Profile", new ProfileController());}

    @Override
    public void initilize(URL url, ResourceBundle rb) {

    }
}

呼叫-

 FXRouter.goTo("Home", new HomeController());

FXML-

  <Button fx:id="exit" layoutX="222.0" layoutY="-1.0" mnemonicParsing="false" onAction="#CloseApp" prefHeight="30.0" prefWidth="50.0" text="Exit" />
        <Button layoutX="166.0" layoutY="-1.0" mnemonicParsing="false" onAction="#SwitchToProfileScene" prefHeight="30.0" prefWidth="56.0" text="Profile" />
        <Button layoutX="118.0" layoutY="-1.0" mnemonicParsing="false" onAction="#SwitchToSellScene" prefHeight="30.0" prefWidth="48.0" text="Sell" />
        <Button layoutX="70.0" layoutY="-1.0" mnemonicParsing="false" onAction="#SwitchToBuyScene" prefHeight="30.0" prefWidth="48.0" text="Buy" />
        <Button layoutX="14.0" layoutY="-1.0" mnemonicParsing="false" onAction="#SwitchToMainScene" prefHeight="30.0" prefWidth="56.0" text="Home" />
    </Pane>
   <Button layoutY="-1.0" mnemonicParsing="false" onAction="#BackButton" prefHeight="30.0" prefWidth="64.0" text="Back" />
</AnchorPane>

所有的 onAction 都给我错误,它找不到调用

没有 FXMLRouter

在这个问题的答案中演示了为 FXML(没有 FXMLRouter 库)手动配置控制器的标准方法:

请注意,如果您手动设置控制器,您应该也在您的 fxml 文件中提供一个 fx:controller 属性。

如果您没有使用 FXMLRouter 库,那么只需按照 linked 问题的答案中的示例进行操作,而忽略此答案的其余部分。


使用 FXMLRouter

我看了代码,FXMLRouter 不能做你想做的事。

它使用了 static load function。如果您想设置您创建的控制器,那么您必须使用加载程序实例,如先前引用的答案中所示。

整个路由器只有一个 class。

如果你想使用它:

  1. 将源代码复制到您的项目中。

  2. 修改 loadNewRoute 方法(以及调用它的方法)以传入您创建的控制器。

    • 如果你想保留当前的 ​​public 接口,你可以这样做并添加新的重载接口,它也接受你本地构建的控制器 class.
  3. 如果手动设置控制器,请更改实现以创建新的 FXMLLoader 并在新的加载器实例上设置控制器,而不是使用静态加载方法。

之前引用的答案中演示了对加载实现所需的修改。

或者,您可以修改 FXMLLoader 代码以使用自定义 FXML 控制器工厂方法,如本答案其他部分所述。

示例 FXMLRouter 实现

这不是我的代码,我已经修改了它,不过根据这个答案中之前的修改建议,原始代码和许可信息在:

我根本没有测试这些修改,也不保证它们会起作用。该代码仅作为示例提供。

import java.io.IOException;
import java.util.AbstractMap;
import java.util.HashMap;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.Parent;
import javafx.util.Duration;
import javafx.animation.FadeTransition;

/**
 * FXRouter allows to manage scenes switching on JavaFX Application with an easy API
 * Inspired by Angular JS $routeProvider
 * @author Marco Trombino
 * @version 1.0.0
 */
public final class FXRouter {
    private static final String WINDOW_TITLE = "";
    private static final Double WINDOW_WIDTH = 800.0;
    private static final Double WINDOW_HEIGHT = 600.0;
    private static final Double FADE_ANIMATION_DURATION = 800.0;

    // FXRouter Singleton
    private static FXRouter router;
    // FXRouter Main Class reference to get main package
    private static Object mainRef;
    // FXRouter Application Stage reference to set scenes
    private static Stage window;

    // Application Stage title
    private static String windowTitle;
    // Application Stage size
    private static Double windowWidth;
    private static Double windowHeight;

    // routes switching animation
    private static String animationType;
    private static Double animationDuration;

    // FXRouter routes map
    private static AbstractMap<String, RouteScene> routes = new HashMap<>();
    // FXRouter current route
    private static RouteScene currentRoute;

    /**
     * FXRouter Inner Class used into routes map
     */
    private static class RouteScene {
        // route .fxml Scene path
        private String scenePath;
        // Scene (Stage) title
        private String windowTitle;
        private double sceneWidth;
        private double sceneHeight;
        // route data passed from goTo()
        private Object data;

        private RouteScene(String scenePath) {
            this(scenePath, getWindowTitle(), getWindowWidth(), getWindowHeight());
        }

        private RouteScene(String scenePath, String windowTitle) {
            this(scenePath, windowTitle, getWindowWidth(), getWindowHeight());
        }

        private RouteScene(String scenePath, double sceneWidth, double sceneHeight) {
            this(scenePath, getWindowTitle(), sceneWidth, sceneHeight);
        }

        /** Route scene constructor
         * @param scenePath: .FXML scene file
         * @param windowTitle: Scene (Stage) title
         * @param sceneWidth: Scene Width
         * @param sceneHeight: Scene Height
         */
        private RouteScene(String scenePath, String windowTitle, double sceneWidth, double sceneHeight) {
            this.scenePath = scenePath;
            this.windowTitle = windowTitle;
            this.sceneWidth = sceneWidth;
            this.sceneHeight = sceneHeight;
        }

        private static String getWindowTitle() {
            return FXRouter.windowTitle != null ? FXRouter.windowTitle : WINDOW_TITLE;
        }

        private static double getWindowWidth() {
            return FXRouter.windowWidth != null ? FXRouter.windowWidth : WINDOW_WIDTH;
        }

        private static double getWindowHeight() {
            return FXRouter.windowHeight != null ? FXRouter.windowHeight : WINDOW_HEIGHT;
        }
    }

    /**
     * FXRouter constructor kept private to apply Singleton pattern
     */
    private FXRouter() {}

    public static void bind(Object ref, Stage win) {
        checkInstances(ref, win);
    }

    public static void bind(Object ref, Stage win, String winTitle) {
        checkInstances(ref, win);
        windowTitle = winTitle;
    }

    public static void bind(Object ref, Stage win, double winWidth, double winHeight) {
        checkInstances(ref, win);
        windowWidth = winWidth;
        windowHeight = winHeight;
    }

    /** FXRouter binder with Application Stage and main package
     * @param ref: Main Class reference
     * @param win: Application Stage
     * @param winTitle: Application Stage title
     * @param winWidth: Application Stage width
     * @param winHeight: Application Stage height
     */
    public static void bind(Object ref, Stage win, String winTitle, double winWidth, double winHeight) {
        checkInstances(ref, win);
        windowTitle = winTitle;
        windowWidth = winWidth;
        windowHeight = winHeight;
    }

    /** set FXRouter references only if they are not set yet
     * @param ref: Main Class reference
     * @param win: Application Stage
     */
    private static void checkInstances(Object ref, Stage win) {
        if(mainRef == null)
            mainRef = ref;
        if(router == null)
            router = new FXRouter();
        if(window == null)
            window = win;
    }

    public static void when(String routeLabel, String scenePath) {
        RouteScene routeScene = new RouteScene(scenePath);
        routes.put(routeLabel, routeScene);
    }

    public static void when(String routeLabel, String scenePath, String winTitle) {
        RouteScene routeScene = new RouteScene(scenePath, winTitle);
        routes.put(routeLabel, routeScene);
    }

    public static void when(String routeLabel, String scenePath, double sceneWidth, double sceneHeight) {
        RouteScene routeScene = new RouteScene(scenePath, sceneWidth, sceneHeight);
        routes.put(routeLabel, routeScene);
    }

    /** Define a FXRouter route
     * @param routeLabel: Route label identifier
     * @param scenePath: .FXML scene file
     * @param winTitle: Scene (Stage) title
     * @param sceneWidth: Scene Width
     * @param sceneHeight: Scene Height
     */
    public static void when(String routeLabel, String scenePath, String winTitle, double sceneWidth, double sceneHeight) {
        RouteScene routeScene = new RouteScene(scenePath, winTitle, sceneWidth, sceneHeight);
        routes.put(routeLabel, routeScene);
    }

    public static void goTo(String routeLabel) throws IOException {
        goTo(routeLabel, null, null);
    }

    public static void goTo(String routeLabel, Object data) throws IOException {
        goTo(routeLabel, data, null);
    }

    /** Switch between FXRouter route and show corresponding scenes
     * @param routeLabel: Route label identifier
     * @param data: Data passed to route
     * @oaram controller: Controller instance to use.
     * @throws Exception: throw FXMLLoader exception if file is not loaded correctly
     */
    public static void goTo(String routeLabel, Object data, Object controller) throws IOException {
        // get corresponding route
        RouteScene route = routes.get(routeLabel);
        // set route data
        route.data = data;
        loadNewRoute(route, controller);
    }

    /** Helper method of goTo() which load and show new scene
     * @throws Exception: throw FXMLLoader exception if file is not loaded correctly
     */
    private static void loadNewRoute(RouteScene route, Object controller) throws IOException {
        // get Main Class package name to get correct files path
        String pathRef = mainRef.getClass().getPackage().getName();

        // set FXRouter current route reference
        currentRoute = route;

        // create correct file path.  "/" doesn't affect any OS
        String scenePath = "/" + pathRef + "/" + route.scenePath;

        // load .fxml resource
        FXMLLoader loader = new FXMLLoader();
        loader.setController(controller);
        Parent resource = loader.load(new Object() { }.getClass().getResource(scenePath));

        // set window title from route settings or default setting
        window.setTitle(route.windowTitle);
        // set new route scene
        window.setScene(new Scene(resource, route.sceneWidth, route.sceneHeight));
        // show the window
        window.show();

        // set scene animation
        routeAnimation(resource);
    }

    /* Syntactic sugar for goTo() method when FXRouter get set */
    public static void startFrom(String routeLabel) throws Exception {
        goTo(routeLabel);
    }

    public static void startFrom(String routeLabel, Object data) throws Exception {
        goTo(routeLabel, data);
    }

    public static void startFrom(String routeLabel, Object data, Object controller) throws Exception {
        goTo(routeLabel, data, controller);
    }

    /** set FXRouter switching animation
     * @param anType: Animation type
     */
    public static void setAnimationType(String anType) {
        animationType = anType;
    }

    /** set FXRouter switching animation
     * @param anType: Animation type
     * @param anDuration: Animation duration
     */
    public static void setAnimationType(String anType, double anDuration) {
        animationType = anType;
        animationDuration = anDuration;
    }

    /** Animate routes switching based on animation type
     */
    private static void routeAnimation(Parent node) {
        String anType = animationType != null ? animationType.toLowerCase() : "";
        switch(anType) {
            case "fade":
                Double fd = animationDuration != null ? animationDuration : FADE_ANIMATION_DURATION;
                FadeTransition ftCurrent = new FadeTransition(Duration.millis(fd), node);
                ftCurrent.setFromValue(0.0);
                ftCurrent.setToValue(1.0);
                ftCurrent.play();
                break;
            default:
                break;
        }
    }

    /** Get current route data
     */
    public static Object getData() {
        return currentRoute.data;
    }

}

替代实施

如果不需要,请忽略此部分。 FXMLRouter 非常简单,如果修改符合您的要求,那就太好了。

有其他可用的库可以完成类似的事情,我不会在这里推荐任何库,但我建议您通过 eden coding 查看本教程:

伊甸园教程中有一个盒子,标题为“Dependency Injector – Full Code”,看完教程后,展开盒子,研究代码,看看你是否能看懂,如果你能看懂,那么你就很好地理解了 FXMLLoader 的高级用法,如果没有,也没关系。

如果您不想使用其中的概念,或者如果您目前拥有的已经适合您的目的,则无需使用其中的概念。我只是提供教程 link 作为参考,因为它有助于很好地解释一些复杂的主题。

更复杂的是将 JavaFX 与 SpringBoot 集成,但那更复杂,所以我不会在这里推荐,除非你真的想利用 SpringBoot 的其他功能SpringBoot.

常见问题解答

Do you think it would be possible to take out the need for a Controller to be passed, if you used a switch?

如果您查看我提供的示例代码,我已经做到了。

不需要开关,你可以使用重载方法(我就是这样做的)。

如果你不想传递控制器,那就不要使用传递控制器的方法。

例如,如果您不想传递控制器,请调用 goTo(routeLabel)goTo(routeLabel, data) 而不是 goTo(routeLabel, data, controller)

新代码与现有实现向后兼容,因此如果您不在调用中添加额外参数,则不会设置控制器(就像原始代码的工作方式一样)。

但如果您不传递控制器,则应在 fx:controller 属性中的 FXML 中指定控制器。

How would you then create the object from the controller classes? > I currently have

public void SwitchToMainScene (ActionEvent event) throws IOException {FXRouter.goTo("Home");}

Would I just add in my BuyController name at the end?

有两种方法可以做到这一点。

最简单的方法是为每次调用手动创建和传递控制器:

public void switchToMainScene(ActionEvent event) throws IOException {
    FXRouter.goTo(
        "Home", 
        null, 
        new BuyController(
            <whatever parameters you want to pass to your new instance>
        )
    );
}

注意:我更改了方法示例的大小写,始终编写遵循 Java 命名约定的代码。如果您不想将任何参数传递给控制器​​构造函数,则不要传递任何参数,在这种情况下,行为将与 FXMLLoader 从 FXML 中的 fx:controller 属性构造控制器相同。

替代方法是使用 controller factory。这在我之前提到的伊甸园编码依赖注入教程中讨论过。

要将工厂与 FXMLRouter 一起使用,请将 FXMLRouter 实现修改为:

  1. 创建一个将 FXML 文件映射到控制器实例的工厂(通过存储的哈希映射或 switch 语句或使用依赖注入框架)。
  2. 在 FXMLRouter 实例上设置工厂(如果需要,可以为整个应用程序静态设置)。
  3. 在加载器实例上设置控制器工厂,而不是在加载器实例上设置控制器实例。

我现在不会在这里提供代码,也许你可以从 Javadoc、eden 教程和前面的手动设置构造函数的示例中找出要做什么。

Whenever I remove the fx:Controller from my .fxml, all of my onAction refuses to work, even though I have them assigned in the controller.

当您删除 fx:controller 引用时,Idea IDE 无法确定哪个控制器与 FXML 文件相关联,并且会突出显示对无法解析的方法的引用的错误用于智能编辑和导航。

然而,在运行时控制器被设置和解析,方法引用也是如此(只要你正确命名它们,不要忘记名称是 case-sensitive),所以这不是一个真正的错误, 你可以忽略它,尽管你确实失去了一些 Idea 智能编辑的好处。

或者,为了允许您在 fxml(保留智能编辑)中指定 fx:controller 值并手动配置控制器,您可以使用控制器工厂。答案中有一个示例实现: