Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Updating UI from different threads in JavaFX

I am developing an application with several TextField objects that need to be updated to reflect changes in associated back-end properties. The TextFields are not editable, only the back-end may change their content.

As I understand, the correct way about this is to run the heavy computation on a separate thread so as not to block the UI. I did this using javafx.concurrent.Task and communicated a single value back to the JavaFX thread using updateMessage(), which worked well. However, I need more than one value to be updated as the back-end does its crunching.

Since the back-end values are stored as JavaFX properties, I tried simply binding them to the textProperty of each GUI element and let the bindings do the work. This doesn't work, however; after running for a few moments, the TextFields stop updating even though the back-end task is still running. No exceptions are raised.

I also tried using Platform.runLater() to actively update the TextFields rather than binding. The issue here is that the runLater() tasks are scheduled faster than the platform can run them, and so the GUI becomes sluggish and needs to time to "catch up" even after the back-end task is finished.

I found a few questions on here:

Logger entries translated to the UI stops being updated with time

Multithreading in JavaFX hangs the UI

but my issue persists.

In summary: I have a back-end making changes to properties, and I want those changes to appear on the GUI. The back-end is a genetic algorithm, so its operation is broken down into discrete generations. What I would like is for the TextFields to refresh at least once in between generations, even if this delays the next generation. It is more important that the GUI responds well than that the GA runs fast.

I can post a few code examples if I haven't made the issue clear.

UPDATE

I managed to do it following James_D's suggestion. To solve the issue of the back-end having to wait for the console to print, I implemented a buffered console of sorts. It stores the strings to print in a StringBuffer and actually appends them to the TextArea when a flush() method is called. I used an AtomicBoolean to prevent the next generation from happening until the flush is complete, as it is done by a Platform.runLater() runnable. Also note that this solution is incredibly slow.

like image 357
eddy Avatar asked Mar 31 '14 21:03

eddy


People also ask

Can we update UI from thread?

However, note that you cannot update the UI from any thread other than the UI thread or the "main" thread. To fix this problem, Android offers several ways to access the UI thread from other threads. Here is a list of methods that can help: Activity.

Is JavaFX multithreaded?

JavaFX provides a complete package to deal with the issues of multithreading and concurrency. There is an interface called Worker, an abstract class called Task, and ScheduledService for this purpose. The Task is basically a Worker implementation, ideal for implementing long running computation.

Is JavaFX thread safe?

The JavaFX scene graph, which represents the graphical user interface of a JavaFX application, is not thread-safe and can only be accessed and modified from the UI thread also known as the JavaFX Application thread.


1 Answers

Not sure if I completely understand, but I think this may help.

Using Platform.runLater(...) is an appropriate approach for this.

The trick to avoiding flooding the FX Application Thread is to use an Atomic variable to store the value you're interested in. In the Platform.runLater method, retrieve it and set it to a sentinel value. From your background thread, update the Atomic variable, but only issue a new Platform.runLater if it's been set back to its sentinel value.

I figured this out by looking at the source code for Task. Have a look at how the updateMessage method (line 1131 at the time of writing) is implemented.

Here's an example which uses the same technique. This just has a (busy) background thread which counts as fast as it can, updating an IntegerProperty. An observer watches that property and updates an AtomicInteger with the new value. If the current value of the AtomicInteger is -1, it schedules a Platform.runLater.

In the Platform.runLater, I retrieve the value of the AtomicInteger and use it to update a Label, setting the value back to -1 in the process. This signals that I am ready for another UI update.

