【问题标题】:How do you transition between controllers within the original window that was created您如何在创建的原始窗口中的控制器之间转换
【发布时间】:2019-12-26 02:07:09
【问题描述】:

我目前有 3 节课。

ScreenController(控制器类):

import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.layout.AnchorPane;
import java.net.URL;
import java.util.ResourceBundle;

public class ScreenController implements Initializable
{
    private AnchorPane window;

    public ScreenController()
    {
        super();
    }

    public ScreenController(AnchorPane window)
    {
        setWindow(window);
    }

    public void setWindow(AnchorPane window)
    {
        this.window = window;
    }

    public void setScreen(String screen)
    {
        try
        {
            Parent root = FXMLLoader.load(getClass().getResource("/com/app/client/resources/fxml/" + screen + ".fxml"));
            window.getChildren().setAll(root);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }

    @Override
    public void initialize(URL location, ResourceBundle resources)
    {
    }
}

LoginScreen(主屏幕):

import com.app.client.java.controllers.ScreenController;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.layout.AnchorPane;

import java.io.IOException;

public class LoginScreen extends ScreenController
{
    @FXML
    private AnchorPane loginWindow;

    @FXML
    private Button goButton;

    public LoginScreen()
    {
        super();
        setWindow(loginWindow);
    }

    @FXML
    public void goButtonPressed(ActionEvent event) throws IOException
    {
        setScreen("Home");
        System.out.println("Success.");
    }
}
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.AnchorPane?>

<AnchorPane fx:id="loginWindow" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" opacity="0.5" prefHeight="500.0" prefWidth="850.0" xmlns="http://javafx.com/javafx/8.0.172-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.app.client.java.classes.LoginScreen">
   <children>
      <Button fx:id="goButton" layoutX="205.0" layoutY="60.0" mnemonicParsing="false" onAction="#goButtonPressed" text="Button" />
   </children>
</AnchorPane>

主屏幕(辅助屏幕):

import com.app.client.java.controllers.ScreenController;
import javafx.fxml.FXML;
import javafx.scene.layout.AnchorPane;

public class HomeScreen extends ScreenController
{
    @FXML
    private static AnchorPane homeWindow = new AnchorPane();

    public HomeScreen()
    {
        super (homeWindow);
    }
}
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.TextArea?>
<?import javafx.scene.layout.AnchorPane?>


<AnchorPane fx:id="homeWindow" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.172-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.app.client.java.classes.HomeScreen">
   <children>
      <TextArea layoutX="200.0" layoutY="100.0" prefHeight="200.0" prefWidth="200.0" text="aksajkasjkasja" />
   </children>
</AnchorPane>

我希望能够使用 setScreen() 函数从主屏幕移动到辅助屏幕。但是,我发现该过程没有成功完成。

我发现另一种可行的方法是(尽管它会调整窗口大小,而不是用新窗口的内容填充初始窗口):

Parent root = FXMLLoader.load(getClass().getResource("/com/app/client/resources/fxml/" + screen + ".fxml"));
Stage stage = (Stage) loginWindow.getScene().getWindow();
Scene scene = new Scene(root);
stage.setScene(scene);

但是,我更喜欢使用初始实现,因为它更简洁、易读,并且理论上可以提供我想要的确切行为。

【问题讨论】:

  • FXMLLoader#load(URL) 方法是静态的,无论您是否在实例上调用它。您需要使用实例load() 方法。通过构造函数或FXMLLoader#setLocation(URL) 设置位置。但是,正如我在对您之前的问题的回答中指出的那样,共享控制器实例并不是一个好主意。你希望通过这个实现什么?控制器实例将替换所有注入的字段,初始化将发生两次,链接的方法现在将链接到多个不相关的对象。
  • 您应该有一个设计良好的模型并在控制器之间共享该模型。然后控制器与模型进行交互,包括观察模型的变化并做出适当的反应。我建议阅读有关 MVC、MVVM 和 MVP 等应用程序架构的内容。
  • 一方面,你不能同时使用setControllerfx:controller,只能使用其中之一。如果我理解的话,您似乎想将 same 控制器实例用于多次加载。如果是这种情况,请不要。

标签: java intellij-idea javafx fxml fxmlloader


【解决方案1】:

您可以在初始化期间获取 JavaFX 应用程序的主要阶段。其他场景类应该有一个带有 getter 和 setter 的 Stage 字段,因此您将能够通过它们的 Controller 传递主舞台。至于窗口大小,您可以通过在setScene() 语句中添加getStage().getWidth()getStage().getHeight() 来解决这个问题。

我要指出的一个小例子:

    public class MainClass extends Application {

