JavaFX Exécutant une grande quantité de compte à rebours à la fois?


Donc je peux voir quelques façons différentes de faire ce dont j'ai besoin et j'ai fait un tas de recherches Google/stack overflow mais je ne trouve pas vraiment ce que je cherche. J'ai besoin d'exécuter plusieurs "compte à rebours"."J'ai besoin d'avoir environ 6 à 10 minuteries de compte à rebours en cours d'exécution à la fois à des moments différents. J'ai un volet d'onglet sur mon programme principal dans lequel j'inclus le FXML et j'injecte les contrôleurs. L'onglet Timers a un contrôleur différent du programme principal.

Donc la 1ère question que j'ai est. Étant donné que cet "onglet" s'exécute sur un contrôleur séparé mais est inclus dans le programme principal, s'exécute-t-il sur un thread d'application séparé?

Voici un exemple de ce à quoi ressemble l'onglet FXML inclus...

Rebours

Lorsque j'appuie sur chaque bouton de démarrage. Je peux créer un Timeline et KeyFrame pour chaque minuterie. Cependant, je ne pense pas vraiment que ce soit la meilleure façon de procéder. Spécialement une fois que vous obtenez jusqu'à 10 délais en cours d'exécution en même temps, et certainement si cela ne fonctionne pas sur un thread d'application séparé du programme principal.

J'ai pensé à envoyer chaque demande de démarrage à un ExecutorService et newCacheThreadPool mais je dois pouvoir mettre à jour les étiquettes sur l'interface graphique avec le temps restant et je comprends que vous n'êtes pas censé le faire avec des services d'arrière-plan. Platform.runLater() peut-être?

L'autre idée était d'utiliser le Timer de la classe java.util.Timer. Cependant, je vois cela comme ayant les mêmes problèmes que le ExecutorService quand j'ai besoin de mettre à jour les étiquettes de l'interface graphique. Je comprends également que la classe Timer ne crée qu'un seul thread et effectue ses tâches séquentiellement. Donc, cela ne fonctionnerait pas.

Ou devrais-je avoir une toute autre classe "compte à rebours" dont je peux créer de nouvelles instances avec chacune, puis démarrer de nouveaux threads. Cependant, si je fais cela, comment puis-je mettre à jour continuellement l'interface graphique. Je devrais toujours interroger la classe de compte à rebours en utilisant un timeline non? Donc, cela irait à l'encontre du but de toute cette chose.

Author: E_net4 the curator, 2018-12-03

4 answers

Donc la 1ère question que j'ai est. Étant donné que cet "onglet" s'exécute sur un contrôleur séparé mais est inclus dans le programme principal, s'exécute-t-il sur un thread d'application séparé?

Non, il ne peut y avoir que une instance d'applicationJavaFX par JVM, et aussi un Thread d'application JavaFX par JVM.

Quant à la façon dont vous pouvez mettre à jour la minuterie, il est bon d'utiliser Timeline - un pour chaque minuterie. {[2] } ne fonctionne pas sur un thread séparé - il est déclenché par le sous-jacent "scene graph rendering pulse" qui est responsable de la mise à jour périodique de l'interface graphique JavaFX. Avoir plus d'instances Timeline signifie simplement qu'il y a plus d'auditeurs qui s'abonnent à l'événement "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...
    }
}

Bien sûr, vous pouvez également utiliser Executor et d'autres threading méthodes, à condition de mettre à jour le Label par Platform.runLater(). Alternativement, vous pouvez utiliser un Task.

Ceci est un exemple général lors de l'utilisation du thread d'arrière-plan:

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);
    }
});
 4
Author: Jai, 2018-12-03 08:19:38

Pour ajouter à la bonne réponse publiée par Jai, vous pouvez tester différentes implémentations pour les performances et savoir si elles utilisent des threads séparés par une simple impression:

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

L'impression montre, comme attendu, que Timeline et PauseTransition sont sur le FX fil, tandis que Task n'est pas:

Fx id de thread 15
TimeLine id de thread 15
PauseTransition id de thread 15
Id de thread de compteur de tâches 19

 1
Author: c0der, 2018-12-03 06:23:17

Ce que vous recherchez est RxJava et son pont vers JavaFX qui est RxJavaFx. Dépendance d'importation:

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

Et exécutez

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

Faites-moi savoir si vous êtes intéressé par cette solution. Je vais vous fournir du matériel à apprendre.

 0
Author: Przemek Krysztofiak, 2018-12-03 12:37:43

Voici une autre façon, java standard. Selon la durée du compte à rebours, on peut vouloir arrêter ces exécuteurs lorsque l'interface graphique est fermée. J'utilise également ScheduledExecutorService pour plusieurs comptes à rebours.

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))); }
} 
 0
Author: 1813222, 2020-06-06 21:57:39