从 JavaFX 中的不同线程更新 UI

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/22772379/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-13 17:48:00  来源:igfitidea点击:

Updating UI from different threads in JavaFX

javamultithreadinguser-interfacejavafx-2

提问by eddy

I am developing an application with several TextFieldobjects that need to be updated to reflect changes in associated back-end properties. The TextFields are not editable, only the back-end may change their content.

我正在开发一个包含多个TextField对象的应用程序,这些对象需要更新以反映相关后端属性的变化。该TextFields为不可编辑,只有后端可能会改变他们的内容。

As I understand, the correct way about this is to run the heavy computation on a separate thread so as not to block the UI. I did this using javafx.concurrent.Taskand communicated a single value back to the JavaFX thread using updateMessage(), which worked well. However, I need more than one value to be updated as the back-end does its crunching.

据我了解,正确的做法是在单独的线程上运行繁重的计算,以免阻塞 UI。我使用 做到了这一点javafx.concurrent.Task,并使用将单个值传送回 JavaFX 线程updateMessage(),效果很好。但是,当后端进行处理时,我需要更新多个值。

Since the back-end values are stored as JavaFX properties, I tried simply binding them to the textPropertyof each GUI element and let the bindings do the work. This doesn't work, however; after running for a few moments, the TextFields stop updating even though the back-end task is still running. No exceptions are raised.

由于后端值存储为 JavaFX 属性,我尝试简单地将它们绑定到textProperty每个 GUI 元素的 的 ,并让绑定完成工作。然而,这不起作用;运行片刻后,TextField即使后端任务仍在运行,s 也会停止更新。没有异常被提出。

I also tried using Platform.runLater()to actively update the TextFields rather than binding. The issue here is that the runLater()tasks are scheduled faster than the platform can run them, and so the GUI becomes sluggish and needs to time to "catch up" even after the back-end task is finished.

我还尝试使用Platform.runLater()主动更新TextFields 而不是绑定。这里的问题是runLater()任务的调度速度比平台可以运行它们的速度快,因此 GUI 变得迟钝,即使在后端任务完成后也需要时间“赶上”。

I found a few questions on here:

我在这里发现了几个问题:

Logger entries translated to the UI stops being updated with time

转换为 UI 的记录器条目不再随时间更新

Multithreading in JavaFX hangs the UI

JavaFX 中的多线程挂起 UI

but my issue persists.

但我的问题仍然存在。

In summary: I have a back-end making changes to properties, and I want those changes to appear on the GUI. The back-end is a genetic algorithm, so its operation is broken down into discrete generations. What I would like is for the TextFields to refresh at least once in between generations, even if this delays the next generation. It is more important that the GUI responds well than that the GA runs fast.

总结:我有一个对属性进行更改的后端,我希望这些更改显示在 GUI 上。后端是一个遗传算法,所以它的操作被分解成离散的代。我希望TextFields 在几代之间至少刷新一次,即使这会延迟下一代。GUI 响应良好比 GA 运行快更重要。

I can post a few code examples if I haven't made the issue clear.

如果我没有把问题说清楚,我可以发布一些代码示例。

UPDATE

更新

I managed to do it following James_D's suggestion. To solve the issue of the back-end having to wait for the console to print, I implemented a buffered console of sorts. It stores the strings to print in a StringBufferand actually appends them to the TextAreawhen a flush()method is called. I used an AtomicBoolean to prevent the next generation from happening until the flush is complete, as it is done by a Platform.runLater()runnable. Also note that this solution is incrediblyslow.

我按照 James_D 的建议设法做到了。为了解决后端必须等待控制台打印的问题,我实现了一个缓冲的控制台。它将要打印的字符串存储在 a 中,StringBufferTextAreaflush()调用方法时实际将它们附加到。我使用 AtomicBoolean 来防止下一代发生,直到刷新完成,因为它是由Platform.runLater()可运行的完成的。另请注意,此解决方案非常慢。

采纳答案by James_D

Not sure if I completely understand, but I think this may help.

不确定我是否完全理解,但我认为这可能会有所帮助。

Using Platform.runLater(...) is an appropriate approach for this.

使用 Platform.runLater(...) 是一个合适的方法。

The trick to avoiding flooding the FX Application Thread is to use an Atomic variable to store the value you're interested in. In the Platform.runLater(...) method, retrieve it and set it to a sentinel value. From your background thread, update the Atomic variable, but only issue a new Platform.runLater(...) if it's been set back to its sentinel value.

避免淹没 FX 应用程序线程的技巧是使用原子变量来存储您感兴趣的值。在 Platform.runLater(...) 方法中,检索它并将其设置为哨兵值。从您的后台线程,更新 Atomic 变量,但如果它已被设置回其哨兵值,则仅发出新的 Platform.runLater(...)。

I figured this out by looking at the source code for Task. Have a look at how the updateMessage(..) method (line 1131 at the time of writing) is implemented.

我通过查看Task源代码发现了这一点。查看 updateMessage(..) 方法(撰写本文时的第 1131 行)是如何实现的。

Here's an example which uses the same technique. This just has a (busy) background thread which counts as fast as it can, updating an IntegerProperty. An observer watches that property and updates an AtomicInteger with the new value. If the current value of the AtomicInteger is -1, it schedules a Platform.runLater().

这是一个使用相同技术的示例。这只是有一个(繁忙的)后台线程,它尽可能快地计数,更新 IntegerProperty。观察者监视该属性并使用新值更新 AtomicInteger。如果 AtomicInteger 的当前值为 -1,则它会调度 Platform.runLater()。

In the Platform.runLater, I retrieve the value of the AtomicInteger and use it to update a Label, setting the value back to -1 in the process. This signals that I am ready for another UI update.

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

If you really want to "drive" the back end from the UI: that is throttle the speed of the backend implementation so you see all updates, consider using an AnimationTimer. An AnimationTimerhas a handle(...)which is called once per frame render. So you could block the back-end implementation (for example by using a blocking queue) and release it once per invocation of the handle method. The handle(...)method is invoked on the FX Application Thread.

如果您真的想从用户界面“驱动”后端:即限制后端实现的速度以便您看到所有更新,请考虑使用AnimationTimer. AnAnimationTimer有一个handle(...),每帧渲染调用一次。因此,您可以阻塞后端实现(例如,通过使用阻塞队列)并在每次调用 handle 方法时将其释放一次。该handle(...)方法在 FX 应用程序线程上调用。

The handle(...)method takes a parameter which is a timestamp (in nanoseconds), so you can use that to slow the updates further, if once per frame is too fast.

handle(...)方法采用一个时间戳参数(以纳秒为单位),因此如果每帧一次太快,您可以使用它进一步减慢更新速度。

For example:

例如:

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

回答by zIronManBox

The best way to performing this is by usage of Taskin JavaFx. This is be by far the best technique I've come across to update UI Controls in JavaFx.

执行此操作的最佳方法是Task在 JavaFx 中使用。这是迄今为止我遇到的在 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();