-2

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.

4E71-NOP
  • 3
  • 5
  • 4
    [mcve] please .. – kleopatra May 27 '23 at 07:59
  • 3
    Show code here, not a download link. – Basil Bourque May 27 '23 at 08:01
  • Some of the content of your question suggests a [mre] should be present. Such an example must be in the question itself, as text and formatted in code blocks. You may want to take the [tour] and perhaps read [ask]. That said, much of your question seems more concept-oriented rather than asking about any specific code. As I was in a good mood, I decided to post an answer addressing the general concept you're asking about. – Slaw May 27 '23 at 08:58
  • Example [task based progress](https://gist.github.com/jewelsea/2305098). There will be more concise examples with more explanation on StackOverflow, but the linked example will probably be OK for now. You can study Slaw's answer or research the stuff in the example you don't understand on your own. – jewelsea May 28 '23 at 01:15
  • Related: [Can I pause a background Task / Service?](https://stackoverflow.com/questions/14941084/javafx2-can-i-pause-a-background-task-service/14967912#14967912), for: handling “calculations I must stop and ask the user informations”. That is kind of irrelevant to the question title, but pertinent to the body text. Please try to focus on one question per question. – jewelsea May 28 '23 at 06:43

1 Answers1

3

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);
    }
}
Slaw
  • 37,820
  • 8
  • 53
  • 80