JavaFX:为TableView每行添加ProgressIndicator并实现任务状态管控
JavaFX TableView任务进度控制问题解决方案
问题描述
需要实现的功能:
- 一个多行TableView,每行包含文本列和进度/状态列
- 点击按钮后依次执行每行的任务(如数据校验)
- 任务执行时状态列显示不确定进度条,完成后标记为已完成
- 所有任务完成后可点击按钮重置状态并重新执行
遇到的问题:
- 程序启动后所有行的ProgressIndicator立即显示不确定状态,如何实现点击按钮后逐行激活显示?
- 任务完成后再次点击按钮无法重启任务,如何实现重置功能?
- 当前整体实现思路是否合理?
原代码
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); } } }
问题解答
1. 解决启动后进度条立即显示的问题
原因:Task的默认progress值是ProgressIndicator.INDETERMINATE_PROGRESS(即-1),表格直接绑定该属性后,启动就会显示不确定状态。
解决方案:
- 初始化
TestTask时手动设置进度为0(默认完成状态) - 通过链式调用确保任务逐行启动,而非一次性提交所有任务
修改TestTask构造方法:
public TestTask(int waitTime, String name) { this.waitTime = waitTime; this.name.set(name); this.updateProgress(0, 1); // 初始设置为已完成状态 }
修改按钮点击事件实现逐行执行:
btn.setOnAction(actionEvent -> { ExecutorService executor = Executors.newSingleThreadExecutor(); CompletableFuture<Void> future = CompletableFuture.completedFuture(null); for (TestTask task : table.getItems()) { future = future.thenRunAsync(() -> { executor.submit(task); try { task.get(); // 等待当前任务完成后再执行下一个 } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }, executor); } future.thenRun(executor::shutdown); });
2. 解决任务无法重启的问题
原因:javafx.concurrent.Task是一次性对象,执行完毕后无法再次提交或运行。
解决方案:
- 将数据模型与任务逻辑分离,创建独立的
TaskModel存储任务元数据 - 每次点击按钮时,为每个模型创建新的
TestTask实例,替换表格中的旧任务
新增数据模型类:
public static class TaskModel { private final ReadOnlyStringWrapper name = new ReadOnlyStringWrapper(); private final int waitTime; private final ReadOnlyObjectWrapper<Task<Void>> currentTask = new ReadOnlyObjectWrapper<>(); public TaskModel(int waitTime, String name) { this.waitTime = waitTime; this.name.set(name); // 初始设置一个完成状态的空任务,避免进度条异常 this.setCurrentTask(new Task<>() { @Override protected Void call() { updateProgress(1,1); return null; } }); } public ReadOnlyStringProperty nameProperty() { return name.getReadOnlyProperty(); } public int getWaitTime() { return waitTime; } public ReadOnlyObjectWrapper<Task<Void>> currentTaskProperty() { return currentTask; } public void setCurrentTask(Task<Void> task) { currentTask.set(task); } }
修改表格列绑定逻辑:
TableColumn<TaskModel, String> nameCol = new TableColumn("Name"); nameCol.setCellValueFactory(cellData -> cellData.getValue().nameProperty()); nameCol.setPrefWidth(75); TableColumn<TaskModel, Double> progressCol = new TableColumn("Progress"); progressCol.setCellValueFactory(cellData -> cellData.getValue().currentTaskProperty().flatMap(Task::progressProperty) ); progressCol.setCellFactory(ProgressIndicatorTableCell.<TaskModel>forTableColumn());
修改按钮事件实现重置:
btn.setOnAction(actionEvent -> { ExecutorService executor = Executors.newSingleThreadExecutor(); CompletableFuture<Void> future = CompletableFuture.completedFuture(null); for (TaskModel model : table.getItems()) { future = future.thenRunAsync(() -> { // 创建新任务实例 TestTask newTask = new TestTask(model.getWaitTime(), model.nameProperty().get()); model.setCurrentTask(newTask); executor.submit(newTask); try { newTask.get(); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }, executor); } future.thenRun(executor::shutdown); });
3. 原实现思路合理性分析
原思路将Task直接作为TableView的数据源,存在以下问题:
- 耦合度高:任务逻辑与UI展示绑定,
Task无法复用,导致无法重启任务 - 状态管理混乱:
Task的生命周期与表格行状态绑定,难以重置或扩展 - 扩展性差:添加任务取消、失败状态显示等功能会非常繁琐
优化后的思路更合理:
- 数据模型与任务逻辑分离,
TaskModel负责存储元数据和当前任务引用,TestTask只负责执行具体逻辑 - 表格绑定模型状态而非直接绑定
Task,UI与业务逻辑解耦 - 任务可通过创建新实例轻松重置,扩展性更强
修改后的完整代码
import java.util.Random; import java.util.concurrent.*; import javafx.application.Application; import javafx.beans.property.ReadOnlyObjectWrapper; 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.layout.BorderPane; import javafx.stage.Stage; import javafx.util.Callback; public class ProgressIndicatorTableCellTest extends Application { public void start(Stage primaryStage) { TableView<TaskModel> table = new TableView<>(); Random rng = new Random(); for (int i = 0; i < 3; i++) { table.getItems().add(new TaskModel(rng.nextInt(3000) + 2000, "Test " + (i+1))); } TableColumn<TaskModel, String> nameCol = new TableColumn("Name"); nameCol.setCellValueFactory(cellData -> cellData.getValue().nameProperty()); nameCol.setPrefWidth(75); TableColumn<TaskModel, Double> progressCol = new TableColumn("Progress"); progressCol.setCellValueFactory(cellData -> cellData.getValue().currentTaskProperty().flatMap(Task::progressProperty) ); progressCol.setCellFactory(ProgressIndicatorTableCell.<TaskModel>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(); CompletableFuture<Void> future = CompletableFuture.completedFuture(null); for (TaskModel model : table.getItems()) { future = future.thenRunAsync(() -> { TestTask newTask = new TestTask(model.getWaitTime(), model.nameProperty().get()); model.setCurrentTask(newTask); executor.submit(newTask); try { newTask.get(); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }, executor); } future.thenRun(executor::shutdown); }); root.setBottom(btn); primaryStage.setScene(new Scene(root, 300, 200)); primaryStage.show(); } public static void main(String[] args) { launch(args); } // 数据模型类:存储任务元数据和当前任务引用 public static class TaskModel { private final ReadOnlyStringWrapper name = new ReadOnlyStringWrapper(); private final int waitTime; private final ReadOnlyObjectWrapper<Task<Void>> currentTask = new ReadOnlyObjectWrapper<>(); public TaskModel(int waitTime, String name) { this.waitTime = waitTime; this.name.set(name); // 初始设置一个完成状态的空任务,避免进度条显示异常 this.setCurrentTask(new Task<>() { @Override protected Void call() { updateProgress(1,1); return null; } }); } public ReadOnlyStringProperty nameProperty() { return name.getReadOnlyProperty(); } public int getWaitTime() { return waitTime; } public ReadOnlyObjectWrapper<Task<Void>> currentTaskProperty() { return currentTask; } public void setCurrentTask(Task<Void> task) { currentTask.set(task); } } // 任务逻辑类:只负责执行具体任务 public static class TestTask extends Task<Void> { private final int waitTime; final ReadOnlyStringWrapper name = new ReadOnlyStringWrapper(); public TestTask(int waitTime, String name) { this.waitTime = waitTime; this.name.set(name); updateProgress(0, 1); // 初始为未启动状态 } public ReadOnlyStringProperty nameProperty() { return name.getReadOnlyProperty(); } @Override protected Void call() throws Exception { updateProgress(ProgressIndicator.INDETERMINATE_PROGRESS, 1); Thread.sleep(waitTime); updateProgress(1, 1); return null; } } } class ProgressIndicatorTableCell<S> extends TableCell<S, Double> { public static <S> Callback<TableColumn<S, Double>, TableCell<S, Double>> forTableColumn() { return param -> new ProgressIndicatorTableCell<>(); } private final ProgressIndicator progressIndicator; private ObservableValue<? extends Number> observable; public ProgressIndicatorTableCell() { this.progressIndicator = new ProgressIndicator(); setGraphic(progressIndicator); } @Override public void updateItem(Double item, boolean empty) { super.updateItem(item, empty); if (empty || item == null) { setGraphic(null); } else { progressIndicator.progressProperty().unbind(); observable = getTableColumn().getCellObservableValue(getIndex()); if (observable != null) { progressIndicator.progressProperty().bind(observable); } else { progressIndicator.setProgress(item); } setGraphic(progressIndicator); } } }
内容的提问来源于stack exchange,提问作者maloomeister
相关产品推荐
相关产品推荐

