Update progress bar and multiple labels from thread


Question

I'm working on a JavaFX application that let's the user select a folder and then parse it's contents to find MP3 files and read their metadata.

I got this working with Swing although I found it hard to make the user interface look good. Thus I'm trying to do the same in JavaFX.

In the original Swing app, I create a thread which starts the parsing of files in the folder the user selected. It works like this:

  1. Find out total number of files to parse - The number of files and the number of folders are continuously presented in two separate labels in the UI
  2. Parse all files to find which ones are MP3 files and store the metadata - The number of found MP3 files are continuously presented in a label in the UI

At the same time a label is updated that shows the status of whats happening and also a progress bar reflecting the progress of the two above steps. The progress of the first step makes up 30 % of the total progress while the second step makes up the other 70 %.

I found an example of how to bind the task to the progress bar, but I also need to update the four labels: status, file count, folder count and MP3 count.

I believe I can handle one of the labels with updateMessage, although I don't know how to manage the other three.

1
3
3/2/2013 12:19:27 PM

Accepted Answer

Use multiple Tasks to split the problem into bits. Use a controlling tasks to monitor status and overall progress of subtasks. Use java.util.concurrent classes to manage Task execution, sequencing and data structures such as LinkedBlockingDeque.

This recommended solution is not the simplest solution to your problem, but should offer a good user experience if done well.


For an example of a divide and conquer approach applied to different problems, see the following code samples:

  1. splits a complex process into multiple managed subtasks.
  2. demonstrates management of execution of multiple workers sequentially or in parallel.

A potential simple alternate approach is to use a single Task for the whole process and report your multiple feedback values back to your JavaFX UI by invoking Platform.runLater from your Task code as needed.

See the Task documentation section "A Task Which Modifies The Scene Graph" for an example of this approach.

Here is something which updates multiple labels at once inside a Platform.runLater call.

Platform.runLater(new Runnable() {
  @Override public void run() {
    status.setText("");
    folderCount.setText("");
    fileCount.setText("");
    mp3Count.setText("");
  }
});

And some code which is similar to your example:

import java.util.Arrays;
import java.util.List;
import static javafx.application.Application.launch;
import javafx.application.*;
import javafx.beans.value.*;
import javafx.concurrent.Task;
import javafx.event.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;

public class Mp3Finder extends Application {
  final Label status = new Label();
  final Label folderCount = new Label();
  final Label fileCount = new Label();
  final Label mp3Count = new Label();

  @Override public void start(Stage stage) {
    final GridPane finderResults = new GridPane();
    finderResults.setPrefWidth(400);
    finderResults.setVgap(10);
    finderResults.setHgap(10);
    finderResults.addRow(0, new Label("Status: "),    status);
    finderResults.addRow(1, new Label("# Folders: "), folderCount);
    finderResults.addRow(2, new Label("# Files: "),   fileCount);
    finderResults.addRow(3, new Label("# mp3s: "),    mp3Count);

    final Button finderStarter = new Button("Find mp3s");
    finderStarter.setOnAction(new EventHandler<ActionEvent>() {
      @Override public void handle(ActionEvent t) {
        startMp3Finder(finderStarter);
      }
    });

    VBox layout = new VBox(10);
    layout.setStyle("-fx-background-color: cornsilk; -fx-padding: 10; -fx-font-size: 16;");
    layout.getChildren().setAll(finderStarter, finderResults);
    stage.setScene(new Scene(layout));
    stage.show();
  }

  private void startMp3Finder(final Node starterNode) {
    starterNode.setDisable(true);

    Mp3FinderTask task = new Mp3FinderTask(status, folderCount, mp3Count);
    task.runningProperty().addListener(new ChangeListener<Boolean>() {
      @Override public void changed(ObservableValue<? extends Boolean> ov, Boolean wasRunning, Boolean isRunning) {
        if (!isRunning) {
          starterNode.setDisable(false);
        }
      }
    });

    final Thread thread = new Thread(task , "mp3-finder");
    thread.setDaemon(true);
    thread.start();
  }

  private class Mp3FinderTask extends Task<List<String>> {
    private final Label status;
    private final Label folderCount;
    private final Label mp3Count;

    public Mp3FinderTask(Label status, Label folderCount, Label mp3Count) {
      this.status = status;
      this.folderCount = folderCount;
      this.mp3Count = mp3Count;
    }

    @Override protected List<String> call() throws Exception {
      initFinderResults();

      updateLabelLater(status, "Finding Folders");
      setProgressIndicator(folderCount);
      List folders = findFolders();
      updateLabelLater(folderCount, folders.size() + "");

      updateLabelLater(status, "Finding Files");
      setProgressIndicator(fileCount);
      List files = findFiles(folders);
      updateLabelLater(fileCount, files.size() + "");

      updateLabelLater(status, "Find mp3s");
      setProgressIndicator(mp3Count);
      List mp3s = findMp3s(files);
      updateLabelLater(mp3Count, mp3s.size() + "");

      updateLabelLater(status, "All mp3s Found");

      return mp3s;
    }

    void updateLabelLater(final Label label, final String text) {
      Platform.runLater(new Runnable() {
        @Override public void run() {
          label.setGraphic(null);
          label.setText(text);
        }
      });
    }

    private List<String> findFolders() throws InterruptedException { 
      // dummy implementation
      Thread.currentThread().sleep(1000);
      return Arrays.asList("folder1", "folder2", "folder3");
    }

    private List<String> findFiles(List<String> folders) throws InterruptedException {
      // dummy implementation
      Thread.currentThread().sleep(1000);
      return Arrays.asList("file1", "file2", "file3", "file4", "file5");
    }

    private List<String> findMp3s(List<String> files) throws InterruptedException {
      // dummy implementation
      Thread.currentThread().sleep(1000);
      return Arrays.asList("music1", "music2");
    }

    private void initFinderResults() {
      Platform.runLater(new Runnable() {
        @Override public void run() {
          status.setText("");
          folderCount.setText("");
          fileCount.setText("");
          mp3Count.setText("");
        }
      });
    }

    private void setProgressIndicator(final Label label) {
      Platform.runLater(new Runnable() {
        @Override public void run() {
          label.setGraphic(new ProgressIndicator());
        }
      });
    }
  }

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

mp3finder


See the StackOverflow question on use of Platform.runLater and accessing the UI from a different thread for more information for more information and links to further resources on concurrency in JavaFX.

8
5/23/2017 12:07:59 PM

Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon