Saltar al contenido

Tarea periódica en segundo plano de JavaFX

Posterior a de una larga búsqueda de datos resolvimos este atasco que pueden tener algunos los lectores. Te brindamos la solución y esperamos que te sea de mucha apoyo.

Solución:

Puede usar Timeline para el caso:

Timeline fiveSecondsWonder = new Timeline(
                 new KeyFrame(Duration.seconds(5), 
                 new EventHandler() 

    @Override
    public void handle(ActionEvent event) 
        System.out.println("this is called every 5 seconds on UI thread");
    
));
fiveSecondsWonder.setCycleCount(Timeline.INDEFINITE);
fiveSecondsWonder.play();

para los procesos en segundo plano (que no le hacen nada a la interfaz de usuario) puede usar el bien antiguo java.util.Timer:

new Timer().schedule(
    new TimerTask() 

        @Override
        public void run() 
            System.out.println("ping");
        
    , 0, 5000);

Prefacio: Esta pregunta suele ser el objetivo duplicado de las preguntas que preguntan cómo realizar acciones periódicas en JavaFX, si la acción debe realizarse en segundo plano o no. Si bien ya existen excelentes respuestas a esta pregunta, esta respuesta intenta consolidar toda la información proporcionada (y más) en una sola respuesta y explicar / mostrar las diferencias entre cada enfoque.

Esta respuesta se centra en las API disponibles en JavaSE y JavaFX y no en bibliotecas de terceros como ReactFX (que se muestra en la respuesta de Tomas Mikula).


Información de fondo: JavaFX y subprocesos

Como la mayoría de los marcos de GUI convencionales, JavaFX es de un solo subproceso. Esto significa que hay un solo hilo dedicado a leer y escribir el estado de la interfaz de usuario y procesar eventos generados por el usuario (por ejemplo, eventos del mouse, eventos clave, etc.). En JavaFX, este hilo se llama “hilo de aplicación JavaFX”, a veces abreviado a solo “hilo FX”, pero otros marcos pueden llamarlo de otra manera. Algunos otros nombres incluyen “subproceso de interfaz de usuario”, “subproceso de envío de eventos” y “subproceso principal”.

Es absolutamente primordial que todo lo que esté conectado a la GUI que se muestra en la pantalla solo se acceda o manipule en el Hilo de aplicación JavaFX. El marco JavaFX no es seguro para subprocesos y el uso de un subproceso diferente para leer o escribir incorrectamente el estado de la interfaz de usuario puede conducir a un comportamiento indefinido. Incluso si no ve ningún problema visible externamente, el acceso al estado compartido entre subprocesos sin la sincronización necesaria es un código roto.

Sin embargo, muchos objetos GUI pueden manipularse en cualquier hilo siempre que no estén “activos”. De la documentación de javafx.scene.Node:

Los objetos de nodo se pueden construir y modificar en cualquier hilo siempre que aún no estén adjuntos a un Scene en un Window es decir showing[emphasis added]. Una aplicación debe adjuntar nodos a dicha escena o modificarlos en el subproceso de aplicación JavaFX.

Pero otros objetos GUI, como Window e incluso algunas subclases de Node (p.ej WebView), son más estrictas. Por ejemplo, de la documentación de javafx.stage.Window:

Los objetos de ventana deben construirse y modificarse en el subproceso de aplicación JavaFX.

Si no está seguro de las reglas de subprocesamiento de un objeto GUI, su documentación debe proporcionar la información necesaria.

Dado que JavaFX es de un solo subproceso, también debe asegurarse de nunca bloquear o monopolizar el subproceso FX. Si el hilo no tiene la libertad de hacer su trabajo, la interfaz de usuario nunca se vuelve a dibujar y los nuevos eventos generados por el usuario no se pueden procesar. No seguir esta regla puede llevar a la infame interfaz de usuario que no responde / congelada y sus usuarios no están contentos.

Prácticamente siempre está mal dormir el Hilo de aplicación JavaFX.


Tareas periódicas

Hay dos tipos diferentes de tareas periódicas, al menos para los propósitos de esta respuesta:

  1. Periódico primer plano “Tareas”.
    • Esto podría incluir cosas como un nodo “parpadeante” o cambiar periódicamente entre imágenes.
  2. Periódico antecedentes Tareas.
    • Un ejemplo podría ser la comprobación periódica de actualizaciones en un servidor remoto y, si las hay, descargar la nueva información y mostrársela al usuario.

Tareas periódicas en primer plano

Si su tarea periódica es corta y simple, entonces usar un hilo en segundo plano es excesivo y solo agrega una complejidad innecesaria. La solución más apropiada es utilizar el javafx.animation API. Las animaciones son asincrónicas pero permanecen completamente dentro del Hilo de aplicación JavaFX. En otras palabras, las animaciones proporcionan una forma de “bucle” en el hilo FX, con retrasos entre cada iteración, sin utilizar realmente bucles.

Hay tres clases especialmente adecuadas para tareas periódicas en primer plano.

Cronología

A Timeline se compone de uno o más KeyFrames. Cada KeyFrame tiene un tiempo específico de cuándo debería completarse. Cada uno también puede tener un controlador “al finalizar” que se invoca después de que haya transcurrido la cantidad de tiempo especificada. Esto significa que puede crear un Timeline con un solo KeyFrame que ejecuta periódicamente una acción, repitiendo tantas veces como desee (incluso para siempre).

import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;

public class App extends Application 

  @Override
  public void start(Stage primaryStage) 
    Rectangle rect = new Rectangle(100, 100);

    // toggle the visibility of 'rect' every 500ms
    Timeline timeline =
        new Timeline(new KeyFrame(Duration.millis(500), e -> rect.setVisible(!rect.isVisible())));
    timeline.setCycleCount(Animation.INDEFINITE); // loop forever
    timeline.play();

    primaryStage.setScene(new Scene(new StackPane(rect), 200, 200));
    primaryStage.show();
  

Desde un Timeline puede tener más de uno KeyFrame es posible ejecutar acciones en diferentes intervalos. Solo tenga en cuenta que los tiempos de cada KeyFrameno apilar. Si tienes uno KeyFrame con un tiempo de dos segundos seguido de otro KeyFrame con un tiempo de dos segundos, ambos KeyFrames finalizará dos segundos después de que se inicie la animación. Tener el segundo KeyFrame terminar dos segundos después del primero, su tiempo debe ser cuatro segundos.

PauseTransition

A diferencia de las otras clases de animación, un PauseTransition en realidad no está acostumbrado animar cualquier cosa. Su propósito principal es ser utilizado como hijo de SequentialTransition para poner una pausa entre otras dos animaciones. Sin embargo, como todas las subclases de Animation puede tener un controlador “al finalizar” que se ejecuta después de que se completa, lo que permite su uso para tareas periódicas.

import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;

public class App extends Application 

  @Override
  public void start(Stage primaryStage) 
    Rectangle rect = new Rectangle(100, 100);

    // toggle the visibility of 'rect' every 500ms
    PauseTransition pause = new PauseTransition(Duration.millis(500));
    pause.setOnFinished(
        e -> 
          rect.setVisible(!rect.isVisible());
          pause.playFromStart(); // loop again
        );
    pause.play();

    primaryStage.setScene(new Scene(new StackPane(rect), 200, 200));
    primaryStage.show();
  

Observe que el controlador finalizado invoca playFromStart(). Esto es necesario para “repetir” la animación nuevamente. los cycleCount La propiedad no se puede usar ya que el controlador finalizado no se invoca al final de cada ciclo, solo se invoca al final del último ciclo. Lo mismo es cierto de Timeline; la razón por la que funciona Timeline anterior se debe a que el controlador finalizado no está registrado con el Timeline pero con el KeyFrame.

