从JavaFX中的不同线程更新UI [英] Updating UI from different threads in JavaFX

查看:113
本文介绍了从JavaFX中的不同线程更新UI的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在开发一个带有几个 TextField 对象的应用程序,这些对象需要更新才能反映相关后端属性的变化。 TextField 不可编辑,只有后端可能会更改其内容。

I am developing an application with several TextField objects 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.

据我了解,关于这个的正确方法是在单独的线程上运行繁重的计算,以便不阻止UI。我使用 javafx.concurrent.Task 执行此操作,并使用 updateMessage()将单个值传回JavaFX线程,效果很好。但是,我需要更新多个值,因为后端会进行其处理。

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.Task and 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.

由于后端值存储为JavaFX属性,我只是尝试了将它们绑定到每个GUI元素的 textProperty ,让绑定完成工作。但是,这不起作用;运行一会儿后,即使后端任务仍在运行, TextField 也会停止更新。没有例外。

Since the back-end values are stored as JavaFX properties, I tried simply binding them to the textProperty of 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.

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

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.

我在这里找到了几个问题:

I found a few questions on here:

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

JavaFX中的多线程挂起用户界面

但我的问题仍然存在。

总结:我有一个后端更改属性,我希望这些更改出现在GUI上。后端是遗传算法,因此其操作被分解为离散的世代。我想要的是 TextField 在代之间至少刷新一次,即使这会延迟下一代。更重要的是,GUI的响应速度要快于GA运行速度。

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.

如果我没有明确问题,我可以发布一些代码示例。

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

更新

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

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 StringBuffer and actually appends them to the TextArea when 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 incredibly slow.

推荐答案

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

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

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

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

避免泛滥FX应用程序线程的技巧是使用原子变量来存储您感兴趣的值。在Platform.runLater(...)方法中,检索它并将其设置为sentinel值。在后台线程中,更新Atomic变量,但只有在它被设置回其sentinel值时才发出新的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.

我通过查看任务的源代码。看看如何实现updateMessage(..)方法(编写本文时的第1131行)。

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.

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

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().

在Platform.runLater中,我检索AtomicInteger的值并使用它更新一个Label,在过程中将值设置回-1。这表示我已准备好进行另一次UI更新。

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.

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 有一个句柄(...),每帧渲染一次。因此,您可以阻止后端实现(例如,通过使用阻塞队列),并在每次调用handle方法时释放一次。在FX应用程序线程上调用句柄(...)方法。

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 AnimationTimer has 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.

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

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.

例如:

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

这篇关于从JavaFX中的不同线程更新UI的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