You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaFX:为TableView每行添加ProgressIndicator并实现任务状态管控

JavaFX TableView任务进度控制问题解决方案

问题描述

需要实现的功能:

  • 一个多行TableView,每行包含文本列和进度/状态列
  • 点击按钮后依次执行每行的任务(如数据校验)
  • 任务执行时状态列显示不确定进度条,完成后标记为已完成
  • 所有任务完成后可点击按钮重置状态并重新执行

遇到的问题:

  1. 程序启动后所有行的ProgressIndicator立即显示不确定状态,如何实现点击按钮后逐行激活显示?
  2. 任务完成后再次点击按钮无法重启任务,如何实现重置功能?
  3. 当前整体实现思路是否合理?

原代码

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 15:35:20