JavaFX 运行 一次有大量倒数计时器?

JavaFX Running a large amount of countdown timers at once?

所以我可以看到几种不同的方法来做我需要的事情,我已经做了一堆 google/stack 溢出搜索,但找不到我真正想要的东西。我需要 运行 多个 "Countdown timers." 我需要大约 6 个可能最多 10 个倒数计时器 运行 在不同的时间同时进行。我的主程序上有一个选项卡窗格,其中包含 FXML 并将控制器注入其中。定时器选项卡与主程序有不同的控制器。

所以我的第一个问题是。由于此 "tab" 在单独的控制器上 运行ning 但包含在主程序中,它是否 运行 在单独的应用程序线程上?

这是包含的选项卡 FXML 的示例...

当我按下每个开始按钮时。我可以为每个计时器创建一个 TimelineKeyFrame。但是,我真的不认为这是最好的方法。特别是当您同时获得多达 10 个时间线 运行ning 时,如果这不是 运行ning 在与主程序分开的应用程序线程上,那肯定是。

我考虑过将每个启动请求发送到 ExecutorServicenewCacheThreadPool,但是我需要能够用当前剩余时间更新 GUI 上的标签,我知道你不应该使用后台服务来做到这一点。 Platform.runLater() 也许吧?

另一个想法是使用 java.util.Timer class 中的 Timer。但是,当我需要更新 GUI 标签时,我认为这与 ExecutorService 存在相同的问题。我也明白 Timer class 只创建一个线程并按顺序执行它的任务。所以,那是行不通的。

或者,我是否应该有一个完整的其他 "CountDown" class,我可以为每个实例创建新实例,然后启动新线程。但是,如果我这样做,我该怎么做不断更新 GUI。我仍然需要使用 timeline 轮询 CountDown class 对吗?所以这会破坏这整件事的目的。

So the 1st question I have is. Since this "tab" is running on a separate controller but is included into the main program, does it run on a separate application thread?

不,每个 JVM 只能有 一个 JavaFX 应用程序实例,每个 JVM 也只能有 一个 JavaFX 应用程序线程。

至于如何更新计时器,可以使用 Timeline - 每个计时器一个。 Timeline 不会在单独的线程上 运行 - 它由负责定期更新 JavaFX GUI 的底层 "scene graph rendering pulse" 触发。拥有更多 Timeline 个实例基本上意味着有更多的侦听器订阅了 "pulse" 事件。

public class TimerController {
    private final Timeline timer;

    private final ObjectProperty<java.time.Duration> timeLeft;

    @FXML private Label timeLabel;

    public TimerController() {
        timer = new Timeline();
        timer.getKeyFrames().add(new KeyFrame(Duration.seconds(1), ae -> updateTimer()));
        timer.setCycleCount(Timeline.INDEFINITE);

        timeLeft = new SimpleObjectProperty<>();
    }
    public void initialize() {
        timeLabel.textProperty().bind(Bindings.createStringBinding(() -> getTimeStringFromDuration(timeLeft.get()), timeLeft));
    }

    @FXML
    private void startTimer(ActionEvent ae) {
        timeLeft.set(Duration.ofMinutes(5)); // For example timer of 5 minutes
        timer.playFromStart();
    }

    private void updateTimer() {
        timeLeft.set(timeLeft.get().minusSeconds(1));
    }

    private static String getTimeStringFromDuration(Duration duration) {
        // Do the conversion here...
    }
}

当然,你也可以使用Executor和其他线程方法,前提是你通过Platform.runLater()更新了Label。或者,您可以使用 Task.

这是使用后台线程时的一般示例:

final Duration countdownDuration = Duration.ofSeconds(5);
Thread timer = new Thread(() -> {
    LocalTime start = LocalTime.now();
    LocalTime current = LocalTime.now();
    LocalTime end = start.plus(countDownDuration);

    while (end.isAfter(current)) {
        current = LocalTime.now();
        final Duration elapsed = Duration.between(current, end);

        Platform.runLater(() -> timeLeft.set(current)); // As the label is bound to timeLeft, this line must be inside Platform.runLater()
        Thread.sleep(1000);
    }
});

要添加到 Jai 发布的 ,您可以测试不同的实现的性能,并通过简单的打印输出了解它们是否使用单独的线程:

import java.io.IOException;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.PauseTransition;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;

public class TimersTest extends Application {

    @Override public void start(Stage stage) throws IOException {

        System.out.println("Fx thread id "+ Thread.currentThread().getId());

        VBox root = new VBox(new TimeLineCounter(), new PauseTransitionCounter(), new TaskCounter());
        stage.setScene(new Scene(root));
        stage.show();
    }

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

abstract class Counter extends Label {

    protected int count = 0;
    public Counter() {
        setAlignment(Pos.CENTER); setPrefSize(25, 25);
        count();
    }

    abstract void count();
}

class TimeLineCounter extends Counter {

