Kamil Markiewicz
Kamil Markiewicz

Reputation: 25

How to get a scheduled event to interact with JavaFX GUI

I'm a beginner Java programmer trying to figure this out. I have a piece of code that does some calculation and updates a label in my JavaFX GUI. It runs every 100ms using a ScheduledExecutorService and a Runnable. The problem is it cannot update the Label of the GUI. I have spent yesterday looking for a way to do it and most of the topics seem to be solved with the use of Platform.runLater but even putting my code into the runLater runnable seems to still not work. Another thing I have found is using the Java concurrency framework, but I don't know how to use that for a repeating scheduled service like this. Here's how I wrote the code:

ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
    Runnable loop = new Runnable() {
        public void run() {
             Platform.runLater(new Runnable() {
                 @Override public void run() {
                     double result = calculation();
                     labelResult.setText("" + result);
                 }
             });
        }
    };
    executor.scheduleAtFixedRate(loop, 0, 100, TimeUnit.MILLISECONDS);

How could I do this?

EDIT: I'm including a full example. Main class:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javafx.application.Application;
import javafx.application.Platform;

public class Main{
    private static long value = 0;
    private static Gui gui;

    public static void main(String[] args){

        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
         Runnable loop = new Runnable() {
                public void run() {
                     Platform.runLater(new Runnable() {
                         @Override public void run() {
                             calculate();
                         }
                     });
                }
            };
        executor.scheduleAtFixedRate(loop, 0, 100, TimeUnit.MILLISECONDS);

        Application.launch(Gui.class, args);
    }

    public static void calculate(){
        double result = value++;
        gui.setResult(result);
    }

    public static void setGui(Gui ref){
        gui = ref;
    }
}

Gui class:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class Gui extends Application{
    private Stage window;
    private Scene scene;
    private HBox layout = new HBox();
    private Label result = new Label("TEST");

    @Override
    public void start(Stage stage) throws Exception {
        window = stage;

        layout.getChildren().addAll(result);

        Main.setGui(this);

        scene = new Scene(layout, 1280, 720);
        window.setTitle("Example");
        window.setResizable(false);
        window.setScene(scene);
        window.show();
    }

    public void setResult(double res){
        result.setText("" + res);
    }
}

Upvotes: 1

Views: 2488

Answers (1)

James_D
James_D

Reputation: 209408

The overall structure of your application is wrong. The reason that your scheduled executor service is failing is that you attempt to start it before you launch the JavaFX application, and consequently your first call to Platform.runLater(...) happens before the FX toolkit has been started and before the FX Application Thread is running.

If you wrap the call to Platform.runLater() in a try block and catch the exception:

Runnable loop = new Runnable() {
    public void run() {
        try {
            Platform.runLater(new Runnable() {
                @Override
                public void run() {
                    calculate();
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
};

you will see the exception:

java.lang.IllegalStateException: Toolkit not initialized
    at com.sun.javafx.application.PlatformImpl.runLater(PlatformImpl.java:273)
    at com.sun.javafx.application.PlatformImpl.runLater(PlatformImpl.java:268)
    at javafx.application.Platform.runLater(Platform.java:83)
    at Main$1.run(Main.java:17)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
    at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

(Incidentally, handling the exception will also allow the executor to continue, so eventually it will "recover" as the toolkit will be started at some point. You may also see other exceptions, because, e.g. there are race conditions on the gui field: some iterations of the executor may get called before gui is initialized.)

You should think of the Application.start() method essentially as the entry point for the application. When you call launch() (or when it is called for you, which happens in most final deployment scenarios), the FX Toolkit is started, then an instance of the Application subclass is created, and start() is invoked on that instance on the FX Application Thread.

So the way to structure this is to drive it all from the start() method. Create an instance of your GUI class there, create an instance of the class that is running your scheduled executor, tie them together, and then just display the UI in the provided stage. Here's one possible example of this refactoring:

Main.java:

import javafx.application.Application;
import javafx.stage.Stage;

public class Main extends Application{
    private Stage window;

    @Override
    public void start(Stage stage) throws Exception {
        window = stage;

        Gui gui = new Gui();
        UpdateService service = new UpdateService(gui);
        service.startService();

        window.setTitle("Example");
        window.setResizable(false);
        window.setScene(gui.getScene());
        window.show();
    }

    public static void main(String[] args) {
        launch(args);
    }


}

UpdateService.java:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javafx.application.Platform;

public class UpdateService {
    private long value = 0;
    private final Gui gui;

    public UpdateService(Gui gui) {
        this.gui = gui;
    }

    public void startService() {

        // create executor that uses daemon threads;
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1, runnable -> {
            Thread t = new Thread(runnable);
            t.setDaemon(true);
            return t;
        });
        Runnable loop = new Runnable() {
            public void run() {
                Platform.runLater(new Runnable() {
                    @Override
                    public void run() {
                        calculate();
                    }
                });
            }
        };

        executor.scheduleAtFixedRate(loop, 0, 100, TimeUnit.MILLISECONDS);

    }

    public void calculate() {
        double result = value++;
        gui.setResult(result);
    }

}

Gui.java:

import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;

public class Gui {

    private Scene scene;
    private HBox layout = new HBox();
    private Label result = new Label("TEST");

    public Gui() {
        layout.getChildren().addAll(result);
        scene = new Scene(layout, 1280, 720);
    }

    public Scene getScene() {
        return scene ;
    }

    public void setResult(double res){
        result.setText("" + res);
    }
}

Finally, note that a cleaner way to get regularly-repeating functionality that runs on the FX Application Thread is to use the Animation API (as in JavaFX periodic background task):

public void startService() {
    Timeline timeline = new Timeline(new KeyFrame(Duration.millis(100), e -> calculate()));
    timeline.setCycleCount(Animation.INDEFINITE);
    timeline.play();
}

Upvotes: 2

Related Questions