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

查看:92
本文介绍了从 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 中的多线程挂起 UI

但我的问题仍然存在.

总而言之:我有一个对属性进行更改的后端,我希望这些更改显示在 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 中,并在调用 flush() 方法时实际将它们附加到 TextArea.我使用 AtomicBoolean 来防止下一代发生,直到刷新完成,因为它是由 Platform.runLater() 可运行的.另请注意,此解决方案慢得难以置信.

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 方法中,检索它并将其设置为哨兵价值.从您的后台线程中,更新 Atomic 变量,但如果它已被设置回其标记值,则仅发出一个新的 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.

我通过查看 Task 的源代码.看看如何实现 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(...),每帧渲染调用一次.因此,您可以阻塞后端实现(例如,通过使用阻塞队列)并在每次调用 handle 方法时将其释放一次.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("
" + 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天全站免登陆