Problème mise à jour texte Label Chronomètre- Java [Résolu]

Signaler
-
 maxoulx -
Bonjour à tous,

Je suis débutant en JavaFX et j'essaye de créer un chronomètre qui démarre à partir du moment ou on appuie sur un bouton.

Pour le moment, je veux juste faire en sorte qu'il aille jusqu'à 60 secondes.

J'ai essayé plusieurs méthodes sans réussite malgré mes recherches. Avec mon code, quand j'appuie sur le bouton, le text de mon label ne se met pas à jour.

(J'arrive à les afficher dans la console Eclipse bien évidemment, mais mon programme plante dès que j'essaie de l'afficher dans ma fenêtre).

Je vous partage mon code,

Merci d'avance pour vos réponses.

Bonne soirée.
package chrono;

import javafx.application.Application ;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class MainApp extends Application{
 private int secondes = 0;
 public void start (Stage primaryStage) throws Exception{
  primaryStage.setTitle("Mon chronomètre");
  
  VBox root = new VBox() ;
  
  Label label = new Label("Temps :");
  
  Button boutonTemps = new Button("Lancer le chrono");
  
  boutonTemps.setOnAction(new EventHandler<ActionEvent>() {
   
   @Override
   public void handle(ActionEvent e) { 
    System.out.println("Bouton activé");
       
       while (secondes < 60) {
         label.setText("Temps :"+secondes+"s");
         try {
          Thread.sleep(1000);       
         }
         catch (InterruptedException z) {
         }
         secondes++; 
       }       
   }
  });
  
  root.getChildren().add(label);
  root.getChildren().add(boutonTemps);
  Scene scene = new Scene (root, 600,400);
    
  primaryStage.setScene(scene);
  primaryStage.show(); 
 }
}

3 réponses

Messages postés
16031
Date d'inscription
samedi 31 mai 2008
Statut
Modérateur
Dernière intervention
7 septembre 2020
2 670
Bonjour,

Une méthode d'action (handle) doit être très rapide, sinon le bouton va rester coincé et c'est toute la fenêtre qui va freezer.
Il est donc hors de question de mettre des sleep dans ce genre de méthodes.

Ce qu'il faut faire c'est avoir un thread à part, qui va gérer la boucle et les sleep. L'action du bouton doit se contenter de démarrer la tâche de fond et rendre la main immédiatement.

Voir :
https://fabrice-bouye.developpez.com/tutoriels/javafx/gui-service-tache-de-fond-thread-javafx/
Bonjour,
Merci de votre réponse.

J'ai essayé de le faire avec un thread et une task.
J'ai séparé mon code comme cela :

MainApp.java :
package chrono;

import javafx.application.Application ;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;


 
public class MainApp extends Application{
 
 VBox root = new VBox() ;
 
 Scene scene = new Scene (root, 600,400);

 Label label = new Label("Temps :");
 
 Task <Void> task ;
 
 Button boutonTemps = new Button("Lancer le chrono");
 
 public void start (Stage primaryStage) throws Exception{ 
  
  primaryStage.setTitle("Mon chronomètre");
  
  boutonTemps.setOnAction(new EventHandler<ActionEvent>() {   
   @Override
   public void handle(ActionEvent e) { 
    task = new Task <Void>() {
     protected Void call() {
      
      new lancerTimer().start(label);   
      return null ;
     }
    };
    Thread thread = new Thread(task);
          thread.start();

   }
  });

    
  root.getChildren().add(label);
  root.getChildren().add(boutonTemps);   
  primaryStage.setScene(scene);
  primaryStage.show(); 
  
 }
 

}


et lancerTimer.java :
package chrono;

import javafx.scene.control.Label;

public class lancerTimer {
 public void start(Label label){
  int secondes = 0 ;
  // TODO Auto-generated method stub
  System.out.println("Bouton activé");
  while (secondes < 60) {
   // Mise à jour label 
   label.setText("Temps :"+secondes+"s");
   // Affichage dans la console
   System.out.println(secondes);
   try {
    Thread.sleep(1000);       
   }
   catch (InterruptedException z) {
   }
   secondes++; 
  }
  
 }
}


J'obtiens cette erreur " Exception in thread "Thread-2" java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-2" et rien ne se passe dans ma fenêtre JavaFX.

Je dois mal maîtriser la notion de Task et Thread...
Si vous arrivez à m'indiquer ce qui ne marche pas... Merci encore de votre aide !
Cordialement
Messages postés
16031
Date d'inscription
samedi 31 mai 2008
Statut
Modérateur
Dernière intervention
7 septembre 2020
2 670 > maxoulx
Bonjour,
"IllegalStateException: Not on FX application thread"
Le problème c'est que tu essayes de modifier l'affichage directement dans le thread de ta tâche de fond, alors qu'il devrait être modifié par le thread d'affichage JavaFX, il faut que tu invoques la méthode Platform.runLater()
Platform.runLater(() -> label.setText("Temps :"+secondes.get()+"s"));

Remarque : plutôt que d'utiliser une boucle et un sleep, tu peux utiliser un ScheduledService, par exemple :
package chrono;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;

import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;

public class TimerService extends ScheduledService<Void> {

    private final AtomicInteger timer;
    private final Consumer<Integer> onCall;

    public TimerService(Consumer<Integer> onCall) {
        timer = new AtomicInteger(0);
        this.onCall = onCall;
    }

    @Override
    protected Task<Void> createTask() {
        return new Task<Void>() {
            @Override
            protected Void call() {
                onCall.accept(timer.getAndIncrement());
                return null;
            }
        };
    }
}

package chrono;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;

public class MainApp extends Application {

    public void start(Stage primaryStage) throws Exception {
        primaryStage.setTitle("Mon chronomètre");

        Label label = new Label("Temps :");
        TimerService timerService = new TimerService(time ->
            Platform.runLater(() -> label.setText("Temps : " + time + "s")));
        timerService.setPeriod(Duration.seconds(1));

        Button boutonTemps = new Button("Démarrer");
        boutonTemps.setOnAction(e -> {
            if (timerService.isRunning()) {
                timerService.cancel();
                Platform.runLater(() -> boutonTemps.setText("Reprendre"));
            } else {
                timerService.restart();
                Platform.runLater(() -> boutonTemps.setText("Interrompre"));
            }
        });

        VBox root = new VBox();
        root.getChildren().add(label);
        root.getChildren().add(boutonTemps);

        Scene scene = new Scene(root, 600, 400);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
Bonjour,
Merci enormément pour cette réponse ultra-complète !
Ca marche, c'est ce qu'il me fallait.

Maintenant, je voudrai passer en mode affichage minute et secondes côte à côte.
C'est à dire obtenir quelque chose du style : Temps 01m 15s.

J'ai bien créé ma variable minutes et fait ma condition dans le Timer Service pour incrémenter les minutes.
public class TimerService extends ScheduledService<Void> {

private final AtomicInteger secondes;
private final AtomicInteger minutes;
private final Consumer<Integer> onCall;

public TimerService(Consumer<Integer> onCall) {
secondes = new AtomicInteger(0);
minutes = new AtomicInteger(0);
this.onCall = onCall;
}

@Override
protected Task<Void> createTask() {
return new Task<Void>() {
@Override
protected Void call() {
onCall.accept(secondes.getAndIncrement());
if (secondes.get() > 59) {
secondes.set(0);
onCall.accept(minutes.getAndIncrement());
// Test affichage dans la console
System.out.println(minutes);
}
return null;
}
};
}
public AtomicInteger getSecondes() {
return secondes;
}

public AtomicInteger getMinutes() {
return minutes;
}
}


Mais du coup, je cherche maintenant à récupérer les minutes, les afficher et récupérer les seconde puis les afficher.

Je pensais pouvoir utiliser les getters de secondes et minutes, mais j'ai une erreur du type " local variable timerService may not have been initialized".

Finalement, comment récupérer les secondes et les minutes indépendamment ?
   TimerService timerService = new TimerService(timer ->
Platform.runLater(() -> label.setText("Temps : "+timerService.getMinutes()+"min"+timerService.getSecondes()+"sec")));


Merci encore.
Messages postés
16031
Date d'inscription
samedi 31 mai 2008
Statut
Modérateur
Dernière intervention
7 septembre 2020
2 670
"local variable timerService may not have been initialized"
Le problème c'est que tu fais
TimerService timerService = new TimerService(... timerService ...)
mais tu ne peux pas utiliser timerService tant que son new n'est pas terminé, il est donc impossible qu'il puisse être utilisé en paramètre de son propre constructeur.

Pour que ça fonctionne avec les minutes, tu n'as pas besoin de modifier TimerService, il faut juste faire la division par 60 du nombre de secondes.

TimerService timerService = new TimerService(time -> {
    final int minutes = time / 60;
    final int seconds = time % 60;
    final String text = "Temps : " + minutes + "min" + secondes + "sec";
    Platform.runLater(() -> label.setText(text));
});
Voilà, j'avais compris ce problème.
Je pensais que je devais gérer le traitement secondes et minutes dans ma class TimerService.

Merci beaucoup !