My initial fxml(say home.fxml
) has a lot of functionalities, hence it takes a lot of time to load completely. So to avoid the time gap between program start and fxml loading I introduced one more fxml(say loader.fxml
) with a gif image which should appear while the main fxml is loading.
The problem is the gif image in my loader.fxml is not moving as in the program is hanging until the home.fxml is loaded completely.
To avoid this I moved the home.fxml loading into a thread as shown in below code.
public class UATReportGeneration extends Application {
private static Stage mainStage;
@Override
public void start(Stage stage) {
Parent loaderRoot = null;
try {
loaderRoot = FXMLLoader.load(getClass().getResource("/uatreportgeneration/fxml/Loader.fxml"));
} catch (IOException ex) {
Logger.getLogger(UATReportGeneration.class.getName()).log(Level.SEVERE, null, ex);
}
Scene loadScene = new Scene(loaderRoot);
stage.setScene(loadScene);
stage.initStyle(StageStyle.UNDECORATED);
stage.getIcons().add(new Image(this.getClass().getResourceAsStream("/uatreportgeneration/Images/logo.png")));
stage.show();
mainStage = new Stage(StageStyle.UNDECORATED);
mainStage.setTitle("Upgrade Analysis");
mainStage.getIcons().add(new Image(this.getClass().getResourceAsStream("/uatreportgeneration/Images/logo.png")));
setStage(mainStage);
new Thread(() -> {
Platform.runLater(() -> {
try {
FXMLLoader loader = new FXMLLoader();
Parent root = loader.load(getClass().getResource("/uatreportgeneration/fxml/Home.fxml"));
Scene scene = new Scene(root);
mainStage.setScene(scene);
mainStage.show();
stage.hide();
System.out.println("Stage showing");
// Get current screen of the stage
ObservableList<Screen> screens = Screen.getScreensForRectangle(new Rectangle2D(mainStage.getX(), mainStage.getY(), mainStage.getWidth(), mainStage.getHeight()));
// Change stage properties
Rectangle2D bounds = screens.get(0).getVisualBounds();
mainStage.setX(bounds.getMinX());
mainStage.setY(bounds.getMinY());
mainStage.setWidth(bounds.getWidth());
mainStage.setHeight(bounds.getHeight());
System.out.println("thread complete");
} catch (IOException ex) {
Logger.getLogger(UATReportGeneration.class.getName()).log(Level.SEVERE, null, ex);
}
});
}).start();
}
public static Stage getStage() {
return mainStage;
}
public static void setStage(Stage stage) {
mainStage = stage;
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
But after this code also my program is hanging(The gif image is not moving). If I load the fxml outside the Platform.runLater()
, I get the exception Not on FX Thread
.
I also tired using Task()
but from that the gif image is moving but the fxml is not loading in the background, if I try to load the fxml outside Platform.runLater()
.
Can anyone please help me and tell me how can I correct the code so that my fxml loads in the background without disturbing the foreground process.
Use a Task
. You need to arrange to create the scene and update the stage on the FX Application Thread. The cleanest way is to use a Task<Parent>
:
Task<Parent> loadTask = new Task<Parent>() {
@Override
public Parent call() throws IOException {
FXMLLoader loader = new FXMLLoader();
Parent root = loader.load(getClass().getResource("/uatreportgeneration/fxml/Home.fxml"));
return root ;
}
};
loadTask.setOnSucceeded(e -> {
Scene scene = new Scene(loadTask.getValue());
mainStage.setScene(scene);
mainStage.show();
stage.hide();
System.out.println("Stage showing");
// Get current screen of the stage
ObservableList<Screen> screens = Screen.getScreensForRectangle(new Rectangle2D(mainStage.getX(), mainStage.getY(), mainStage.getWidth(), mainStage.getHeight()));
// Change stage properties
Rectangle2D bounds = screens.get(0).getVisualBounds();
mainStage.setX(bounds.getMinX());
mainStage.setY(bounds.getMinY());
mainStage.setWidth(bounds.getWidth());
mainStage.setHeight(bounds.getHeight());
System.out.println("thread complete");
});
loadTask.setOnFailed(e -> loadTask.getException().printStackTrace());
Thread thread = new Thread(loadTask);
thread.start();
Here is a SSCCE using this technique:
main.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.VBox?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TextField?>
<VBox spacing="10" xmlns:fx="http://javafx.com/fxml/1" fx:controller="MainController">
<padding>
<Insets top="24" left="24" right="24" bottom="24"/>
</padding>
<TextField />
<Button fx:id="button" text="Show Window" onAction="#showWindow"/>
</VBox>
MainController (uses the Task
approach shown above):
import java.io.IOException;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;
public class MainController {
@FXML
private Button button ;
@FXML
private void showWindow() {
Task<Parent> loadTask = new Task<Parent>() {
@Override
public Parent call() throws IOException, InterruptedException {
// simulate long-loading process:
Thread.sleep(5000);
FXMLLoader loader = new FXMLLoader(getClass().getResource("test.fxml"));
Parent root = loader.load();
return root ;
}
};
loadTask.setOnSucceeded(e -> {
Scene scene = new Scene(loadTask.getValue());
Stage stage = new Stage();
stage.initOwner(button.getScene().getWindow());
stage.setScene(scene);
stage.show();
});
loadTask.setOnFailed(e -> loadTask.getException().printStackTrace());
Thread thread = new Thread(loadTask);
thread.start();
}
}
test.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Button?>
<?import javafx.geometry.Insets?>
<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="TestController">
<padding>
<Insets top="24" left="24" right="24" bottom="24"/>
</padding>
<center>
<Label fx:id="label" text="This is a new window"/>
</center>
<bottom>
<Button text="OK" onAction="#closeWindow" BorderPane.alignment="CENTER">
<BorderPane.margin>
<Insets top="5" bottom="5" left="5" right="5"/>
</BorderPane.margin>
</Button>
</bottom>
</BorderPane>
TestController:
import javafx.fxml.FXML;
import javafx.scene.control.Label;
public class TestController {
@FXML
private Label label ;
@FXML
private void closeWindow() {
label.getScene().getWindow().hide();
}
}
Main application:
import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws IOException {
primaryStage.setScene(new Scene(FXMLLoader.load(getClass().getResource("main.fxml"))));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Notice that after pressing the button, you can still type in the text field during the five seconds it takes to "load" the FXML, so the UI is remaining responsive.
your approach using Task
was already correct. you were just missing a bit more: you were just missing another Platform#invokeLater()
to update the UI:
new Thread(new Task() {
@Override
protected Object call() throws Exception {
// Simulating long loading
Thread.sleep(5000);
FXMLLoader loader = new FXMLLoader(getClass().getResource("home.fxml"));
Parent root = loader.load();
Scene scene = new Scene(root);
// Updating the UI requires another Platform.runLater()
Platform.runLater(new Runnable() {
@Override
public void run() {
mainStage.setScene(scene);
mainStage.show();
stage.hide();
System.out.println("Stage showing");
// Get current screen of the stage
ObservableList<Screen> screens = Screen.getScreensForRectangle(new Rectangle2D(mainStage.getX(), mainStage.getY(), mainStage.getWidth(), mainStage.getHeight()));
// Change stage properties
Rectangle2D bounds = screens.get(0).getVisualBounds();
mainStage.setX(bounds.getMinX());
mainStage.setY(bounds.getMinY());
mainStage.setWidth(bounds.getWidth());
mainStage.setHeight(bounds.getHeight());
System.out.println("thread complete");
}
});
return null;
}
}).start();
In addition to @James_D answer. As mentioned by him, Stages and Scene should not be added in background thread, they should be added only in FX main thread.
I have added tooltips
in my home.fxml
, which is nothing but a PopupWindow
. Therefore to the background thread, it appeared as a new stage. Hence it threw IllegalStateException
. After removing the tooltips
from the fxml, the fxml was able to load as a background process as there were no stages created in that thread.