使用 ScheduledExecutorService 更新 JavaFX 元素

Use ScheduledExecutorService to update JavaFX elements

目前我正在制作一个程序,提醒我什么时候给我的植物浇水,同时也会考虑天气。我想显示当前的温度和湿度,我已经编写了足够好的代码。但是,此代码仅在通过按下按钮手动 运行 方法时有效,并且当我尝试在 ScheduledExecutorService 中 运行 时抛出 Exception in thread "pool-3-thread-1" java.lang.IllegalStateException: Not on FX application thread; currentThread = pool-3-thread-1。根据我的理解,JavaFX 不允许其他线程在没有 Platform.runLater 的情况下编辑 JavaFX 组件,但是我似乎找不到任何关于 Platform.runLater 与 ScheduledExecutorService.

结合的信息

这是我的更新方法:

public void update() {
    final Runnable updater = new Runnable() {
        public void run() {
            humidityLabel.setText("Humidity: " + Double.toString(Weather.getHumidity()) + "%");
            humidityDialArm.setRotate(Weather.getHumidity() * 1.8);
            tempLabel.setText("Temperature: " + Double.toString(Weather.getTemperature()) + "°F");
            temperatureDialArm.setRotate(Weather.getTemperature()*1.5);
            icon = Weather.getIcon();
            conditionLabel.setText(Weather.getCondition());
        }
    };
    final ScheduledFuture<?> updaterHandle = scheduler.scheduleAtFixedRate(updater, 10, 10, TimeUnit.MINUTES);
        
}

这是我的主要方法:

public static void main(String[] args) {
    App app = new App();
    launch();
    app.update();        
    
}

我在 GitHub 上发现了一个类似的问题 here, however I haven't been able to find a way to get Platform.runLater to work well with the ScheduledExecutorService. I also found this,但是除了可以修复之外,我不知道该问题的解决方法是什么。我还尝试在 main 中放置一个 while 循环,它会不断更新它,但这只会导致程序挂起并最终崩溃。即使它确实有效,这也会使它长时间 运行 无法使用,因为我正在使用的 API 限制了每天的 GET 请求数量。

使用ScheduledService

javafx.concurrent.ScheduledService class 提供了一种重复执行操作并轻松与 FX 线程通信的方法。这是一个例子:

import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.scene.image.Image;

public class WeatherService extends ScheduledService<WeatherService.Result> {
  @Override protected Task<Result> createTask() {
    return new Task<>() {
      @Override protected Result call() throws Exception {
        // this is invoked on the background thread
        return new Result(
            Weather.getTemperature(),
            Weather.getHumidity(),
            Weather.getCondition(),
            Weather.getIcon()
        );
      }
    };
  }

  public record Result(double temperature, double humidity, String condition, Image icon) {}
}

然后在使用该服务的地方添加一个 on-succeeded 处理程序来处理更新 UI:

service.setOnSucceeded(e -> {
  var result = service.getValue();
  // update UI (this handler is invoked on the UI thread)
});

要让它每​​ 10 分钟执行一次,初始延迟 10 分钟,以匹配您对 ScheduledExecutorService 所做的操作,您可以这样做:

service.setDelay(javafx.util.Duration.minutes(10));
service.setPeriod(javafx.util.Duration.minutes(10));
// you can define the thread pool used with 'service.setExecutor(theExecutor)'

首次配置服务时。您还需要维护对服务的强引用;如果它被垃圾收集,则不会重新安排任务。


使用Platform#runLater(Runnable)

如果您出于某种原因必须使用 ScheduledExecutorService,那么您应该 运行 在 runLater 调用中更新 UI 的代码。这是一个例子:

public void update() {
  final Runnable updater =
      () -> {
        // get information on background thread
        double humidity = Weather.getHumidity();
        double temperature = Weather.getTemperature();
        Image icon = Weather.getIcon();
        String condition = Weather.getCondition();

        // update UI on FX thread
        Platform.runLater(
            () -> {
              humidityLabel.setText("Humidity: " + humidity + "%");
              humidityDialArm.setRotate(humidity * 1.8);
              tempLabel.setText("Temperature: " + temperature + "°F");
              temperatureDialArm.setRotate(temperature * 1.5);
              iconView.setImage(icon);
              conditionLabel.setText(condition);
            });
      };
  scheduler.scheduleAtFixedRate(updater, 10, 10, TimeUnit.MINUTES);
}