JavaFX Update progressbar in tableview from Task

ksarunas picture ksarunas · May 23, 2013 · Viewed 12.5k times · Source

I know Task has a method updateProgress, I would need to bind progressbar to task, however I cannot do that, as I do not have progressbar as an object.

My program has a TableView. Once user enters download url and clicks download new row created in the TableView. Row has some info and progressbar column. I then start a new thread - task. Where all download is being done and I need to update progress bar in that row somehow.

I tried binding SimpleDoubleProperty to the Task but it does not update progress bar...

Answer

jewelsea picture jewelsea · May 24, 2013

James D solved this in Oracle JavaFX forum thread: Table cell progress indicator. I have just copied that solution into this answer.

The solution creates multiple tasks and monitors their progress via a set of progress bars in a TableView.

The original thread also includes a solution which uses ProgressIndicators in case you prefer those to ProgressBars.

progress

import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.ProgressIndicator ;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.ProgressBarTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class ProgressBarTableCellTest extends Application {

  @Override
  public void start(Stage primaryStage) {
    TableView<TestTask> table = new TableView<TestTask>();
    Random rng = new Random();
    for (int i = 0; i < 20; i++) {
      table.getItems().add(
          new TestTask(rng.nextInt(3000) + 2000, rng.nextInt(30) + 20));
    }

    TableColumn<TestTask, String> statusCol = new TableColumn("Status");
    statusCol.setCellValueFactory(new PropertyValueFactory<TestTask, String>(
        "message"));
    statusCol.setPrefWidth(75);

    TableColumn<TestTask, Double> progressCol = new TableColumn("Progress");
    progressCol.setCellValueFactory(new PropertyValueFactory<TestTask, Double>(
        "progress"));
    progressCol
        .setCellFactory(ProgressBarTableCell.<TestTask> forTableColumn());

    table.getColumns().addAll(statusCol, progressCol);

    BorderPane root = new BorderPane();
    root.setCenter(table);
    primaryStage.setScene(new Scene(root));
    primaryStage.show();

    ExecutorService executor = Executors.newFixedThreadPool(table.getItems().size(), new ThreadFactory() {
      @Override
      public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setDaemon(true);
        return t;
      }
    });


    for (TestTask task : table.getItems()) {
      executor.execute(task);
    }
  }

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

  static class TestTask extends Task<Void> {

    private final int waitTime; // milliseconds
    private final int pauseTime; // milliseconds

    public static final int NUM_ITERATIONS = 100;

    TestTask(int waitTime, int pauseTime) {
      this.waitTime = waitTime;
      this.pauseTime = pauseTime;
    }

    @Override
    protected Void call() throws Exception {
      this.updateProgress(ProgressIndicator.INDETERMINATE_PROGRESS, 1);
      this.updateMessage("Waiting...");
      Thread.sleep(waitTime);
      this.updateMessage("Running...");
      for (int i = 0; i < NUM_ITERATIONS; i++) {
        updateProgress((1.0 * i) / NUM_ITERATIONS, 1);
        Thread.sleep(pauseTime);
      }
      this.updateMessage("Done");
      this.updateProgress(1, 1);
      return null;
    }

  }
}

Explanatory Text Based on Comment Questions

You only need to read this section if you are having difficulties understanding how the above code works and want to gain a deeper understanding of cell value and property connections.

There is no kind of binding here (at least I do not see).

The binding (or ChangeListener, which amounts to the same thing) is hidden behind the implementation of the PropertyValueFactory and the ProgressBarTableCell. Let's look at the relevant code:

TableColumn<TestTask, Double> progressCol = new TableColumn("Progress");
progressCol.setCellValueFactory(
  new PropertyValueFactory<TestTask, Double>("progress")
);
progressCol.setCellFactory(
  ProgressBarTableCell.<TestTask> forTableColumn()
);

The progressCol is defined to take a TestTask as the data row and extract a double value out of the test task property.

The cell value factory defines how the double value for the column is populated. It is defined based upon a PropertyValueFactory which takes the parameter "progress". This tells the property value factory to use JavaFX naming conventions and the Java reflection API to lookup relevant methods to retrieve the data from a TestTask instance. In this case it will invoke a method named progressProperty() on the TestTask instance to retrieve the ReadOnlyDoubleProperty reflecting the tasks progress.

As it states in it's documentation, the PropertyValueFactory is just short hand for the mess of code below, but the key fact is that it is returning an ObservableValue which the Table implementation can use to set the value of the cell as the cell changes.

TableColumn<Person,String> firstNameCol = new TableColumn<Person,String>("First Name");
firstNameCol.setCellValueFactory(new Callback<CellDataFeatures<Person, String>, ObservableValue<String>>() {
  public ObservableValue<String> call(CellDataFeatures<Person, String> p) {
    // p.getValue() returns the Person instance for a particular TableView row
    return p.getValue().firstNameProperty();
  }
});

OK, so now we have a cell's value being reflected to the double value of the task's progress whenever the task makes any progress. But we still need to graphically represent that double value somehow. This is what the ProgressBarTableCell does. It is a table cell which contains a progress bar. The forTableColumn method creates a factory which produces the ProgressBarTableCells for each non-empty row in the column and sets the progress bar's progress to match the cell value which has been linked to the task's progress property by the PropertyValueFactory.

Confusing in understanding the detailed implementation . . . sure. But these high level helper factories and cells take care of a lot of the low level linkage details for you so that you don't need to code them over and over and from a plain API usage point of view it is (hopefully) simple and logical.

Also there is no properties (like SimpleStringProperty etc.) so the question would be, what if I need like two more columns with SimpleStringProperty, how do I add them to this kind of TableView?

Use the PropertyValueFactory once again. Let's image you have a string property called URL, then you can add the columns like this:

TableColumn<TestTask, Double> urlCol = new TableColumn("URL");
urlCol.setCellValueFactory(
  new PropertyValueFactory<TestTask, Double>("url")
);

Note we only needed to set the cell value factory, this is because the default cell factory for the column will return a cell containing a label which directly displays the string value of the cell.

Now for the above to work correctly we need a method on TestTask which provides a url for the task, for example:

final ReadOnlyStringWrapper url = new ReadOnlyStringWrapper();

public TestTask(String url) {
  this.url.set(url);
}

public ReadOnlyStringProperty urlProperty() {
  return url.getReadOnlyProperty()
}

Note that the naming convention is really important here, it must be urlProperty() it can't be anything else or the PropertyValueFactory won't find the property value accessor.

Note for these purposes, a simple String value with a getUrl() would have worked just as well as a property as a PropertyValueFactory will work with a getter as well as a property method. The only advantage of using a property method is that it allows the table value data to update automatically based on property change events, which is not possible with a straight getter. But here because the url is effectively final and doesn't change for a given task, it doesn't make a difference whether a getter or property method is provided for this file from the task.