3

I'm trying to do the following in JavaFX:

  • Have a TableView with multiple rows.
  • Each row contains columns with text and one Progress/Status column.
  • When a specific Button is pressed, for each row of the TableView some task should be performed, one row after the other. (e.g. check some data, ...)
  • While this task is performed, a indeterminate ProgressIndicator shall be shown in the Status column, until the task for this row is finished, then the indicator shows as done.
  • When all tasks for each row are done, the button can be pressed again to reset the status and execute the tasks again.

I had found some help in this related Stackoverflow post and also here and tried to tweak this as needed but got stuck on some issues:

  1. Currently, each ProgressIndicator for each row is displayed immediately (as indeterminate) when I run the program. How can I only activate them / make them visible for each row one after another once the button is pressed?
  2. Pressing the button again once the fake tasks are done does not restart it. How would I have to modify / rebuild the program to make resets possible?
  3. Does the overall approach make sense?

My current runnable code:

import java.util.Random;
import java.util.concurrent.*;

import javafx.application.Application;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Callback;

public class ProgressIndicatorTableCellTest extends Application {
    public void start(Stage primaryStage) {
        TableView<TestTask> table = new TableView<>();
        Random rng = new Random();
        for (int i = 0; i < 3; i++) {
            table.getItems().add(new TestTask(rng.nextInt(3000) + 2000, "Test"));
        }

        TableColumn<TestTask, String> nameCol = new TableColumn("Name");
        nameCol.setCellValueFactory(new PropertyValueFactory<TestTask, String>("name"));
        nameCol.setPrefWidth(75);

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

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

        BorderPane root = new BorderPane();
        root.setCenter(table);
        Button btn = new Button("Start");
        btn.setOnAction(actionEvent -> {
            ExecutorService executor = Executors.newSingleThreadExecutor();

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

        root.setBottom(btn);
        primaryStage.setScene(new Scene(root));
        primaryStage.show();

    }

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

    public static class TestTask extends Task<Void> {
        private final int waitTime; // milliseconds
        final ReadOnlyStringWrapper name = new ReadOnlyStringWrapper();
        public static final int NUM_ITERATIONS = 100;

        public TestTask(int waitTime, String name) {
            this.waitTime = waitTime;
            this.name.set(name);
        }

        public ReadOnlyStringProperty nameProperty() {
            return name.getReadOnlyProperty();
        }

        @Override
        protected Void call() throws Exception {
            this.updateProgress(ProgressIndicator.INDETERMINATE_PROGRESS, 1);
            Thread.sleep(waitTime);
            this.updateProgress(1, 1);
            return null;
        }
    }
}

class ProgressIndicatorTableCell<S> extends TableCell<S, Double> {
    public static <S> Callback<TableColumn<S, Double>, TableCell<S, Double>> forTableColumn() {
        return new Callback<TableColumn<S, Double>, TableCell<S, Double>>() {
            @Override
            public TableCell<S, Double> call(TableColumn<S, Double> param) {
                return new ProgressIndicatorTableCell<>();
            }
        };
    }

    private final ProgressIndicator progressIndicator;
    private ObservableValue observable;

    public ProgressIndicatorTableCell() {
        this.progressIndicator = new ProgressIndicator();
        setGraphic(progressIndicator);
    }

    @Override
    public void updateItem(Double item, boolean empty) {
        super.updateItem(item, empty);

        if (empty) {
            setGraphic(null);
        } else {
            progressIndicator.progressProperty().unbind();

            observable = getTableColumn().getCellObservableValue(getIndex());
            if (observable != null) {
                progressIndicator.progressProperty().bind(observable);
            } else {
                progressIndicator.setProgress(item);
            }

            setGraphic(progressIndicator);
        }
    }
}

And the current output:

enter image description here enter image description here

maloomeister
  • 2,461
  • 1
  • 12
  • 21
  • For 1., doesn't the obvious modification (`if (empty || item <= 0) { setGraphic(null); } else { ... }`) work? – James_D Jan 11 '23 at 13:41
  • @James_D thanks for the suggestion, I already tried this however this doesn't work. This will make the progress indicator never visible when it's indeterminate (because then `item == -1` if I remember correctly). – maloomeister Jan 11 '23 at 13:45
  • tasks cannot be restarted. every time you want to run a task you have to create it. from here the table design also becomes wrong. – mr mcwolf Jan 11 '23 at 13:46
  • 1
    Actually, looking at the changes you made to the `TestTask`, the progress can only either be indeterminate, or 1. If this is the case, I don't understand the point of observing the `progressProperty` at all. Just observe the `state` property, and make the progress indicator invisible, visible with value -1, or visible with value 1 as appropriate. – James_D Jan 11 '23 at 13:47
  • 1
    For 2. you would need the model class to subclass (or encapsulate) a `Service`, not a `Task`. – James_D Jan 11 '23 at 13:48
  • @James_D yes, that's the case. I want the indicator to only be indeterminate or 1. Can you explain what you mean by "_Just observe the `state` property ..._"? I tought I needed to observe the `progressProperty` to be able to update the indicator at all. Thanks. – maloomeister Jan 11 '23 at 13:54
  • 1
    I mean the `state` property of the task, instead of the `progress` property *of the task*. Then just update the progress property of the indicator. – James_D Jan 11 '23 at 13:58
  • hmm don't quite understand your requirement: in the comment _ want the indicator to only be indeterminate or 1._ and in the description: _ each row is displayed immediately .. make them visible for each row one after another [after the task is started]_ - so you want to visualize the complete three-state (ready/running == indeterminate/done of the task. So comming back to the first comment by @James_D: why don't you implement that three-state appearance in the cell by hiding the indicator if not yet started? Please modify the example to demonstrate _indicator never visible_ of you comment – kleopatra Jan 11 '23 at 14:20
  • @James_D Thanks for the hints! Seems like I got 1. working. I was confused because I thought I needed to observe the progress either way. But now observing the `state` I got it working. – maloomeister Jan 11 '23 at 14:21
  • @kleopatra Yeah I was confused and I got it wrong before, where I weren't able to set the indicators visible according the task status. – maloomeister Jan 11 '23 at 14:25
  • Here is something with similar ideas. https://stackoverflow.com/questions/51955550/remove-tableview-entries-when-status-change – SedJ601 Jan 11 '23 at 14:25

1 Answers1

5

Here is a version that implements your first question. With this requirement, the cell is only a function of the task's state. If it's RUNNING, display an indeterminate progress indicator; if it's SUCCEEDED display a progress indicator with value 1; otherwise, display nothing.

Note the original question is very old and uses a lot of outdated code styles. I've updated accordingly.

import javafx.application.Application;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

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

public class ProgressIndicatorTableCellTest extends Application {
    public void start(Stage primaryStage) {
        TableView<TestTask> table = new TableView<>();
        Random rng = new Random();
        for (int i = 0; i < 3; i++) {
            table.getItems().add(new TestTask(rng.nextInt(3000) + 2000, "Test"));
        }

        TableColumn<TestTask, String> nameCol = new TableColumn<>("Name");
        nameCol.setCellValueFactory(data -> data.getValue().nameProperty());
        nameCol.setPrefWidth(75);

        TableColumn<TestTask, Worker.State> progressCol = new TableColumn<>("Progress");
        progressCol.setCellValueFactory(data -> data.getValue().stateProperty());
        progressCol.setCellFactory(col -> new ProgressIndicatorTableCell<>());

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

        BorderPane root = new BorderPane();
        root.setCenter(table);
        Button btn = new Button("Start");
        btn.setOnAction(actionEvent -> {
            ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
                Thread t = new Thread(r);
                t.setDaemon(true);
                return t;
            });

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

        root.setBottom(btn);
        primaryStage.setScene(new Scene(root));
        primaryStage.show();

    }

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

    public static class TestTask extends Task<Void> {
        private final int waitTime; // milliseconds
        final ReadOnlyStringWrapper name = new ReadOnlyStringWrapper();
        public static final int NUM_ITERATIONS = 100;

        public TestTask(int waitTime, String name) {
            this.waitTime = waitTime;
            this.name.set(name);
        }

        public ReadOnlyStringProperty nameProperty() {
            return name.getReadOnlyProperty();
        }

        @Override
        protected Void call() throws Exception {
            this.updateProgress(ProgressIndicator.INDETERMINATE_PROGRESS, 1);
            Thread.sleep(waitTime);
            this.updateProgress(1, 1);
            return null;
        }
    }
}

class ProgressIndicatorTableCell<S> extends TableCell<S, Worker.State> {

    private final ProgressIndicator progressIndicator = new ProgressIndicator();

    @Override
    protected void updateItem(Worker.State state, boolean empty) {
        super.updateItem(state, empty);
        if (state == Worker.State.SUCCEEDED) {
            progressIndicator.setProgress(1);
            setGraphic(progressIndicator);
        } else if (state == Worker.State.RUNNING) {
            progressIndicator.setProgress(-1);
            setGraphic(progressIndicator);
        } else {
            setGraphic(null);
        }
    }

}

To allow for "restarting", you should use a Service instead of just a Task. This version will allow for a restart if the button is pressed multiple times, returning everything to the initial state before proceeding.

This version also factors the processing work out of the model class, which is desirable for properly assigning responsibilities to classes:

Item.java:

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleObjectProperty;

public class Item {

    public enum State {WAITING, PROCESSING, READY}

    final ReadOnlyStringWrapper name = new ReadOnlyStringWrapper();

    private final ObjectProperty<State> state = new SimpleObjectProperty<>(State.WAITING);


    public Item(String name) {
        this.name.set(name);
    }

    public ReadOnlyStringProperty nameProperty() {
        return name.getReadOnlyProperty();
    }

    public State getState() {
        return state.get();
    }

    public ObjectProperty<State> stateProperty() {
        return state;
    }

    public void setState(State state) {
        this.state.set(state);
    }
}

ProcessManager.java:

import javafx.application.Platform;
import javafx.concurrent.Service;
import javafx.concurrent.Task;

import java.util.List;
import java.util.Random;

public class ProcessManager {

    private final List<Item> items;

    private Random rng = new Random();

    private Service<Void> service = new Service<>() {

        @Override
        protected Task<Void> createTask() {
            return new Task<>() {
                @Override
                protected Void call() throws Exception {
                    for (Item task: items) {
                        try {
                            Platform.runLater(() -> task.setState(Item.State.PROCESSING));
                            Thread.sleep(2000 + rng.nextInt(3000));
                            Platform.runLater(() -> task.setState(Item.State.READY));
                        } catch (InterruptedException exc) {
                            Thread.currentThread().interrupt();
                        }
                        if (isCancelled()) {
                            Platform.runLater(() -> task.setState(Item.State.WAITING));
                            break;
                        }
                    }
                    return null;
                }
            };
        }
    };


    public ProcessManager(List<Item> items) {
        this.items = items ;
        service.setOnCancelled(e -> items.forEach(task -> task.setState(Item.State.WAITING)));
    }


    public void process() {
        service.restart();
    }
}

and the application:

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class ProgressIndicatorTableCellTest extends Application {

    public void start(Stage primaryStage) {

        ObservableList<Item> tasks = FXCollections.observableArrayList();

        ProcessManager processManager = new ProcessManager(tasks);

        TableView<Item> table = new TableView<>();
        for (int i = 0; i < 3; i++) {
            Item task = new Item("Item " + (i + 1));
            tasks.add(task);
        }
        table.setItems(tasks);

        TableColumn<Item, String> nameCol = new TableColumn<>("Name");
        nameCol.setCellValueFactory(data -> data.getValue().nameProperty());
        nameCol.setPrefWidth(75);

        TableColumn<Item, Item.State> progressCol = new TableColumn<>("Progress");
        progressCol.setCellValueFactory(data -> data.getValue().stateProperty());
        progressCol.setCellFactory(col -> new TableCell<>() {
            private final ProgressIndicator indicator = new ProgressIndicator();
            @Override
            protected void updateItem(Item.State state, boolean empty) {
                super.updateItem(state, empty);
                if (state == Item.State.PROCESSING) {
                    indicator.setProgress(-1);
                    setGraphic(indicator);
                } else if (state == Item.State.READY) {
                    indicator.setProgress(1);
                    setGraphic(indicator);
                } else {
                    setGraphic(null);
                }
            }
        });

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

        BorderPane root = new BorderPane();
        root.setCenter(table);
        Button btn = new Button("Start");
        btn.setOnAction(actionEvent -> processManager.process());

        root.setBottom(btn);
        primaryStage.setScene(new Scene(root));
        primaryStage.show();

    }

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

}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • Thanks a lot, it finally clicked now! The calls to `updateProgress` in the `call()` are obsolete aswell now right? About the second part: I don't need it to be a `Task`, this was only taken from the linked original. I only need something to be executed to show the indicators updating. I will try changing it to a `Service`. – maloomeister Jan 11 '23 at 14:30
  • Personally, I would split the TableView implementation from the underlying data. Create TableView where TableModel is a POJO with a StringProperty and a DoubleProperty as fields. Put them into an ObservableList. When you click the Button, some independant process runs which starts up Tasks and Binds the DoubleProperties to the Tasks' Progress properties. This separates the View from the back-end implementation. – DaveB Jan 11 '23 at 16:40
  • @James_D Yeah, like that. – DaveB Jan 11 '23 at 20:24