0

I currently have 3 classes.

ScreenController (controller class):

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 (primary screen):

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>

HomeScreen (secondary screen):

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>

I would like to be able to move from the primary screen to the secondary screen using the setScreen() function. However, I'm finding that the process doesn't complete successfully.

Another approach I've found that works is (Although it resizes the window, rather than filling the initial window with the contents of the new one):

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

However, I'd prefer to use the initial implementation due to it being more concise, readable and, theoretically, provides the exact behaviour I would like.

Slaw
  • 37,820
  • 8
  • 53
  • 80
  • 2
    The `FXMLLoader#load(URL)` method is static, regardless of if you call it on an instance. You need to use the instance `load()` method. Set the location either via the constructor or via `FXMLLoader#setLocation(URL)`. However, as I noted in my answer to your previous question, sharing the controller instance is not a good idea. What are you hoping to accomplish with this? The controller instance will have all injected fields replaced, initialization will happen twice, and linked methods will now be linked to multiple, unrelated objects. – Slaw Aug 20 '19 at 21:52
  • 2
    You should have a well designed model and share said model between controllers. The controllers then interact with the model, including observing it for changes and reacting appropriately. I recommend reading about application architectures such as MVC, MVVM, and MVP. – Slaw Aug 20 '19 at 21:59
  • 2
    For one, you cannot use `setController` and `fx:controller` at the same time, only one or the other. And if I understood, it seemed as if you wanted to use the _same_ controller instance for _multiple loads_. If that's the case, don't. – Slaw Aug 21 '19 at 09:27
  • 1
    Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/198225/discussion-between-slaw-and-trojanthehorse). – Slaw Aug 21 '19 at 09:27

2 Answers2

0

There are a couple issues with what you currently have:

  1. In your LoginScreen constructor you call setWindow with the value of a yet-to-be-injected field:

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

    No FXML fields will have been injected while the constructor of the controller is executing—meaning loginWindow is null. The reason for this self-evident: The FXMLLoader has to first construct the controller instance before it can start injecting the appropriate fields.

    The order of events are: (1) Controller instantiated, (2) fields injected, (3) initialize method invoked; I believe linking any event handlers/change listeners is included in step two. What this means is any initialization that needs to happen regarding FXML fields should be done in the initialize method.

    You have the same problem in your HomeScreen constructor with super(homeWindow), though there are other problems there which are addressed in the next point.

  2. In addition to trying to access a yet-to-be-injected field in the constructor, there are two other problems with the following:

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

    The first problem is you initialize a field that is meant to be injected. Never do this. A good rule of thumb is: If the field is annotated with @FXML then don't manually assign a value to it. The FXML field will eventually be injected which means any value you assign to it beforehand will simply be replaced. This can lead to subtle problems since any code with a reference to the previous value won't be using the object that was actually added to the scene graph.

    The other problem is your field is static. Injecting static fields is not supported in JavaFX 8+. It used to be possible in older versions, from what I understand, but that behavior was never officially supported (i.e. was an implementation detail). Besides, it doesn't make sense to have something inherently instance-based (FXML+controllers) set a static field which would affect all instances.

    A bonus problem: When you make homeWindow non-static you can no longer use super(homeWindow) because you can't reference it before the super constructor is invoked.

Using the two modified classes should allow your code to run:

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
    }

}

However don't use:

window.getChildren().setAll(root);

In your ScreenController#setScreen method—it causes a subtle problem. You're adding a root as a child of the window node. But when this happens, the new instance of ScreenController (associated with the new root) has its window == root. In other words, the window created with LoginScreen is now the parent of the window created with HomeScreen. Depending on how a more complex application is designed, this can lead to progressively deeper nesting of "roots".

That said, you already have another approach where you actually replace the entire Scene. The issue you're having there, as you stated, is that the Stage resizes to fit the new Scene. This can be fixed by replacing the root of the Scene, rather than the Scene itself:

window.getScene().setRoot(root);

Some potentially helpful resources:

Slaw
  • 37,820
  • 8
  • 53
  • 80
  • Thanks for the help, I too realised the static misplacement and the error in the constructor (of sending a not-yet instantiated property to the constructors), so this was amended already. However, I didn't realise the initialise method could be used in such a way and you also fixed my flaw wherein the login screen was becoming the parent of any subsequent scene (causing nesting) by suggesting the newer implementation - which was going to be my next query, so thank you very much for all the help. –  Aug 24 '19 at 14:56
0

You can get the main stage of a JavaFX Application during initialization. Other scene classes should have a Stage field with getter and setter and so you will be able to pass the main stage through their Controller. As for the window resize, you can fix that by adding getStage().getWidth() and getStage().getHeight() in the setScene() statement.

A small example of what I am trying to point:

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

     }

So the above function starts from MainClass where the main stage is created. Notice the part in the middle which is a bit separeted from the rest of the code, where by getting the controller of the loaded Scene I am passing the stage to it. You can pass the stage that way to all of your scenes. Also notice the part where the scene is set, where I use two more parameters extracted from the stage; the width and the height. Other than that, there are more ways to get the stage in pretty much every scene that runs on the primary stage, just by doing:

    @FXML private Button aButton;

    public Button getAButton(){
       return aButton;
    }

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

This will work in all scenes based in the primary stage and it only requires you to have a registered in the Scene Graph Node no matter the type.