Подтвердить что ты не робот

Обновление пользовательского интерфейса из разных потоков в JavaFX

Я разрабатываю приложение с несколькими объектами TextField, которые необходимо обновить, чтобы отражать изменения связанных свойств back-end. TextField не редактируются, только исходный код может изменять их содержимое.

Как я понимаю, правильный способ - запустить тяжелое вычисление в отдельном потоке, чтобы не блокировать пользовательский интерфейс. Я сделал это с помощью javafx.concurrent.Task и передал одно значение обратно в поток JavaFX с помощью updateMessage(), который работал хорошо. Тем не менее, мне нужно больше, чем одно значение, которое нужно обновить, так как back-end делает хруст.

Так как базовые значения хранятся как свойства JavaFX, я попробовал просто привязать их к textProperty каждого элемента GUI и позволить привязкам выполнять работу. Однако это не работает; после запуска в течение нескольких секунд, TextField перестанет обновляться, даже если задняя задача все еще запущена. Никаких исключений не возникает.

Я также попытался использовать Platform.runLater() для активного обновления TextField вместо привязки. Проблема здесь в том, что задачи runLater() планируются быстрее, чем платформа может их запускать, поэтому графический интерфейс становится вялым и требует времени, чтобы "догнать" даже после завершения конечной задачи.

Я нашел здесь несколько вопросов:

Записи регистратора, переведенные на пользовательский интерфейс, перестают обновляться со временем

Многопоточность в JavaFX висит в пользовательском интерфейсе

но моя проблема сохраняется.

Вкратце: у меня есть фоновый ввод изменений в свойствах, и я хочу, чтобы эти изменения появлялись в графическом интерфейсе. Back-end - это генетический алгоритм, поэтому его работа разбивается на дискретные поколения. Я бы хотел, чтобы TextField обновлялся хотя бы один раз между поколениями, даже если это задерживает следующее поколение. Более важно, чтобы GUI реагировал хорошо, чем GA быстро работает.

Я могу опубликовать несколько примеров кода, если я не сделал проблему ясной.

UPDATE

Мне удалось сделать это после предложения James_D. Чтобы решить проблему, когда серверу приходилось ждать, пока консоль будет печатать, я применил буферизованную консоль. Он сохраняет строки для печати в StringBuffer и фактически добавляет их к TextArea при вызове метода flush(). Я использовал AtomicBoolean, чтобы предотвратить следующее поколение, пока поток не будет завершен, как это сделано с помощью Platform.runLater() runnable. Также обратите внимание, что это решение невероятно медленно.

4b9b3361

Ответ 1

Не уверен, что я полностью понимаю, но я думаю, что это может помочь.

Использование Platform.runLater(...) является подходящим подходом для этого.

Трюк, чтобы избежать наводнения потока приложений FX, - это использовать переменную Atomic для хранения интересующего вас значения. В методе Platform.runLater(...) извлеките его и установите для него значение сторожевой. Из фонового потока обновите Atomic-переменную, но выпустите новую платформу Platform.runLater(...), если она вернулась к ее значению.

Я понял это, посмотрев исходный код для задачи. Посмотрите, как реализуется метод updateMessage (..) (строка 1131 на момент написания).

Вот пример, который использует ту же технику. У этого просто есть (занятый) поток фона, который считается настолько быстрым, насколько это возможно, обновляя IntegerProperty. Наблюдатель наблюдает за этим свойством и обновляет AtomicInteger с новым значением. Если текущее значение AtomicInteger равно -1, оно планирует Platform.runLater().

В Platform.runLater я извлекаю значение AtomicInteger и использую его для обновления Label, устанавливая значение в -1 в процессе. Это означает, что я готов к другому обновлению пользовательского интерфейса.

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

Если вы действительно хотите "управлять" задним концом с пользовательского интерфейса: это уменьшает скорость реализации бэкэнд, поэтому вы видите все обновления, подумайте об использовании AnimationTimer. AnimationTimer имеет handle(...), который вызывается один раз для каждого кадра. Таким образом, вы можете заблокировать реализацию на заднем плане (например, используя блокирующую очередь) и освободить ее один раз за вызов метода дескриптора. Метод handle(...) вызывается в потоке приложения FX.

Метод handle(...) принимает параметр, который является меткой времени (в наносекундах), поэтому вы можете использовать это, чтобы замедлять обновления, если один раз для каждого кадра слишком быстро.

Например:

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

Ответ 2

Лучший способ выполнить это - использование Task в JavaFx. Это, безусловно, лучший метод, с которым я столкнулся, чтобы обновить элементы управления пользовательским интерфейсом в 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();