I have a JavaFX application that checks for the presence of a database at startup. If the database is not found, a "seed" database must be created before the program can proceed.
Since creating the seed database can be lengthy, I want to display a splash screen and show progress updates as the process proceeds. Only after the seed database is completely written should the program proceed.
Here is the class for the splash screen:
public final class SplashWindow {
private final Label message;
private final ProgressBar progress;
private final Stage splashStage;
public SplashWindow() {
Image img = new Image(IMAGE_PREFIX + "splash_image.png");
double imgWidth = img.getWidth();
ImageView splashImage = new ImageView(img);
splashImage.setFitWidth(imgWidth);
splashImage.setPreserveRatio(true);
message = new Label("Saving seed database...");
message.setPrefWidth(imgWidth);
message.setAlignment(Pos.CENTER);
progress = new ProgressBar();
progress.setPrefWidth(imgWidth);
Pane splashVBox = new VBox(3);
splashVBox.setPadding(new Insets(5));
splashVBox.getChildren().addAll(splashImage, progress, message);
splashStage = new Stage(StageStyle.UTILITY);
splashStage.setScene(new Scene(splashVBox));
}
public void bindMessageProperty(ReadOnlyStringProperty sp) {
message.textProperty().bind(sp);
}
public void bindProgressProperty(ReadOnlyDoubleProperty dp) {
progress.progressProperty().bind(dp);
}
public void show() {
splashStage.show();
}
public void shutdown() {
message.textProperty().unbind();
progress.progressProperty().unbind();
splashStage.hide();
}
}
When run, the splash screen shows correctly with the image, progress bar and text message area.
The SplashWindow
class is called by the following method:
private void saveSeedDatabase(ObservableList<RefModel> docList) {
SplashWindow splash = new SplashWindow();
Task<Integer> saveTask = new Task<Integer>() {
@Override
protected Integer call() throws InterruptedException {
updateMessage("Saving references...");
int docsSaved = 0;
for (RefModel rm : docList) {
if (isCancelled()) {
updateMessage("Cancelled");
break;
}
updateMessage("Saving: " + rm.getTitle());
saveNewReference(rm);
docsSaved++;
updateProgress(docsSaved, docList.size());
}
updateMessage("Saved " + docsSaved + " references to database");
return docsSaved;
}
};
saveTask.setOnSucceeded((WorkerStateEvent t) -> {
splash.shutdown();
});
splash.bindMessageProperty(saveTask.messageProperty());
splash.bindProgressProperty(saveTask.progressProperty());
splash.show();
new Thread(saveTask).start();
try {
saveTask.get();
} catch (InterruptedException | ExecutionException ex) {
// Do nothing
}
}
When run, the splash screen is displayed but never shows the update messages and progress. It shows a wait cursor when the mouse if over the splash screen.
If the try/catch block at the end of the method is commented out, the main program attempts to proceed without waiting for the database to be written. In this case, the splash screen is hidden by the main program window, but does display the update messages as it works. From debugging this, it looks like everything is running on the correct thread -- the database stuff is on a worker thread, the SplashWindow
stuff is on the FX event thread.
It seems clear that the call to saveTask.get()
is blocking the UI thread, but I am not sure why.
@JewelSea has written a nice alternative that doesn't fit my program architecture very well. It would not be impossible to alter my program to work with his solution.
However, I don't understand why the get()
call blocks the UI. What am I doing wrong.