我正在JavaFX中为一个相当大的Java项目构建一个GUI。这个项目有许多不同的工作线程在后台进行一些繁重的计算,我试图在GUI中可视化这些工作线程的进度。通过进展,我的意思不仅仅是一个百分比,还包括Task类中未包含的其他变量,例如:(例如):
由于这些进度变量变化非常快,并且因为我必须从JavaFX线程(Platform.runLater())执行GUI更新,所以JavaFX事件队列很快就会过载。我试图通过构建一个能够从JavaFX线程外部异步更新GUI属性的实用程序类来解决这个问题。应跳过快速连续更新,以便仅显示最新值,从而避免使用Runnables群集JavaFX事件队列。
因此,我构建了以下类GUIUpdater
来将Properties(通常是一个GUI元素,如Label)绑定到ObservableValues(如SimpleStringProperty)。这个类有两个InnerClasses:
PropertyUpdater
负责将单个Property绑定到单个ObservableValue并进行更新。Updater
为Platform.runLater()提供了一个可重用的Runnable对象。实用程序类:
package main;
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 class GUIUpdater {
private ConcurrentLinkedQueue<PropertyUpdater<?>> dirtyPropertyUpdaters = new ConcurrentLinkedQueue<>();
private Updater updater = new Updater();
private 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 <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 <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 void scheduleUpdate(PropertyUpdater<?> updater) {
this.dirtyPropertyUpdaters.add(updater);
// Make sure the isUpdating var isn't changed concurrently by the Updater thread (on the JavaFX event queue)
synchronized (this) {
if (!this.isUpdating) {
this.isUpdating = true;
Platform.runLater(this.updater);
}
}
}
/**
* Class used for binding a single ObservableValue to a Property and updating it.
*
* @param <T>
*/
private 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.this.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 class Updater implements Runnable {
@Override
public void run() {
// Loop through the individual PropertyUpdaters, updating them one by one:
while(!GUIUpdater.this.dirtyPropertyUpdaters.isEmpty()) {
PropertyUpdater<?> curUpdater = GUIUpdater.this.dirtyPropertyUpdaters.poll();
curUpdater.update();
}
// Make sure we're not clearing the mark when scheduleUpdate() is still setting it:
synchronized (GUIUpdater.this) {
GUIUpdater.this.isUpdating = false;
}
}
}
}
这是一个用于测试GUIUpdater
实用程序类的简单类:
package main;
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 GUIUpdater guiUpdater = new GUIUpdater();
private Label lblState = 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 Label and a progressBar:
flowPane.getChildren().add(this.lblState);
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:
JavaFXTest.this.guiUpdater.bind(JavaFXTest.this.lblState.textProperty(), testTask.myStateProperty());
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 myState = new SimpleStringProperty();
@Override
protected Void call() throws Exception {
// Count:
try {
int maxValue = 1000000;
System.out.println("Starting...");
for(int i = 0; i < maxValue; i++) {
this.updateProgress(i, maxValue - 1);
this.myState.set("Hello " + i);
}
System.out.println("Done!");
} catch(Exception e) {
e.printStackTrace();
}
// Unbind:
JavaFXTest.this.guiUpdater.unbind(JavaFXTest.this.lblState.textProperty(), this.myStateProperty());
return null;
}
public SimpleStringProperty myStateProperty() {
return this.myState;
}
}
}
代码的问题是有时Label没有更新到最新值(在本例中为999999)。它似乎主要发生在应用程序启动后,因此启动应用程序,单击“开始”按钮,关闭它并重复此过程应该在几次尝试后复制问题。据我所知,我已经在需要的地方添加了synchronized
块,这就是为什么我不明白问题的来源。
即使我主要寻找所描述问题的解决方案,所有建议都非常受欢迎(即使是那些与问题无关的建议)!我在代码中也添加了注释,所以我希望与上面的信息一起提供有关问题和代码的足够详细信息。
提前致谢!
答案 0 :(得分:0)
我相信此功能可以通过Task
的{{3}}:
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);
...
}
答案 1 :(得分:0)
我能够自己修复这个问题。在各个位置添加System.out
几天后,问题是由于isUpdating
变量的并发问题。当JavaFX线程位于while
循环和synchronized
中的Updater.run
块之间时,会出现此问题。我通过在同一个对象上同步Updater.run
和GUIUpdater.scheduleUpdate
方法解决了这个问题。
我还将GUIUpdater
变成了一个仅限静态的对象,因为有多个实例会将Runnables
放在JavaFX事件队列中而不管其他GUIUpdater
个实例,从而阻塞事件队列。总而言之,这是结果GUIUpdater
类:
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;
}
}
}
}
这是测试人员类的略微更新版本(我不会详细介绍这个版本,因为它完全不重要):
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;
}
}
}