import java.text.NumberFormat; import java.util.concurrent.atomic.AtomicInteger; import javafx.application.Application; import javafx.application.Platform; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.AnchorPane; import javafx.stage.Stage;  public class ConcurrentModel extends Application {    @Override   public void start(Stage primaryStage) {          final AtomicInteger count = new AtomicInteger(-1);          final AnchorPane root = new AnchorPane();     final Label label = new Label();     final Model model = new Model();     final NumberFormat formatter = NumberFormat.getIntegerInstance();     formatter.setGroupingUsed(true);     model.intProperty().addListener(new ChangeListener<Number>() {       @Override       public void changed(final ObservableValue<? extends Number> observable,           final Number oldValue, final Number newValue) {         if (count.getAndSet(newValue.intValue()) == -1) {           Platform.runLater(new Runnable() {             @Override             public void run() {               long value = count.getAndSet(-1);               label.setText(formatter.format(value));             }           });                   }        }     });     final Button startButton = new Button("Start");     startButton.setOnAction(new EventHandler<ActionEvent>() {       @Override       public void handle(ActionEvent event) {         model.start();       }     });      AnchorPane.setTopAnchor(label, 10.0);     AnchorPane.setLeftAnchor(label, 10.0);     AnchorPane.setBottomAnchor(startButton, 10.0);     AnchorPane.setLeftAnchor(startButton, 10.0);     root.getChildren().addAll(label, startButton);      Scene scene = new Scene(root, 100, 100);     primaryStage.setScene(scene);     primaryStage.show();   }    public static void main(String[] args) {     launch(args);   }    public class Model extends Thread {     private IntegerProperty intProperty;      public Model() {       intProperty = new SimpleIntegerProperty(this, "int", 0);       setDaemon(true);     }      public int getInt() {       return intProperty.get();     }      public IntegerProperty intProperty() {       return intProperty;     }      @Override     public void run() {       while (true) {         intProperty.set(intProperty.get() + 1);       }     }   } } 

If you really want to "drive" the back end from the UI: that is throttle the speed of the backend implementation so you see all updates, consider using an AnimationTimer. An AnimationTimer has a handle(...) which is called once per frame render. So you could block the back-end implementation (for example by using a blocking queue) and release it once per invocation of the handle method. The handle(...) method is invoked on the FX Application Thread.

The handle(...) method takes a parameter which is a timestamp (in nanoseconds), so you can use that to slow the updates further, if once per frame is too fast.

For example:

import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue;  import javafx.animation.AnimationTimer; import javafx.application.Application; import javafx.beans.property.LongProperty; import javafx.beans.property.SimpleLongProperty; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.stage.Stage; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.TextArea; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox;   public class Main extends Application {     @Override     public void start(Stage primaryStage) {                  final BlockingQueue<String> messageQueue = new ArrayBlockingQueue<>(1);                  TextArea console = new TextArea();                  Button startButton = new Button("Start");         startButton.setOnAction(event -> {             MessageProducer producer = new MessageProducer(messageQueue);             Thread t = new Thread(producer);             t.setDaemon(true);             t.start();         });                  final LongProperty lastUpdate = new SimpleLongProperty();                  final long minUpdateInterval = 0 ; // nanoseconds. Set to higher number to slow output.                  AnimationTimer timer = new AnimationTimer() {              @Override             public void handle(long now) {                 if (now - lastUpdate.get() > minUpdateInterval) {                     final String message = messageQueue.poll();                     if (message != null) {                         console.appendText("\n" + message);                     }                     lastUpdate.set(now);                 }             }                      };                  timer.start();                  HBox controls = new HBox(5, startButton);         controls.setPadding(new Insets(10));         controls.setAlignment(Pos.CENTER);                  BorderPane root = new BorderPane(console, null, null, controls, null);         Scene scene = new Scene(root,600,400);         primaryStage.setScene(scene);         primaryStage.show();     }          private static class MessageProducer implements Runnable {         private final BlockingQueue<String> messageQueue ;                  public MessageProducer(BlockingQueue<String> messageQueue) {             this.messageQueue = messageQueue ;         }                  @Override         public void run() {             long messageCount = 0 ;             try {                 while (true) {                     final String message = "Message " + (++messageCount);                     messageQueue.put(message);                 }             } catch (InterruptedException exc) {                 System.out.println("Message producer interrupted: exiting.");             }         }     }          public static void main(String[] args) {         launch(args);     } } 
like image 140
James_D Avatar answered Sep 18 '22 19:09

James_D