Desde el cycleCount la propiedad no se puede utilizar para PauseTransition para múltiples ciclos, hace que sea más difícil realizar un bucle solo un cierto número de veces (en lugar de para siempre). Tienes que realizar un seguimiento del estado tú mismo y solo invocar playFromStart() cuando sea apropiado. Manten eso en mente variables locales declarada fuera de una expresión lambda o clase anónima pero usada dentro de dicha expresión lambda o clase anónima debe ser final o efectivamente final.

Temporizador de animación

los AnimationTimer class es el nivel más bajo de la API de animación de JavaFX. No es una subclase de Animation y por lo tanto no tiene ninguna de las propiedades que se utilizaron anteriormente. En cambio, tiene un método abstracto que, cuando se inicia el temporizador, se invoca una vez por cuadro con la marca de tiempo (en nanosegundos) del cuadro actual: #handle(long). Para ejecutar algo periódicamente con AnimationTimer (que no sea una vez por fotograma) requerirá calcular manualmente las diferencias de tiempo entre las invocaciones de handle utilizando el argumento del método.

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class App extends Application 

  @Override
  public void start(Stage primaryStage) 
    Rectangle rect = new Rectangle(100, 100);

    // toggle the visibility of 'rect' every 500ms
    AnimationTimer timer =
        new AnimationTimer() 

          private long lastToggle;

          @Override
          public void handle(long now) 
            if (lastToggle == 0L) 
              lastToggle = now;
             else 
              long diff = now - lastToggle;
              if (diff >= 500_000_000L)  // 500,000,000ns == 500ms
                rect.setVisible(!rect.isVisible());
                lastToggle = now;
              
            
          
        ;
    timer.start();

    primaryStage.setScene(new Scene(new StackPane(rect), 200, 200));
    primaryStage.show();
  

Para la mayoría de los casos de uso similares a los anteriores, use Timeline o PauseTransition sería la mejor opción.

Tareas periódicas en segundo plano

Si su tarea periódica requiere mucho tiempo (por ejemplo, cálculos costosos) o está bloqueada (por ejemplo, E / S), entonces se debe utilizar un subproceso en segundo plano. JavaFX viene con algunas utilidades de simultaneidad integradas para ayudar con la comunicación entre los subprocesos en segundo plano y el subproceso FX. Estas utilidades se describen en:

  • El tutorial de Concurrencia en JavaFX, y
  • La documentación de las clases en el javafx.concurrent paquete.

Para tareas periódicas en segundo plano que necesitan comunicarse con el hilo FX, la clase que se debe usar es javafx.concurrent.ScheduledService. Esa clase ejecutará su tarea periódicamente, reiniciando después de una ejecución exitosa, en base a un período específico. Si está configurado para hacerlo, incluso reintentará una cantidad configurable de veces después de ejecuciones fallidas.

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.concurrent.Worker.State;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;

public class App extends Application 

  // maintain a strong reference to the service
  private UpdateCheckService service;

  @Override
  public void start(Stage primaryStage) 
    service = new UpdateCheckService();
    service.setPeriod(Duration.seconds(5));

    Label resultLabel = new Label();
    service.setOnRunning(e -> resultLabel.setText(null));
    service.setOnSucceeded(
        e -> 
          if (service.getValue()) 
            resultLabel.setText("UPDATES AVAILABLE");
           else 
            resultLabel.setText("UP-TO-DATE");
          
        );

    Label msgLabel = new Label();
    msgLabel.textProperty().bind(service.messageProperty());

    ProgressBar progBar = new ProgressBar();
    progBar.setMaxWidth(Double.MAX_VALUE);
    progBar.progressProperty().bind(service.progressProperty());
    progBar.visibleProperty().bind(service.stateProperty().isEqualTo(State.RUNNING));

    VBox box = new VBox(3, msgLabel, progBar);
    box.setMaxHeight(Region.USE_PREF_SIZE);
    box.setPadding(new Insets(3));

    StackPane root = new StackPane(resultLabel, box);
    StackPane.setAlignment(box, Pos.BOTTOM_LEFT);

    primaryStage.setScene(new Scene(root, 400, 200));
    primaryStage.show();

    service.start();
  

  private static class UpdateCheckService extends ScheduledService 

    @Override
    protected Task createTask() 
      return new Task<>() 

        @Override
        protected Boolean call() throws Exception 
          updateMessage("Checking for updates...");
          for (int i = 0; i < 1000; i++) 
            updateProgress(i + 1, 1000);
            Thread.sleep(1L); // fake time-consuming work
          
          return Math.random() < 0.5; // 50-50 chance updates are "available"
        
      ;
    
  