    @Override
    void count() {

        Timeline timeline = new Timeline();
        timeline.setCycleCount(Animation.INDEFINITE);
        KeyFrame keyFrame = new KeyFrame(
                Duration.seconds(1),
                event -> {  setText(String.valueOf(count++) );  }
        );
        timeline.getKeyFrames().add(keyFrame);
        System.out.println("TimeLine thread id "+ Thread.currentThread().getId());
        timeline.play();
    }
}

class PauseTransitionCounter extends Counter {

    @Override
    void count() {

        PauseTransition pauseTransition = new PauseTransition(Duration.seconds(1));
        pauseTransition.setOnFinished(event ->{
            setText(String.valueOf(count++) );
            pauseTransition.play();
        });
        System.out.println("PauseTransition thread id "+ Thread.currentThread().getId());
        pauseTransition.play();
    }
}

class TaskCounter extends Counter {

    @Override
    void count() { count(this); }

    void count(final Label label) {

         Task<Void> counterTask = new Task<>() {
                @Override
                protected Void call() throws Exception {
                    try {
                        System.out.println("Task counter thread id "+ Thread.currentThread().getId());
                        while(true){
                            Platform.runLater(() -> label.setText(String.valueOf(count++)));
                            Thread.sleep(1000);
                        }
                    } catch (InterruptedException e) {e.printStackTrace();     }
                    return null;
                }
            };

            Thread th = new Thread(counterTask);   th.setDaemon(true);    th.start();
    }
}

打印输出显示,正如预期的那样,TimelinePauseTransition 在 FX 线程上,而 Task 不是:

Fx thread id 15
TimeLine thread id 15
PauseTransition thread id 15
Task counter thread id 19

您正在寻找的是 RxJava 及其到 JavaFx 的桥梁,即 RxJavaFx。 导入依赖:

<dependency>
    <groupId>io.reactivex.rxjava2</groupId>
    <artifactId>rxjavafx</artifactId>
    <version>2.2.2</version>
</dependency>

和运行

import java.util.concurrent.TimeUnit;

import io.reactivex.Observable;
import io.reactivex.rxjavafx.observables.JavaFxObservable;
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
import io.reactivex.schedulers.Schedulers;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class TimersApp extends Application {

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

    @Override
    public void start(Stage stage) throws Exception {

        VBox vBox = new VBox();
        for (int i = 0; i < 4; i++) {
            ToggleButton button = new ToggleButton("Start");
            Label label = new Label("0");
            HBox hBox = new HBox(button, label, new Label("seconds"));
            vBox.getChildren().add(hBox);

            JavaFxObservable.valuesOf(button.selectedProperty())
            .switchMap(selected -> {
                if (selected) {
                    button.setText("Stop");
                    return Observable.interval(1, TimeUnit.SECONDS, Schedulers.computation()).map(next -> ++next);
                } else {
                    button.setText("Start");
                    return Observable.empty();
                }
            })
            .map(String::valueOf)
            .observeOn(JavaFxScheduler.platform())
            .subscribe(label::setText);
        }

        stage.setScene(new Scene(vBox));
        stage.show();
    }
}

如果您对此解决方案感兴趣,请告诉我。我会提供一些资料让你学习。

这是另一种方式,标准java。根据倒计时运行的时间长短,人们可能希望在 GUI 关闭时停止这些执行程序。我也在使用 ScheduledExecutorService 进行多次倒计时。

import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class CountDownExecutor extends Application{

    private final int i= 15;
    private final DateTimeFormatter HH_MM_SS = DateTimeFormatter.ofPattern("HH:mm:ss");
    private final Label l1=new Label("00:00:00");
    private final Insets insets = new Insets(3,5,3,5);
    private final Button button = new Button("Start");

    private ScheduledExecutorService executor=null;
    private AtomicInteger atomicInteger = new AtomicInteger();

    public static void main(String[] args) {
        Application.launch(CountDownExecutor.class, args);
    }

    @Override
    public void start(Stage stage) {
        HBox hb = new HBox();
        button.setOnMouseClicked(a-> countDown());
        button.setPadding(insets);
        l1.setPadding(insets);
        hb.getChildren().addAll(button,l1);
        Scene scene = new Scene(hb);
        stage.setOnCloseRequest((ev)-> {if(executor!=null) executor.shutdownNow();});
        stage.setScene(scene);
        stage.show();
    }

    public void countDown() {
        Platform.runLater( () -> button.setDisable(true));
        atomicInteger.set(i);
        setCountDown(LocalTime.ofSecondOfDay(atomicInteger.get()));
        executor = Executors.newScheduledThreadPool(1);

        Runnable r = ()->{
            int j = atomicInteger.decrementAndGet();
            if(j<1 ){
                executor.shutdown();
                Platform.runLater( () ->{ 
                    button.setDisable(false);
                });
                setCountDown(LocalTime.ofSecondOfDay(0));
            }else {
                setCountDown(LocalTime.ofSecondOfDay(j));
            }
        };
        executor.scheduleAtFixedRate(r, 1, 1, TimeUnit.SECONDS);
    }

    public void setCountDown(LocalTime lt)  { Platform.runLater(() -> l1.setText(lt.format(HH_MM_SS))); }
}