Search code examples
javamultithreadingjavafxprogress-barrunnable

Can we have a long process in a thread updates a progress bar?


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  :

  1. create a new Thread.
  2. create a Runnable along with the use of Platform.RunLater.

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.

zip file

Thanks.


Solution

  • 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.

    Proof of Concept

    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.

    MockTask.java

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

    Main.java

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