Aktualizacja interfejsu użytkownika z różnych wątków w JavaFX

Rozwijam aplikację z kilkoma obiektami TextField, które wymagają aktualizacji, aby odzwierciedlić zmiany w powiązanych właściwościach zaplecza. TextFields nie są edytowalne, tylko back-end może zmienić ich zawartość.

Jak rozumiem, poprawnym sposobem jest uruchomienie ciężkich obliczeń na osobnym wątku, aby nie blokować interfejsu użytkownika. Zrobiłem to używając javafx.concurrent.Task i przekazałem pojedynczą wartość z powrotem do wątku JavaFX używając updateMessage(), co działało dobrze. Jednak potrzebuję więcej niż jednego wartość, która ma być aktualizowana, gdy back-end robi swoje crunching.

Ponieważ wartości zaplecza są przechowywane jako właściwości JavaFX, próbowałem po prostu powiązać je z textProperty każdego elementu GUI i pozwolić powiązaniom wykonać pracę. To jednak nie działa; po uruchomieniu przez kilka chwil, TextField s przestają aktualizować, mimo że zadanie back-end jest nadal uruchomione. Nie ma wyjątków.

Próbowałem również użyć Platform.runLater() do aktywnej aktualizacji TextField s zamiast wiązania. Problem polega na tym, że runLater() zadania są zaplanowane szybciej niż platforma może je uruchomić, a więc GUI staje się powolny i potrzebuje czasu, aby "nadrobić zaległości"nawet po zakończeniu zadania back-end.

Znalazłem tu kilka pytań:

Wpisy Loggera przetłumaczone na interfejs użytkownika przestają być aktualizowane z czasem

Wielowątkowość w JavaFX zawiesza interfejs

Ale mój problem nadal trwa.

Podsumowując: mam back-end dokonujący zmian w właściwościach i chcę te zmiany pojawiające się w GUI. Back-end jest algorytmem genetycznym, więc jego działanie jest podzielone na dyskretne generacje. Chciałbym, aby TextFields odświeżały się przynajmniej raz między pokoleniami, nawet jeśli opóźni to następne pokolenie. Ważniejsze jest to, że GUI reaguje dobrze niż to, że GA działa szybko.

Mogę opublikować kilka przykładów kodu, jeśli nie wyjaśniłem problemu.

UPDATE

Udało mi się to zrobić po James_D ' s sugestia. Aby rozwiązać problem konieczności oczekiwania na wydruk konsoli, zaimplementowałem buforowaną konsolę. Przechowuje ciągi znaków do wydrukowania w StringBuffer i faktycznie dołącza je do TextArea, gdy wywołana jest metoda flush(). Użyłem AtomicBoolean, aby zapobiec pojawieniu się nowej generacji, dopóki kolor nie zostanie ukończony, jak to robi Platform.runLater() runnable. Zauważ również, że to rozwiązanie jest niewiarygodnie powolne.

Author: Community, 2014-04-01

2 answers

Nie wiem, czy w pełni rozumiem, ale myślę, że to może pomóc.

Korzystanie Z Platformy.runLater(...) jest odpowiednim podejściem w tym zakresie.

Sztuką unikania zalewania wątku aplikacji FX jest użycie zmiennej atomowej do przechowywania interesującej Cię wartości. Na peronie.runLater(...) method, retrieve it and set it to a sentinel value. W wątku w tle zaktualizuj zmienną Atomic, ale wydaj tylko nową platformę.runLater(...) jeśli został ustawiony z powrotem na jego wartość wartownicza.

Rozgryzłem to patrząc na kod źródłowy zadania. Zobacz, jak updateMessage(..) metoda (linia 1131 w momencie pisania) jest zaimplementowana.

Oto przykład, który używa tej samej techniki. To po prostu ma (zajęty) wątek tła, który liczy się tak szybko, jak to możliwe, aktualizując IntegerProperty. Obserwator obserwuje tę właściwość i aktualizuje AtomicInteger z nową wartością. Jeśli bieżąca wartość AtomicInteger wynosi -1, to rozkłada Peron.runLater ().

Na peronie.runLater, pobieram wartość AtomicInteger i używam jej do aktualizacji etykiety, ustawiając wartość z powrotem na -1 w procesie. Oznacza to, że jestem gotowy na kolejną aktualizację interfejsu użytkownika.
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);
      }
    }
  }
}

Jeśli naprawdę chcesz "napędzać" back-end z interfejsu użytkownika, czyli zmniejszyć prędkość implementacji backendu, aby zobaczyć wszystkie aktualizacje, rozważ użycie AnimationTimer. Funkcja AnimationTimer ma funkcję handle(...), która jest wywoływana raz na renderowanie klatki. Żebyś mógł zablokować implementacja back-end (na przykład poprzez użycie kolejki blokującej) i zwalnia ją raz na wywołanie metody handle. Metoda handle(...) jest wywoływana w wątku aplikacji FX.

Metoda handle(...) pobiera parametr, który jest znacznikiem czasu (w nanosekundach), więc można go użyć do dalszego spowolnienia aktualizacji, jeśli raz na klatkę jest zbyt szybki.

Na przykład:

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);
    }
}
 29
Author: James_D,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-04-01 10:25:04

Najlepszym sposobem na to jest użycie Task w JavaFx. Jest to zdecydowanie najlepsza technika, z jaką spotkałem się, aby zaktualizować kontrolki interfejsu w 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
Author: zIronManBox,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-06-23 11:20:16