从 JavaFX 中的不同线程更新 UI

Posted

技术标签:

【中文标题】从 JavaFX 中的不同线程更新 UI【英文标题】:Updating UI from different threads in JavaFX 【发布时间】:2014-05-11 10:19:01 【问题描述】:

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

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

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

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

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

Logger entries translated to the UI stops being updated with time

Multithreading in JavaFX hangs the UI

但我的问题仍然存在。

总之:我有一个对属性进行更改的后端,我希望这些更改出现在 GUI 上。后端是一种遗传算法,因此它的操作被分解成离散的世代。我希望TextFields 在两代之间至少刷新一次,即使这会延迟下一代。与 GA 运行速度快相比,GUI 响应良好更重要。

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

更新

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

【问题讨论】:

相关问题:Most efficient way to log messages to JavaFX TextArea via threads 【参考方案1】:

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

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

避免 FX 应用程序线程泛滥的技巧是使用 Atomic 变量来存储您感兴趣的值。在 Platform.runLater 方法中,检索它并将其设置为标记值。在您的后台线程中,更新 Atomic 变量,但仅在将其设置回其标记值时才发出新的 Platform.runLater

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

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

Platform.runLater 中,我检索AtomicInteger 的值并使用它来更新Label,在此过程中将该值设置回-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 中“驱动”后端:即限制后端实现的速度以便查看所有更新,请考虑使用 AnimationTimerAnimationTimer 有一个 handle(...),每帧渲染调用一次。因此,您可以阻塞后端实现(例如通过使用阻塞队列)并在每次调用句柄方法时释放它一次。在 FX 应用程序线程上调用 handle(...) 方法。

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

【讨论】:

我认为 API 文档中提到的合并更复杂,我想我应该先看看哈哈。无论如何,这种方法可以防止 GUI 冻结,这很好,但是就像我在上一段中所说的那样,我还想要一种强制后端等待 GUI 更新的方法,有什么想法吗?我之所以想要这个是因为我有一个TextArea作为控制台,但是打印到它上面是相当耗时的,而且随意跳过打印是没有好处的。就像我说的 GA 性能是次要的,如果系统受到TextArea 控制台的瓶颈,那也没关系。 更新了一个从 UI 限制后端的示例。可能还有其他方法可以做到这一点。 感谢节流示例,我没有想到要使用这样的 JFX 计时器。 put() 期间可能发生的异常使得与 GA 一起使用有点棘手,所以我决定现在坚持使用布尔变量。感谢您的帮助! BlockingQueue.put(...) 不会抛出异常,除非你中断线程。 这个解决方案对我很有帮助。理想情况下,这正是我需要的东西,并且节省了我大量研究文档的时间。非常感谢!【参考方案2】:

执行此操作的最佳方法是在 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();

【讨论】:

正如我在问题中所述,我使用的是Task。一年多前解决的问题是我有多个值可以与主线程进行通信。 Task 提供了一些线程安全的更新方法,例如 updateProgress,但我追求的是一种可扩展的解决方案,不受进度、消息、值和标题更新的限制。 现在支持JDK8中的消息、标题和值

以上是关于从 JavaFX 中的不同线程更新 UI的主要内容,如果未能解决你的问题,请参考以下文章

JavaFX 中的多线程会挂起 UI

JavaFX UI 在事件侦听器中的 JavaFX 应用程序线程中冻结,但可与 Platform.runLater 一起使用

“调用线程无法访问此对象,因为不同的线程拥有它”从 WPF 中的不同线程更新 UI 控件时出现错误

JavaFx监听serversocket并根据输入更新UI遇到了问题

当 Cocoa 应用程序中的主线程被阻塞时,UI 不会更新

在 Java FX 工作线程中不断更新 UI