FXML ProgressBar 的迭代线程策略

Iterate thread strategy for the FXML ProgressBar

我在这里把自己写进了一个角落。在我的 FXML 文件中,我声明了一个进度条和一个上传进度标签(用于批量上传):

Program_view.fxml

<Label fx:id="uploadProgressLabel" layoutX="384" layoutY="579" text="Upload Progress">
    <font>
        <Font size="18" />
    </font>
</Label>

...

<ProgressBar fx:id="uploadProgressBar" layoutX="185" layoutY="606" prefHeight="18" prefWidth="534" progress="0" />

然后我有一个 UI 控制器,我从 FXML 导入所有元素:

UI_Controller.java

@FXML Label uploadProgressLabel;
@FXML ProgressBar uploadProgressBar;

稍后在UI控制器中,有一个按钮,其作用是更新“上传”进度条,但它没有。我尝试了几种不同的线程/任务策略,其中 none 似乎在 运行ning 时工作。

UI_Controller.java

@FXML
protected void distributeSetButtonClick() {
    //In a previous version of this project using swing, I tossed this whole function into a new thread and that made the progress bar happy
    //new Thread(() -> {
    final boolean[] done = {false};
    if (logTextArea.getText().equalsIgnoreCase("This is your console log, logs relating to uploading your set will appear here")) {
        logTextArea.setText("");
    }

    //Upload each file and iterate label + counter
    for (int i = 1; i <= uploadImages.size(); i++) {
        System.out.println("Test: " + i);
        uploadProgressLabel.setText("Uploading image " + i + "/" + uploadImages.size());
        File f = uploadImages.get(i - 1);
        mediaIds.add(UploadFile.uploadFile(f, logTextArea, i - 1, uploadImages.size()));
        double currentProgress = (1.0 / uploadImages.size()) * i;
        uploadProgressBar.setProgress(currentProgress);
    }

    uploadProgressLabel.setText("Completed uploading: " + uploadImages.size() + " images");
    String areaUpdate = filesSelectedTextArea.getText();
    if (mediaIds.size() == uploadImages.size()) {
        areaUpdate += "\r\n\r\n All Images uploaded successfully";
    } else {
        areaUpdate += "\r\n\r\n One or more files had an error while uploading";
    }

    filesSelectedTextArea.setText(areaUpdate);
}

...

我的问题是,如何让进度条/标签在主线程上更新?当我尝试将它们从主线程中移出时,我收到一条关于它们不在 JavaFX 线程上的错误。我也试过将逻辑移到任务中,看起来像这样(然后在主线程上有一个 运行 任务)也无济于事:

Tasker.java

public static Task<Void> updateProgressBar(ProgressBar p, double value) {
   Task<Void> task = new Task<Void>() {
       @Override
       protected Void call() throws Exception {
           p.setProgress(value);
           return null;
       }
   };

   p.progressProperty().bind(task.progressProperty());
   return task;
}

一些指导将不胜感激。

与大多数其他 UI 工具包一样,JavaFX 是单线程的,FX 应用程序线程负责处理用户事件和呈现 UI。这意味着:

  1. 不得从后台线程更新UI控件。如果您这样做,JavaFX 将在许多(尽管不是全部)情况下抛出 IllegalStateExceptions。 (请注意,Swing 不会抛出异常;它只会让您容易在某个不确定的点出现任意故障。)
  2. Long-running 进程(例如您的文件上传)不得 在 FX 应用程序线程上 运行。由于此线程负责呈现 UI 和处理用户事件,因此在 long-running 过程完成之前,无法进行 UI 更新(例如更新标签和进度条)。此外,UI 在此期间将没有响应。

您应该使用 Task 来实现较长的 运行ning 进程,并 运行 在后台线程上执行该任务。 Task 具有 thread-safe update 方法,这些方法将在 FX 应用程序线程上更新其属性(例如 progressmessage),因此您可以安全地绑定属性UI 个元素添加到这些属性中。它还具有 onSucceededonFailed 处理程序,它们也在 FX 应用程序线程上执行。 onSucceeded 处理程序可以访问任务中的任何 return 值。

所以您的代码应该类似于:

@FXML
protected void distributeSetButtonClick(){
    //In a previous version of this project using swing, I tossed this whole function into a new thread and that made the progress bar happy
    Task<String> task = new Task<>() {
        @Override
        protected String call() throws Exception {
            final boolean[] done = {false};
        
            //Upload each file and iterate label + counter
            for (int i = 1; i <= uploadImages.size(); i++) {
                System.out.println("Test: " + i);
                updateMessage("Uploading image " + i + "/" + uploadImages.size());
                File f = uploadImages.get(i - 1);
                mediaIds.add(UploadFile.uploadFile(f, logTextArea, i - 1, uploadImages.size()));
                updateProgress(i, uploadImages.size());
            }
            if (mediaIds.size() == uploadImages.size()) {
                return "All Images uploaded successfully";
            } else {
                return "One or more files had an error while uploading";
            }
        }

    };

    if (logTextArea.getText().equalsIgnoreCase("This is your console log, logs relating to uploading your set will appear here")) {
        logTextArea.setText("");
    }

    task.setOnSucceeded(event -> {
        filesSelectedTextArea.append("\n\n"+task.getValue());
        uploadProgressLabel.setText("Completed uploading: " + uploadImages.size() + " images");
    });


    uploadProgressLabel.textProperty().unbind()
    uploadProgressLabel.textProperty().bind(task.messageProperty());

    uploadProgressBar.progressProperty().unbind();
    uploadProgressBar.progressProperty().bind(task.progressProperty());

    Thread thread = new Thread(task);
    thread.setDaemon(true);
    thread.start();
}