I’m facing a problem with thread and interface.
In my example I have a long process. Along calculations I must stop and ask the user informations. I cannot do that before. So I have to open a window and retrieve the user answer.
There are 2 ways that I know of :
The « copy multiple file » process will be a good example to explain what’s the problem. In this example we know we have to « launch » a long process : Copy every single file (one by one). The main interface has a ProgressBar. The goal is to update the ProgressBar on a regular basis. Then ; along the computation emerges a specific case which require the user attention.
If I used the Thread approach : The ProgressBar updates properly (using bound properties). I end up with the exception « not a JavaFx application » as soon as I try to open a new window (from this side process). This is normal and documented on this very website.
If I used the Runnable approach : the problem is about updates. The new window opens but the progress bar isn’t changed until a « refresh » occurs (see code example in the zip file linked below).
Any other suggestion I could find is not well documented or even explained properly (like service). So I'm stuck.
I’m a surprised not to be able to do that. I’m wondering if I’m doing something wrong or if I don’t use the right approach. Maybe this is JavaFx limitation. Any help greatly appreciated.
Thanks.
Golden Rule of JavaFX: Any and all interactions with a live scene graph must occur on the JavaFX Application Thread. No exceptions.
In JavaFX, if you're going to run work on a background thread, and you need that work to report progress back to the user, then you should first consider using a javafx.concurrent.Task
. It provides a nice API for publishing messages, progress, and a result on the JavaFX Application Thread. From there, you just need to figure out how to prompt the user for more information in the middle of the task executing on a background thread.
The simplest solution, at least in my opinion, is to use a CompletableFuture
. You can configure it to execute a Supplier
on the FX thread and have the background thread call join()
to wait for a result. This only requires that you provide some sort of callback to your Task
for the future to invoke. That callback could be anything. Some options include:
java.util.function.Supplier
java.util.function.Function
javafx.util.Callback
(essentially equivalent to a Function
)
...
Or even your own interface/class. It doesn't have to be a functional interface, by the way.
Note this callback/listener idea can be applied to more than just prompting the user. For instance, if you've created a model/business class to perform the actual work, and you don't want this class to know anything about JavaFX, then you can adapt it to report messages/progress in its own way. The Task
would then register listeners/callbacks as needed. That essentially makes the Task
just a lightweight adapter allowing JavaFX to observe the progress and update the UI accordingly.
The code below launches a task that fakes long-running work. This task will prompt the user to ask if the task should continue upon half the work being completed. The task then waits for a response, and whether or not it will continue depends on said response.
The primary window is displaying a progress bar and message label to show such things still work as expected. When the task prompts the user, a modal alert will be displayed that will wait for the user to respond.
Note I use a Callback<String, Boolean>
for simplicity. But that interface is generic, and so it can be used with any types you want.
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.util.Callback;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Supplier;
public class MockTask extends Task<Void> {
private final Callback<String, Boolean> promptCallback;
public MockTask(Callback<String, Boolean> promptCallback) {
this.promptCallback = Objects.requireNonNull(promptCallback);
}
@Override
protected Void call() throws Exception {
int iterations = 10_000;
updateProgress(0, iterations);
for (int i = 0; i < iterations; i++) {
updateMessage("Processing " + i + "...");
Thread.sleep(1L);
updateProgress(i + 1, iterations);
if (i == iterations / 2) {
boolean shouldContinue = promptUser("Should task continue?");
if (!shouldContinue) {
throw new CancellationException();
}
}
}
return null;
}
private boolean promptUser(String prompt) {
Supplier<Boolean> supplier = () -> promptCallback.call(prompt); // adapt Callback to Supplier
Executor executor = Platform::runLater; // tells CompletableFuture to execute on FX thread
// Calls the supplier on the FX thread and waits for a result
return CompletableFuture.supplyAsync(supplier, executor).join();
}
}
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
private Stage primaryStage;
@Override
public void start(Stage primaryStage) {
this.primaryStage = primaryStage;
var progIndicator = new ProgressIndicator();
var msgLabel = new Label();
var root = new VBox(progIndicator, msgLabel);
root.setAlignment(Pos.CENTER);
root.setSpacing(10);
primaryStage.setScene(new Scene(root, 500, 300));
primaryStage.show();
var task = new MockTask(this::handlePrompt);
progIndicator.progressProperty().bind(task.progressProperty());
msgLabel.textProperty().bind(task.messageProperty());
var thread = new Thread(task, "task-thread");
thread.setDaemon(true);
thread.start();
}
private boolean handlePrompt(String prompt) {
var alert = new Alert(Alert.AlertType.CONFIRMATION, prompt, ButtonType.YES, ButtonType.NO);
alert.initOwner(primaryStage);
alert.setHeaderText(null);
return alert.showAndWait().map(bt -> bt == ButtonType.YES).orElse(false);
}
}