web-dev-qa-db-fra.com

Mise à jour de l'interface utilisateur à partir de différents threads dans JavaFX

Je développe une application avec plusieurs objets TextField qui doivent être mis à jour pour refléter les changements dans les propriétés back-end associées. Les TextField ne sont pas modifiables, seul le back-end peut changer leur contenu.

Si je comprends bien, la bonne façon de procéder consiste à exécuter le calcul lourd sur un thread séparé afin de ne pas bloquer l'interface utilisateur. J'ai fait cela en utilisant javafx.concurrent.Task Et j'ai communiqué une valeur unique au thread JavaFX en utilisant updateMessage(), ce qui fonctionnait bien. Cependant, j'ai besoin de plus d'une valeur à mettre à jour car le back-end fait son crunching.

Étant donné que les valeurs back-end sont stockées en tant que propriétés JavaFX, j'ai simplement essayé de les lier au textProperty de chaque élément GUI et de laisser les liaisons faire le travail. Cela ne fonctionne pas, cependant; après avoir fonctionné pendant quelques instants, les TextField s'arrêtent de se mettre à jour même si la tâche principale est toujours en cours d'exécution. Aucune exception n'est levée.

J'ai également essayé d'utiliser Platform.runLater() pour mettre à jour activement les TextField plutôt que de les lier. Le problème ici est que les tâches runLater() sont planifiées plus rapidement que la plate-forme ne peut les exécuter, et donc l'interface graphique devient lente et a besoin de temps pour "rattraper" même une fois la tâche principale terminée.

J'ai trouvé quelques questions ici:

Les entrées de l'enregistreur traduites dans l'interface utilisateur cessent d'être mises à jour avec le temps

Le multithreading dans JavaFX bloque l'interface utilisateur

mais mon problème persiste.

En résumé: j'ai un back-end apportant des modifications aux propriétés et je souhaite que ces modifications apparaissent sur l'interface graphique. Le back-end est un algorithme génétique, donc son fonctionnement est décomposé en générations discrètes. Ce que je voudrais, c'est que les TextFields se rafraîchissent au moins une fois entre les générations, même si cela retarde la génération suivante. Il est plus important que l'interface graphique réponde bien que que le GA s'exécute rapidement.

Je peux poster quelques exemples de code si je n'ai pas clarifié le problème.

