I\'m building a GUI in JavaFX for a rather large Java project. This project has many different worker threads doing some heavy computations in the background and I\'m trying
I believe this functionality can be achieved through the Task
's messageProperty:
public void handle(ActionEvent event) {
...
JavaFXTest.this.lblState.textProperty().bind(testTask.messageProperty());
...
}
...
protected Void call() throws Exception {
...
this.updateProgress(i, maxValue - 1);
this.updateMessage("Hello " + i);
...
}
I was able to fix the issue myself. After a few days of adding System.out
in various places it turned out the problem was due to concurrency issues with the isUpdating
variable. The problem occured when the JavaFX thread was inbetween the while
loop and the synchronized
block in Updater.run
. I solved the problem by making both the Updater.run
and GUIUpdater.scheduleUpdate
methods synchronized on the same object.
I also made the GUIUpdater
into a static-only object as having multiple instances will place Runnables
in the JavaFX event queue regardless of the other GUIUpdater
instances, clogging up the event queue. All in all, this is the resulting GUIUpdater
class:
package be.pbeckers.javafxguiupdater;
import java.util.concurrent.ConcurrentLinkedQueue;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
/**
* Class for enabling fast updates of GUI components from outside the JavaFX thread.
* Updating GUI components (such as labels) should be done from the JavaFX thread by using Platform.runLater for example.
* This makes it hard to update the GUI with a fast changing variable as it is very easy to fill up the JavaFX event queue faster than it can be emptied (i.e. faster than it can be drawn).
* This class binds ObservableValues to (GUI) Properties and ensures that quick consecutive updates are ignored, only updating to the latest value.
*/
public abstract class GUIUpdater {
private static ConcurrentLinkedQueue<PropertyUpdater<?>> dirtyPropertyUpdaters = new ConcurrentLinkedQueue<>();
private static Updater updater = new Updater();
private static boolean isUpdating = false;
/**
* Binds an ObservableValue to a Property.
* Updates to the ObservableValue can be made from outside the JavaFX thread and the latest update will be reflected in the Property.
* @param property (GUI) Property to be updated/
* @param observable ObservableValue to update the GUI property to.
*/
public static <T> void bind(Property<T> property, ObservableValue<T> observable) {
PropertyUpdater<T> propertyUpdater = new PropertyUpdater<>(property, observable);
observable.addListener(propertyUpdater);
}
/**
* Unbinds the given ObservableValue from the given Property.
* Updates to the ObservableValue will no longer be reflected in the Property.
* @param property (GUI) Property to unbind the ObservableValue from.
* @param observable ObservableValue to unbind from the given Property.
*/
public static <T> void unbind(Property<T> property, ObservableValue<T> observable) {
PropertyUpdater<T> tmpPropertyUpdater = new PropertyUpdater<>(property, observable);
observable.removeListener(tmpPropertyUpdater);
}
/**
* Schedules an update to the GUI by using a call to Platform.runLater().
* The updated property is added to the dirtyProperties list, marking it for the next update round.
* Will only submit the event to the event queue if the event isn't in the event queue yet.
* @param updater
*/
private static synchronized void scheduleUpdate(PropertyUpdater<?> updater) {
GUIUpdater.dirtyPropertyUpdaters.add(updater);
if (!GUIUpdater.isUpdating) {
GUIUpdater.isUpdating = true;
Platform.runLater(GUIUpdater.updater);
}
}
/**
* Class used for binding a single ObservableValue to a Property and updating it.
*
* @param <T>
*/
private static class PropertyUpdater<T> implements ChangeListener<T> {
private boolean isDirty = false;
private Property<T> property = null;
private ObservableValue<T> observable = null;
public PropertyUpdater(Property<T> property, ObservableValue<T> observable) {
this.property = property;
this.observable = observable;
}
@Override
/**
* Called whenever the ObservableValue has changed. Marks this Updater as dirty.
*/
public synchronized void changed(ObservableValue<? extends T> observable, T oldValue, T newValue) {
if (!this.isDirty) {
this.isDirty = true;
GUIUpdater.scheduleUpdate(this);
}
}
/**
* Updates the Property to the ObservableValue and marks it as clean again.
* Should only be called from the JavaFX thread.
*/
public synchronized void update() {
T value = this.observable.getValue();
this.property.setValue(value);
this.isDirty = false;
}
@Override
/**
* Two PropertyUpdaters are equals if their Property and ObservableValue map to the same object (address).
*/
public boolean equals(Object otherObj) {
PropertyUpdater<?> otherUpdater = (PropertyUpdater<?>) otherObj;
if (otherObj == null) {
return false;
} else {
// Only compare addresses (comparing with equals also compares contents):
return (this.property == otherUpdater.property) && (this.observable == otherUpdater.observable);
}
}
}
/**
* Simple class containing the Runnable for the call to Platform.runLater.
* Hence, the run() method should only be called from the JavaFX thread.
*
*/
private static class Updater implements Runnable {
@Override
public void run() {
synchronized (GUIUpdater.class) {
// Loop through the individual PropertyUpdaters, updating them one by one:
while(!GUIUpdater.dirtyPropertyUpdaters.isEmpty()) {
PropertyUpdater<?> curUpdater = GUIUpdater.dirtyPropertyUpdaters.poll();
curUpdater.update();
}
// Mark as updated:
GUIUpdater.isUpdating = false;
}
}
}
}
And this is a slightly updated version of the tester class (I'm not going into detail on this one as it's totally unimportant):
package be.pbeckers.javafxguiupdater.test;
import be.pbeckers.javafxguiupdater.GUIUpdater;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.concurrent.Task;
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.control.ProgressBar;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class JavaFXTest extends Application {
private Label lblCurFile = new Label();
private Label lblErrors = new Label();
private Label lblBytesParsed = new Label();
private ProgressBar prgProgress = new ProgressBar();
public static void main(String args[]) {
JavaFXTest.launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
// Init window:
FlowPane flowPane = new FlowPane();
primaryStage.setScene(new Scene(flowPane));
primaryStage.setTitle("JavaFXTest");
// Add a few Labels and a progressBar:
flowPane.getChildren().add(this.lblCurFile);
flowPane.getChildren().add(this.lblErrors);
flowPane.getChildren().add(this.lblBytesParsed);
flowPane.getChildren().add(this.prgProgress);
// Add button:
Button btnStart = new Button("Start");
btnStart.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
// Create task:
TestTask testTask = new TestTask();
// Bind:
GUIUpdater.bind(JavaFXTest.this.lblCurFile.textProperty(), testTask.curFileProperty());
GUIUpdater.bind(JavaFXTest.this.lblErrors.textProperty(), testTask.errorsProperty());
GUIUpdater.bind(JavaFXTest.this.lblBytesParsed.textProperty(), testTask.bytesParsedProperty());
JavaFXTest.this.prgProgress.progressProperty().bind(testTask.progressProperty()); // No need to use GUIUpdater here, Task class provides the same functionality for progress.
// Start task:
Thread tmpThread = new Thread(testTask);
tmpThread.start();
}
});
flowPane.getChildren().add(btnStart);
// Show:
primaryStage.show();
}
/**
* A simple task containing a for loop to simulate a fast running and fast updating process.
* @author DePhille
*
*/
private class TestTask extends Task<Void> {
private SimpleStringProperty curFile = new SimpleStringProperty();
private SimpleStringProperty errors = new SimpleStringProperty();
private SimpleStringProperty bytesParsed = new SimpleStringProperty();
@Override
protected Void call() throws Exception {
// Count:
try {
int maxValue = 1000000;
long startTime = System.currentTimeMillis();
System.out.println("Starting...");
for(int i = 0; i < maxValue; i++) {
this.updateProgress(i, maxValue - 1);
// Simulate some progress variables:
this.curFile.set("File_" + i + ".txt");
if ((i % 1000) == 0) {
//this.errors.set("" + (i / 1000) + " Errors");
}
//this.bytesParsed.set("" + (i / 1024) + " KBytes");
}
long stopTime = System.currentTimeMillis();
System.out.println("Done in " + (stopTime - startTime) + " msec!");
} catch(Exception e) {
e.printStackTrace();
}
// Unbind:
GUIUpdater.unbind(JavaFXTest.this.lblCurFile.textProperty(), this.curFileProperty());
GUIUpdater.unbind(JavaFXTest.this.lblErrors.textProperty(), this.errorsProperty());
GUIUpdater.unbind(JavaFXTest.this.lblBytesParsed.textProperty(), this.bytesParsedProperty());
return null;
}
public SimpleStringProperty curFileProperty() {
return this.curFile;
}
public SimpleStringProperty errorsProperty() {
return this.errors;
}
public SimpleStringProperty bytesParsedProperty() {
return this.bytesParsed;
}
}
}