      @Override
      public void start(Stage stage) throws Exception {

        InputStream sceneStream = MainClass.class.getResourceAsStream("/fxml"+
        "/newScene/main.fxml");
        FXMLLoader loader = new FXMLLoader();
        Parent root = loader.load(sceneStream);
        Scene scene = new Scene(root);
        stage.setTitle("App title");

        NewScene controller = loader.getController();
        controller.setMainStage(stage);

        stage.setScene(scene, stage.getWidth(), stage.getHeight());
        stage.show();

     }

所以上面的函数是从创建主阶段的 MainClass 开始的。注意中间的部分,它与代码的其余部分有点分开,通过获取加载的Scene 的控制器,我将阶段传递给它。您可以通过这种方式将舞台传递到所有场景。还要注意设置场景的部分,我使用了从舞台中提取的另外两个参数;宽度和高度。除此之外,还有更多方法可以在主舞台上运行的几乎每个场景中获得舞台,只需执行以下操作:

    @FXML private Button aButton;

    public Button getAButton(){
       return aButton;
    }

    Stage stage = (Stage) getAButton().getScene().getWindow();

这将适用于基于初级阶段的所有场景,并且只需要您在场景图中注册Node,无论类型如何。

【讨论】:

    【解决方案2】:

    您目前有几个问题:

    1. 在您的 LoginScreen 构造函数中,您使用尚未注入字段的值调用 setWindow

      public LoginScreen()
      {
          super();
          setWindow(loginWindow);
      }
      

      在执行控制器的构造函数时不会注入任何 FXML 字段——这意味着 loginWindownull。原因不言而喻:FXMLLoader 必须先构造控制器实例,然后才能开始注入相应的字段。

      事件的顺序是:(1) 实例化控制器,(2) 注入字段,(3) 调用初始化方法;我相信链接任何事件处理程序/更改侦听器都包含在第二步中。这意味着任何需要对 FXML 字段进行的初始化都应该在 initialize 方法中完成。

      您的HomeScreen 构造函数与super(homeWindow) 存在相同的问题,但还有其他问题将在下一点中解决。

    2. 除了试图在构造函数中访问一个尚未注入的字段之外,还有以下两个问题:

      @FXML
      private static AnchorPane homeWindow = new AnchorPane();
      

      第一个问题是您初始化了一个要注入的字段。 永远不要这样做。一个好的经验法则是:如果该字段使用@FXML 注释,则不要手动为其分配值。 FXML 字段最终将被注入,这意味着您预先分配给它的任何值都将被简单地替换。这可能会导致一些微妙的问题,因为任何引用先前值的代码都不会使用实际添加到场景图中的对象。

      另一个问题是您的字段是静态的。 JavaFX 8+ 不支持注入静态字段。据我了解,它曾经在旧版本中是可能的,但这种行为从未得到官方支持(即是一个实现细节)。此外,让基于实例(FXML+控制器)的东西设置一个会影响所有实例的静态字段是没有意义的。

      一个额外的问题:当您将homeWindow 设为非静态时,您不能再使用super(homeWindow),因为在调用超级构造函数之前您无法引用它。

    使用这两个修改后的类应该可以让您的代码运行:

    LoginScreen.java

    public class LoginScreen extends ScreenController {
    
        @FXML private AnchorPane loginWindow;
        @FXML private Button goButton;
    
        @Override
        public void initialize(URL location, ResourceBundle resources) {
            super.initialize(location, resources);
            setWindow(loginWindow); // set window in initialize method
        }
    
        @FXML
        public void goButtonPressed(ActionEvent event) throws IOException {
            setScreen("Home");
            System.out.println("Success.");
        }
    
    }
    

    HomeScreen.java

    public class HomeScreen extends ScreenController {
    
        @FXML private AnchorPane homeWindow;
    
        @Override
        public void initialize(URL location, ResourceBundle resources) {
            super.initialize(location, resources);
            setWindow(homeWindow); // set window in initialize method
        }
    
    }
    

    但是不要使用:

    window.getChildren().setAll(root);
    

    在您的ScreenController#setScreen 方法中——它会导致一个微妙的问题。您正在添加 root 作为 window 节点的子节点。但是当这种情况发生时,ScreenController 的新实例(与新的root 关联)具有其window == root。换句话说,使用LoginScreen 创建的window 现在是使用HomeScreen 创建的window 的父级。根据如何设计更复杂的应用程序,这可能会导致“根”的嵌套越来越深。

    也就是说,您已经有了另一种方法,可以实际替换整个 Scene。正如您所说,您遇到的问题是Stage 调整大小以适应新的Scene。这可以通过替换Sceneroot 而不是Scene 本身来解决:

    window.getScene().setRoot(root);
    

    一些可能有用的资源:

    【讨论】:

    • 感谢您的帮助,我也意识到了构造函数中的静态错位和错误(向构造函数发送尚未实例化的属性),因此已经对此进行了修改。但是,我没有意识到可以以这种方式使用初始化方法,并且您还修复了我的缺陷,即登录屏幕通过建议更新的实现成为任何后续场景的父级(导致嵌套) - 这将是我的下一个查询,非常感谢您的所有帮助。
    猜你喜欢
    • 2011-07-02
    • 1970-01-01
    • 1970-01-01
    • 2018-09-26
    • 2020-12-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-03
    相关资源
    最近更新 更多