[~ # ~] mise à jour [~ # ~]

J'ai réussi à le faire en suivant la suggestion de James_D. Pour résoudre le problème du back-end devant attendre que la console s'imprime, j'ai implémenté une sorte de console tamponnée. Il stocke les chaînes à imprimer dans un StringBuffer et les ajoute en fait à la TextArea lorsqu'une méthode flush() est appelée. J'ai utilisé un AtomicBoolean pour empêcher la génération suivante de se produire jusqu'à ce que le vidage soit terminé, car il est effectué par une exécutable Platform.runLater(). Notez également que cette solution est incroyablement lente.

31
eddy_hunter

Je ne sais pas si je comprends parfaitement, mais je pense que cela peut aider.

L'utilisation de Platform.runLater (...) est une approche appropriée pour cela.

L'astuce pour éviter d'inonder le thread d'application FX consiste à utiliser une variable atomique pour stocker la valeur qui vous intéresse. Dans la méthode Platform.runLater (...), récupérez-la et définissez-la sur une valeur sentinelle. À partir de votre thread d'arrière-plan, mettez à jour la variable Atomic, mais n'émettez un nouveau Platform.runLater (...) que s'il a été réinitialisé à sa valeur sentinelle.

J'ai compris cela en regardant le code source de la tâche . Jetez un œil à la façon dont la méthode updateMessage (..) (ligne 1131 au moment de la rédaction) est implémentée.

Voici un exemple qui utilise la même technique. Cela a juste un thread d'arrière-plan (occupé) qui compte aussi vite que possible, mettant à jour un IntegerProperty. Un observateur observe cette propriété et met à jour un AtomicInteger avec la nouvelle valeur. Si la valeur actuelle de AtomicInteger est -1, il planifie un Platform.runLater ().

Dans Platform.runLater, je récupère la valeur de l'AtomicInteger et l'utilise pour mettre à jour une étiquette, redéfinissant la valeur sur -1 dans le processus. Cela signifie que je suis prêt pour une autre mise à jour de l'interface utilisateur.

import Java.text.NumberFormat;
import Java.util.concurrent.atomic.AtomicInteger;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;

public class ConcurrentModel extends Application {

  @Override
  public void start(Stage primaryStage) {

    final AtomicInteger count = new AtomicInteger(-1);

    final AnchorPane root = new AnchorPane();
    final Label label = new Label();
    final Model model = new Model();
    final NumberFormat formatter = NumberFormat.getIntegerInstance();
    formatter.setGroupingUsed(true);
    model.intProperty().addListener(new ChangeListener<Number>() {
      @Override
      public void changed(final ObservableValue<? extends Number> observable,
          final Number oldValue, final Number newValue) {
        if (count.getAndSet(newValue.intValue()) == -1) {
          Platform.runLater(new Runnable() {
            @Override
            public void run() {
              long value = count.getAndSet(-1);
              label.setText(formatter.format(value));
            }
          });          
        }

      }
    });
    final Button startButton = new Button("Start");
    startButton.setOnAction(new EventHandler<ActionEvent>() {
      @Override
      public void handle(ActionEvent event) {
        model.start();
      }
    });

    AnchorPane.setTopAnchor(label, 10.0);
    AnchorPane.setLeftAnchor(label, 10.0);
    AnchorPane.setBottomAnchor(startButton, 10.0);
    AnchorPane.setLeftAnchor(startButton, 10.0);
    root.getChildren().addAll(label, startButton);

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

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

  public class Model extends Thread {
    private IntegerProperty intProperty;

    public Model() {
      intProperty = new SimpleIntegerProperty(this, "int", 0);
      setDaemon(true);
    }

    public int getInt() {
      return intProperty.get();
    }

    public IntegerProperty intProperty() {
      return intProperty;
    }

    @Override
    public void run() {
      while (true) {
        intProperty.set(intProperty.get() + 1);
      }
    }
  }
}

Si vous voulez vraiment "piloter" le back-end à partir de l'interface utilisateur: c'est-à-dire limiter la vitesse de l'implémentation du backend pour voir toutes les mises à jour, pensez à utiliser un AnimationTimer. Un AnimationTimer a une handle(...) qui est appelée une fois par rendu d'image. Vous pouvez donc bloquer l'implémentation back-end (par exemple en utilisant une file d'attente de blocage) et la libérer une fois par appel de la méthode handle. La méthode handle(...) est invoquée sur le thread d'application FX.

La méthode handle(...) prend un paramètre qui est un horodatage (en nanosecondes), vous pouvez donc l'utiliser pour ralentir davantage les mises à jour, si une fois par image est trop rapide.

Par exemple:

import Java.util.concurrent.ArrayBlockingQueue;
import Java.util.concurrent.BlockingQueue;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;


public class Main extends Application {
    @Override
    public void start(Stage primaryStage) {

        final BlockingQueue<String> messageQueue = new ArrayBlockingQueue<>(1);

        TextArea console = new TextArea();

        Button startButton = new Button("Start");
        startButton.setOnAction(event -> {
            MessageProducer producer = new MessageProducer(messageQueue);
            Thread t = new Thread(producer);
            t.setDaemon(true);
            t.start();
        });

        final LongProperty lastUpdate = new SimpleLongProperty();

        final long minUpdateInterval = 0 ; // nanoseconds. Set to higher number to slow output.

        AnimationTimer timer = new AnimationTimer() {

            @Override
            public void handle(long now) {
                if (now - lastUpdate.get() > minUpdateInterval) {
                    final String message = messageQueue.poll();
                    if (message != null) {
                        console.appendText("\n" + message);
                    }
                    lastUpdate.set(now);
                }
            }

        };

        timer.start();

        HBox controls = new HBox(5, startButton);
        controls.setPadding(new Insets(10));
        controls.setAlignment(Pos.CENTER);

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

    private static class MessageProducer implements Runnable {
        private final BlockingQueue<String> messageQueue ;

        public MessageProducer(BlockingQueue<String> messageQueue) {
            this.messageQueue = messageQueue ;
        }

        @Override
        public void run() {
            long messageCount = 0 ;
            try {
                while (true) {
                    final String message = "Message " + (++messageCount);
                    messageQueue.put(message);
                }
            } catch (InterruptedException exc) {
                System.out.println("Message producer interrupted: exiting.");
            }
        }
    }

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

La meilleure façon de procéder consiste à utiliser Task dans JavaFx. C'est de loin la meilleure technique que j'ai rencontrée pour mettre à jour les contrôles d'interface utilisateur dans JavaFx.

Task task = new Task<Void>() {
    @Override public Void run() {
        static final int max = 1000000;
        for (int i=1; i<=max; i++) {
            updateProgress(i, max);
        }
        return null;
    }
};
ProgressBar bar = new ProgressBar();
bar.progressProperty().bind(task.progressProperty());
new Thread(task).start();
5
zIronManBox