Aquí hay una nota de la documentación de ScheduledService:

El tiempo para esta clase no es absolutamente confiable. Un subproceso de eventos muy ocupado puede introducir un retraso de tiempo en el comienzo de la ejecución de la Tarea en segundo plano, por lo que es probable que valores muy pequeños para el período o retraso sean inexactos. Un retraso o período de cientos de milisegundos o más debería ser bastante confiable.

Y otro:

los ScheduledService introduce una nueva propiedad llamada lastValue. los lastValue es el valor que se calculó con éxito por última vez. Porque un Service limpia su value propiedad en cada ejecución, y porque el ScheduledService reprogramará una ejecución inmediatamente después de su finalización (a menos que entre en el estado cancelado o fallido), el value propiedad no es demasiado útil en un ScheduledService. En la mayoría de los casos, querrá utilizar en su lugar el valor devuelto por lastValue.

La última nota significa atar a la value propiedad de un ScheduledService es con toda probabilidad inútil. El ejemplo anterior funciona a pesar de consultar el value propiedad porque la propiedad se consulta en el onSucceeded handler, antes de que se reprograme el servicio.

Sin interacción con la interfaz de usuario

Si la tarea periódica en segundo plano no necesita interactuar con la interfaz de usuario, puede utilizar las API estándar de Java. Más específicamente, ya sea:

  • los java.util.Timer clase (nojavax.swing.Timer),
  • O el mas moderno java.util.concurrent.ScheduledExecutorService interfaz.

Tenga en cuenta que ScheduledExecutorService admite hilo quinielas, diferente a Timer que solo admite un solo hilo.

ScheduledService no es una opción

Si por alguna razón no puedes usar ScheduledService, pero necesita interactuar con la interfaz de usuario de todos modos, entonces debe asegurarse de que el código que interactúa con la interfaz de usuario, y solo ese código, se ejecute en el hilo FX. Esto se puede lograr usando Platform#runLater(Runnable).

Ejecute el Runnable especificado en el subproceso de aplicación JavaFX en algún momento no especificado en el futuro. Este método, que se puede llamar desde cualquier hilo, publicará el Runnable en una cola de eventos y luego regresará inmediatamente a la persona que llama. Los Runnables se ejecutan en el orden en que se publican. Un ejecutable pasado al método runLater se ejecutará antes de que cualquier Runnable pase a una llamada posterior a runLater. Si se llama a este método después de que se haya cerrado el tiempo de ejecución de JavaFX, la llamada se ignorará: no se ejecutará Runnable y no se lanzará ninguna excepción.

NOTA: las aplicaciones deben evitar inundar JavaFX con demasiados Runnables pendientes. De lo contrario, la aplicación puede dejar de responder. Aplicaciones se les anima a agrupar varias operaciones en menos llamadas runLater. Además, las operaciones de larga duración deben realizarse en un subproceso en segundo plano siempre que sea posible, liberando el subproceso de la aplicación JavaFX para las operaciones de la GUI.

[...]

Preste atención a la nota de la documentación anterior. los javafx.concurent.Task La clase evita esto uniendo actualizaciones a su message, progress, y value propiedades. Esto se implementa actualmente mediante el uso de un AtomicReference y operaciones estratégicas de puesta en marcha. Si está interesado, puede echar un vistazo a la implementación (JavaFX es de código abierto).

Preferiría la transición de pausa:

PauseTransition wait = new PauseTransition(Duration.seconds(5));
wait.setOnFinished((e) -> 
    /*YOUR METHOD*/
    wait.playFromStart();
);
wait.play();

Ten en cuenta difundir esta división si si solucionó tu problema.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)


Tags : /

Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *