JavaFX Running a large amount of countdown timers at once?

江枫思渺然 提交于 2020-06-23 17:34:12

问题


So I can see a couple different ways to do what I need and I've done a bunch of google/stack overflow searching but can't find really what I'm looking for. I need to run multiple "Countdown timers." I need to have about 6 possibly up to 10 countdown timers running at once all at different times. I have a tab pane on my main program that I am including the FXML and injecting the controllers into. The Timers Tab has a different controller than the main program.

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?

Here is an example of what the included tab FXML looks like...

When I press each start button. I can create a Timeline and KeyFrame for each timer. However, I don't really think that is the best way to go about it. Specially once you get up to 10 timelines running at the same time, and definitely if this is not running on a separate application thread from the main program.

I thought about sending each start request to an ExecutorService and newCacheThreadPool however I need to be able to update the labels on the GUI with the current time remaining and I understand you are not supposed to do that with background services. Platform.runLater() maybe?

The other idea was using the Timer from the java.util.Timer class. However, I see this as having the same problems as the ExecutorService when I need to update the GUI Labels. I also understand that the Timer class only creates one thread and performs it's tasks sequentially. So, that wouldn't work.

Or, should I have a whole other "CountDown" class that I can create new instances of with each one and then start new threads in. However, if I do that, how do I continually update the GUI. I still would have to poll the CountDown class using a timeline right? So that would defeat the purpose of this whole thing.


回答1:


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?

No, there can only be one JavaFX Application instance per JVM, and also one JavaFX Application Thread per JVM.

As for how you could update the timer, it is fine to use Timeline - one for each timer. Timeline does not run on separate thread - it is triggered by the underlying "scene graph rendering pulse" which is responsible for updating the JavaFX GUI periodically. Having more Timeline instances basically just means that there are more listeners that subscribes to the "pulse" event.

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...
    }
}

Of course, you can also use Executor and other threading methods, provided you update the Label via Platform.runLater(). Alternatively, you could use a Task.

This is a general example when using background thread:

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);
    }
});



回答2:


To add to the good answer posted by Jai, you could test different implementations for performance and find out if they use separate threads by a simple printout:

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();
    }
}

The printout shows, as expected, that Timeline and PauseTransition are on the FX thread, while Task is not:

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




回答3:


What you are looking for is RxJava and its bridge to JavaFx which is RxJavaFx. Import dependency:

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

and run

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();
    }
}

Let me know if you are intrested in this solution. I will provide you some materials to learn.




回答4:


Here is another way, standard java. Depending on how long the countdown runs one may want to stop those executors when the GUI is closed. I am using the ScheduledExecutorService also for multiple countdowns.

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))); }
} 


来源:https://stackoverflow.com/questions/53587355/javafx-running-a-large-amount-of-countdown-timers-at-once

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!