I have one progress indicator on my main screen UI that is shared by various tabs and services. Each TabController has its own instance of Service. In my MainController class, for each tab I have bound each Service's progress property to the ProgressIndicator.
@FXML
Region veil;
@FXML
ProgressIndicator progressDial;
progressDial.progressProperty().bind(tabController.commandService.progressProperty());
veil.visibleProperty().bind(tabController.commandService.runningProperty());
progressDial.visibleProperty().bind(tabController.commandService.runningProperty());
tabController.commandService.messageProperty().addListener(new ChangeListener<String>() {
@Override
public void changed(ObservableValue<? extends String> ov, String t, String newValue) {
addCommentary(newValue);
}
});
However I see that after the first service uses it, the progress dial does not appear for the execution of subsequent services or tasks. I am wondering if I am misusing the ProgressIndicator since each Service probably runs concurrently. I am guessing that the progress wasn't reset after the first finished. How do I reset it? The progress property is read only.
ReadOnlyDoubleProperty progressProperty() Gets the ReadOnlyDoubleProperty representing the progress.
And calling updateProgress(0) does nothing to make the dial reappear.
I tried to explicitly reset it using the ProgressIndicator as a global
mainController.progressDial.setProgress(0);
but this failed
java.lang.RuntimeException: A bound value cannot be set.
at javafx.beans.property.DoublePropertyBase.set(DoublePropertyBase.java:159)
I could be mistaken, but I think this is a fault in the JavaFX UI controls design. Updating progress to 0 should reset the progress Indicator.
There is a bit of writing in my answer because it's not exactly clear to me from your question what is going wrong with your instance. Hopefully either the explanation or the sample code in the answer is useful.
I could be mistaken, but I think this is a fault in the JavaFX UI controls design. Updating progress to 0 should reset the progress Indicator.
You are slightly mistaken. You have bound the progress of the indicator to the progress of a task. The task is completed and progress is 1. Now if you want to re-use the same indicator for another task or make it measure the progress of something else, you have to first stop it from measuring the progress of the original task. To disassociate the progress indicator for the original task, unbind it's progress. Once the progress indicator's progress is no longer bound to the original task's progress, you are free to set the indicator to whatever value you want, or bind it to something else.
Similarly, you can only bind the progress indicator's progress to one thing at a time (unless you bi-directionally bind the indicator, which you can't do with task progress because task progress is read only and bi-directionally binding to multiple task progress values would be incorrect anyway as each task would be at a different progress point).
make the dial reappear.
I'm not sure from your description why the dial would disappear in the first place so that it would need to reappear. Normally, when a progress indicator's progress reaches 1, it still stays visible reporting fully completed progress, it doesn't automatically disappear. You are likely setting the visibility of the indicator to false or modifying it's opacity to zero. Both of those properties have nothing to do with the actual progress measured by the indicator. Or maybe you are removing the indicator from the displayed scene. If you are modifying visibility and setting the indicator to invisible after a task is completed and you want to subsequently see it again to measure the progress of another task, then you will need to make sure it is in the scene, with opacity > 0 and visibility set to true.
A suggestion
You can only run a task once, so after it is done, it doesn't make a lot of sense to set it's progress back to zero if it had already made some progress.
Property types
A progress indicator's progress property is a plain DoubleProperty
, not a ReadOnlyDoubleProperty
, so it is directly settable (as long as it is not bound to another value).
A task's progress property which is read only and must be changed via updateProgress. The task's progress property was likely made read only so that updates to it can be ensured to be threadsafe by special code in the updateProgress
routine.
Sample Code
Consider the following code which (I believe) accomplishes the intent of what you are trying to do. The code simulates running a triathlon where each stage (swim, bike, run) of the triathlon is a separate task. While a triathlon is being run, a progress indicator shows the progress of each stage of the triathlon. When the triathlon completes the progress indicator fades away until a new triathlon is started. Sorry the sample is so lengthy, I found it hard to come up with something more concise.
import javafx.animation.FadeTransition;
import javafx.application.Application;
import javafx.beans.*;
import javafx.beans.property.*;
import javafx.beans.value.*;
import javafx.concurrent.Task;
import javafx.event.*;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Triathlon extends Application {
private final Random random = new Random();
private final ExecutorService exec = Executors.newSingleThreadExecutor();
@Override public void start(Stage stage) throws Exception {
final TaskMonitor taskMonitor = new TaskMonitor();
final ProgressIndicator progressIndicator = new ProgressIndicator();
progressIndicator.progressProperty().bind(
taskMonitor.currentTaskProgressProperty()
);
final Label currentRaceStage = new Label();
currentRaceStage.textProperty().bind(
taskMonitor.currentTaskNameProperty()
);
createMainLayout(
stage,
createStartRaceButton(
exec,
taskMonitor
),
createRaceProgressView(
taskMonitor,
progressIndicator,
currentRaceStage
)
);
}
@Override public void stop() throws Exception {
exec.shutdownNow();
}
private Button createStartRaceButton(final ExecutorService exec, final TaskMonitor taskMonitor) {
final Button startButton = new Button("Start Race");
startButton.disableProperty().bind(taskMonitor.idleProperty().not());
startButton.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent actionEvent) {
runRace(exec, taskMonitor);
}
});
return startButton;
}
private HBox createRaceProgressView(final TaskMonitor taskMonitor, ProgressIndicator progressIndicator, Label currentRaceStage) {
final HBox raceProgress = new HBox(10);
raceProgress.getChildren().setAll(
currentRaceStage,
progressIndicator
);
raceProgress.setOpacity(0);
raceProgress.setAlignment(Pos.CENTER);
final FadeTransition fade = new FadeTransition(Duration.seconds(0.75), raceProgress);
fade.setToValue(0);
taskMonitor.idleProperty().addListener(new InvalidationListener() {
@Override
public void invalidated(Observable observable) {
if (taskMonitor.idleProperty().get()) {
fade.playFromStart();
} else {
fade.stop();
raceProgress.setOpacity(1);
}
}
});
return raceProgress;
}
private void createMainLayout(Stage stage, Button startButton, HBox raceProgress) {
final VBox layout = new VBox(10);
layout.getChildren().setAll(
raceProgress,
startButton
);
layout.setAlignment(Pos.CENTER);
layout.setStyle("-fx-background-color: cornsilk; -fx-padding: 10px;");
stage.setScene(new Scene(layout, 200, 130));
stage.show();
}
private void runRace(ExecutorService exec, TaskMonitor taskMonitor) {
StageTask swimTask = new StageTask("Swim", 30, 40);
StageTask bikeTask = new StageTask("Bike", 210, 230);
StageTask runTask = new StageTask("Run", 120, 140);
taskMonitor.monitor(swimTask, bikeTask, runTask);
exec.execute(swimTask);
exec.execute(bikeTask);
exec.execute(runTask);
}
class TaskMonitor {
final private ReadOnlyObjectWrapper<StageTask> currentTask = new ReadOnlyObjectWrapper<>();
final private ReadOnlyStringWrapper currentTaskName = new ReadOnlyStringWrapper();
final private ReadOnlyDoubleWrapper currentTaskProgress = new ReadOnlyDoubleWrapper();
final private ReadOnlyBooleanWrapper idle = new ReadOnlyBooleanWrapper(true);
public void monitor(final StageTask task) {
task.stateProperty().addListener(new ChangeListener<Task.State>() {
@Override
public void changed(ObservableValue<? extends Task.State> observableValue, Task.State oldState, Task.State state) {
switch (state) {
case RUNNING:
currentTask.set(task);
currentTaskProgress.unbind();
currentTaskProgress.set(task.progressProperty().get());
currentTaskProgress.bind(task.progressProperty());
currentTaskName.set(task.nameProperty().get());
idle.set(false);
break;
case SUCCEEDED:
case CANCELLED:
case FAILED:
task.stateProperty().removeListener(this);
idle.set(true);
break;
}
}
});
}
public void monitor(final StageTask... tasks) {
for (StageTask task: tasks) {
monitor(task);
}
}
public ReadOnlyObjectProperty<StageTask> currentTaskProperty() {
return currentTask.getReadOnlyProperty();
}
public ReadOnlyStringProperty currentTaskNameProperty() {
return currentTaskName.getReadOnlyProperty();
}
public ReadOnlyDoubleProperty currentTaskProgressProperty() {
return currentTaskProgress.getReadOnlyProperty();
}
public ReadOnlyBooleanProperty idleProperty() {
return idle.getReadOnlyProperty();
}
}
class StageTask extends Task<Duration> {
final private ReadOnlyStringWrapper name;
final private int minMinutesElapsed;
final private int maxMinutesElapsed;
public StageTask(String name, int minMinutesElapsed, int maxMinutesElapsed) {
this.name = new ReadOnlyStringWrapper(name);
this.minMinutesElapsed = minMinutesElapsed;
this.maxMinutesElapsed = maxMinutesElapsed;
}
@Override protected Duration call() throws Exception {
Duration duration = timeInRange(
minMinutesElapsed, maxMinutesElapsed
);
for (int i = 0; i < 25; i++) {
updateProgress(i, 25);
Thread.sleep((int) (duration.toMinutes()));
}
updateProgress(25, 25);
return duration;
}
private Duration timeInRange(int min, int max) {
return Duration.minutes(
random.nextDouble() * (max - min) + min
);
}
public ReadOnlyStringProperty nameProperty() {
return name.getReadOnlyProperty();
}
}
public static void main(String[] args) {
Application.launch(Triathlon.class);
}
}
Update for Additional Question
Instead of being a triathlon, suppose each stage was instead, an independent event (like in the Olympics). So swim, bike, run etc. are instances of SportService. They execute concurrently. On the stadium electronic scoreboard is a progress indicator dial that is shared by all SportServices swim, bike, run etc. It gives me the approximate general progress - though I realize that is vague but is a summary of how everything is progressing without seeing the details of each event.
Run the events in parallel using the mechanism defined in Creating multiple parallel tasks. Create a single progress indicator for your overall olympics progress and bind it to the progress of the sum of progress for all tasks using the low level binding api.
ObservableList<Service> services = FXCollections.observableArrayList();
. . . add services to list.
// extract the progress property for each of the added services.
final ReadOnlyDoubleProperty[] taskProgressList = new ReadOnlyDoubleProperty[services.size()];
for (int i = 0; i < taskProgressList.length; i++) {
taskProgressList[i] = services.get(i).progressProperty();
}
// calculate the average progress of all services.
DoubleBinding overallProgress = Bindings.createDoubleBinding(new Callable<Double>() {
@Override public Double call() throws Exception {
double value = 0;
for (int i = 0; i < taskProgressList.length; i++) {
value += taskProgressList[i].get();
}
value /= taskProgressList.length;
return value;
}
}, taskProgressList);
// bind the overall progress to our indicator
ProgressIndicator overallProgressIndicator = new ProgressIndicator();
overallProgressIndicator.progressProperty().bind(overallProgress);
Here is a another sample which demonstrates use of the overallProgress DoubleBinding
.
import javafx.application.Application;
import javafx.beans.binding.*;
import javafx.beans.property.*;
import javafx.collections.*;
import javafx.concurrent.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class FirstLineSequentialVsParallelService extends Application {
private static final String[] URLs = {
"https://www.google.com",
"https://www.yahoo.com",
"https://www.microsoft.com",
"https://www.oracle.com"
};
private static final String CSS_DATA_URL = "data:text/css,";
private static final String MESSAGES_STYLE = // language=CSS
"""
.messages {
-fx-background-color: cornsilk;
-fx-padding: 10;
}
.label {
-fx-font-family: monospace;
}
""";
private static final String MESSAGES_CSS = CSS_DATA_URL + MESSAGES_STYLE;
private ExecutorService sequentialFirstLineExecutor;
private ExecutorService parallelFirstLineExecutor;
@Override
public void init() {
sequentialFirstLineExecutor = Executors.newFixedThreadPool(
1,
new FirstLineThreadFactory("sequential")
);
parallelFirstLineExecutor = Executors.newFixedThreadPool(
URLs.length,
new FirstLineThreadFactory("parallel")
);
}
@SuppressWarnings("ResultOfMethodCallIgnored")
@Override
public void stop() throws Exception {
parallelFirstLineExecutor.shutdown();
parallelFirstLineExecutor.awaitTermination(3, TimeUnit.SECONDS);
sequentialFirstLineExecutor.shutdown();
sequentialFirstLineExecutor.awaitTermination(3, TimeUnit.SECONDS);
}
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
final VBox messages = new VBox();
messages.getStylesheets().add(MESSAGES_CSS);
messages.getStyleClass().add("messages");
messages.getChildren().addAll(
new Label("Parallel Execution"),
new Label("------------------")
);
DoubleBinding parallelProgress = fetchFirstLines(messages, parallelFirstLineExecutor);
ProgressMonitoredLabel parallelProgressTotalProgress = new ProgressMonitoredLabel("Parallel Execution Total Progress");
parallelProgressTotalProgress.progress.progressProperty().bind(parallelProgress);
messages.getChildren().add(parallelProgressTotalProgress);
messages.getChildren().addAll(
new Label("Sequential Execution"),
new Label("--------------------")
);
DoubleBinding sequentialProgress = fetchFirstLines(messages, sequentialFirstLineExecutor);
ProgressMonitoredLabel sequentialTotalProgress = new ProgressMonitoredLabel("Sequential Execution Total Progress");
sequentialTotalProgress.progress.progressProperty().bind(sequentialProgress);
messages.getChildren().add(sequentialTotalProgress);
stage.setScene(new Scene(messages, 600, 650));
stage.show();
}
private DoubleBinding fetchFirstLines(final VBox monitoredLabels, ExecutorService executorService) {
ObservableList<FirstLineService> services = FXCollections.observableArrayList();
for (final String url : URLs) {
final FirstLineService service = new FirstLineService();
service.setExecutor(executorService);
service.setUrl(url);
final ProgressMonitoredLabel monitoredLabel = new ProgressMonitoredLabel(url);
monitoredLabels.getChildren().add(monitoredLabel);
monitoredLabel.progress.progressProperty().bind(service.progressProperty());
service.setOnSucceeded(t ->
monitoredLabel.addStrings(
service.getMessage(),
service.getValue()
)
);
service.start();
services.add(service);
}
final ReadOnlyDoubleProperty[] taskProgressList = new ReadOnlyDoubleProperty[services.size()];
for (int i = 0; i < taskProgressList.length; i++) {
taskProgressList[i] = services.get(i).progressProperty();
}
return Bindings.createDoubleBinding(() -> {
double value = 0;
for (ReadOnlyDoubleProperty readOnlyDoubleProperty : taskProgressList) {
value += readOnlyDoubleProperty.get();
}
value /= taskProgressList.length;
return value;
}, taskProgressList);
}
public static class ProgressMonitoredLabel extends HBox {
final ProgressBar progress;
final VBox labels;
public ProgressMonitoredLabel(String initialString) {
super(20);
progress = new ProgressBar();
labels = new VBox();
labels.getChildren().addAll(new Label(initialString), new Label());
progress.setPrefWidth(100);
progress.setMinWidth(ProgressBar.USE_PREF_SIZE);
HBox.setHgrow(labels, Priority.ALWAYS);
setMinHeight(60);
getChildren().addAll(progress, labels);
}
public void addStrings(String... strings) {
for (String string : strings) {
labels.getChildren().add(
labels.getChildren().size() - 1,
new Label(string)
);
}
}
}
public static class FirstLineService extends Service<String> {
private final StringProperty url = new SimpleStringProperty(this, "url");
public final void setUrl(String value) {
url.set(value);
}
public final String getUrl() {
return url.get();
}
protected Task<String> createTask() {
final String _url = getUrl();
return new Task<>() {
{
updateProgress(0, 100);
}
protected String call() throws Exception {
updateMessage("Called on thread: " + Thread.currentThread().getName());
URL u = URI.create(_url).toURL();
BufferedReader in = new BufferedReader(
new InputStreamReader(u.openStream()));
String result = in.readLine();
in.close();
// pause just so that it really takes some time to run the task
// so that parallel execution behaviour can be observed.
for (int i = 0; i < 100; i++) {
updateProgress(i, 100);
Thread.sleep(50);
}
return result;
}
};
}
}
static class FirstLineThreadFactory implements ThreadFactory {
static final AtomicInteger poolNumber = new AtomicInteger(1);
private final String type;
public FirstLineThreadFactory(String type) {
this.type = type;
}
@Override
public Thread newThread(Runnable runnable) {
Thread thread = new Thread(runnable, "LineService-" + poolNumber.getAndIncrement() + "-thread-" + type);
thread.setDaemon(true);
return thread;
}
}
}