web-dev-qa-db-ja.com

JavaFXの異なるスレッドからUIを更新する

関連するバックエンドプロパティの変更を反映するために更新する必要があるいくつかのTextFieldオブジェクトを含むアプリケーションを開発しています。 TextFieldsは編集できません。バックエンドのみがコンテンツを変更できます。

私が理解しているように、これについての正しい方法は、UIをブロックしないように、重いスレッドを別のスレッドで実行することです。 _javafx.concurrent.Task_を使用してこれを行い、updateMessage()を使用して単一の値をJavaFXスレッドに返しました。ただし、バックエンドが処理を実行するため、複数の値を更新する必要があります。

バックエンドの値はJavaFXプロパティとして保存されるため、単純に各GUI要素のtextPropertyにバインドして、バインディングに機能を任せました。ただし、これは機能しません。しばらく実行した後、バックエンドタスクがまだ実行されていても、TextFieldsは更新を停止します。例外は発生しません。

また、Platform.runLater()を使用して、バインディングではなくTextFieldsをアクティブに更新しようとしました。ここでの問題は、runLater()タスクがプラットフォームで実行できるよりも早くスケジュールされるため、GUIが遅くなり、バックエンドタスクが完了した後でも「追いつく」必要があることです。

私はここでいくつかの質問を見つけました:

Iに変換されたロガーエントリは時間とともに更新されなくなります

JavaFXのマルチスレッドによりUIがハングする

しかし、私の問題は解決しません。

要約すると、プロパティに変更を加えるバックエンドがあり、それらの変更をGUIに表示する必要があります。バックエンドは遺伝的アルゴリズムであるため、その動作は個別の世代に分けられます。 TextFieldsが世代間で少なくとも1回更新されるようにしたいのですが、これは次の世代を遅らせます。 GAが高速で実行されることよりも、GUIが適切に応答することが重要です。

問題を明確にしていない場合は、いくつかのコード例を投稿できます。

[〜#〜] update [〜#〜]

私はJames_Dの提案に従ってそれをなんとかしました。コンソールが印刷されるのを待たなければならないバックエンドの問題を解決するために、バッファ付きのコンソールを実装しました。印刷する文字列をStringBufferに保存し、flush()メソッドが呼び出されたときに実際にそれらをTextAreaに追加します。 AtomicBooleanを使用して、Platform.runLater() runnableによって行われるように、フラッシュが完了するまで次の世代が発生しないようにしました。また、このソリューションは信じられないほど遅いことに注意してください。

31
eddy_hunter

私が完全に理解しているかどうかはわかりませんが、これが役立つと思います。

Platform.runLater(...)を使用することは、このための適切なアプローチです。

FXアプリケーションスレッドのフラッディングを回避するためのトリックは、Atomic変数を使用して、目的の値を格納することです。Platform.runLater(...)メソッドで、それを取得し、センチネル値に設定します。バックグラウンドスレッドからAtomic変数を更新しますが、新しいPlatform.runLater(...)を発行するのは、それがセンチネル値に戻された場合のみです。

タスクのソースコード を見て、これを理解しました。 updateMessage(..)メソッド(執筆時の1131行目)の実装方法をご覧ください。

同じ手法を使用した例を次に示します。これには、できるだけ速くカウントする(ビジー)バックグラウンドスレッドがあり、IntegerPropertyを更新します。オブザーバーはそのプロパティを監視し、AtomicIntegerを新しい値で更新します。 AtomicIntegerの現在の値が-1の場合、Platform.runLater()をスケジュールします。

Platform.runLaterで、AtomicIntegerの値を取得し、それを使用してラベルを更新し、プロセスで値を-1に戻します。これは、別のUI更新の準備ができたことを示しています。

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

UIからバックエンドを本当に「駆動」したい場合:つまり、すべての更新を確認できるようにバックエンド実装の速度を調整します。AnimationTimerの使用を検討してください。 AnimationTimerには、フレームレンダリングごとに1回呼び出されるhandle(...)があります。そのため、バックエンド実装をブロックして(たとえば、ブロッキングキューを使用して)、handleメソッドの呼び出しごとに1回解放することができます。 handle(...)メソッドは、FXアプリケーションスレッドで呼び出されます。

handle(...)メソッドは、タイムスタンプ(ナノ秒単位)のパラメーターを受け取るため、これを使用して、フレームごとに1回が速すぎる場合、更新をさらに遅くすることができます。

例えば:

_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

これを実行する最良の方法は、JavaFxでTaskを使用することです。これは、私がJavaFxのUIコントロールを更新するために出会った中で、最高のテクニックです。

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