在JavaFX中从不同的线程更新用户界面

42
我正在开发一个应用程序,其中有几个 TextField 对象需要更新以反映相关后端属性的更改。这些 TextField 不可编辑,只有后端可以更改它们的内容。
据我了解,正确的做法是在单独的线程上运行繁重的计算,以避免阻塞用户界面。我使用 javafx.concurrent.Task 完成了此操作,并使用 updateMessage() 与 JavaFX 线程通信,成功地返回了单个值。但是,当后端进行计算时,我需要更新多个值。
由于后端值存储为 JavaFX 属性,所以我尝试将它们简单绑定到每个 GUI 元素的 textProperty 上并让绑定完成这项工作。然而,这不起作用;在运行一段时间后,TextField 停止更新,即使后端任务仍在运行,也没有引发任何异常。
我还尝试使用 Platform.runLater() 主动更新 TextField 而不是绑定。这里的问题是 runLater() 任务比平台能够运行它们的速度更快,因此 GUI 变得缓慢,即使后端任务完成后,GUI 也需要时间来“赶上”。
我在这里找到了一些问题: Logger entries translated to the UI stops being updated with time Multithreading in JavaFX hangs the UI 但我的问题仍然存在。总结:我有一个后端正在更改属性,我希望这些更改能在GUI上显示出来。后端是遗传算法,因此其操作被划分为离散的代。我希望即使这会延迟下一代,TextField至少在代之间刷新一次。重要的是GUI的响应要好,而不是GA运行快。
如果我没有清楚表达问题,我可以发几个代码示例。
更新:
我成功地按照James_D的建议做到了。为了解决后端必须等待控制台打印的问题,我实现了一种缓冲控制台。它将要打印的字符串存储在StringBuffer中,并在调用flush()方法时将其实际添加到TextArea中。我使用了AtomicBoolean防止在flush完成之前进行下一代,因为这是通过Platform.runLater()可运行的。请注意,这个解决方案非常慢。

2个回答

46

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

使用Platform.runLater(...)是解决这个问题的适当方法。

避免淹没FX应用程序线程的诀窍是使用原子变量存储您感兴趣的值。在Platform.runLater方法中,检索它并将其设置为哨兵值。从后台线程中,更新原子变量,但仅在它被设置回其哨兵值时才发出新的Platform.runLater

我通过查看源代码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(...)方法,每帧渲染调用一次。因此,您可以阻止后端实现(例如使用阻塞队列),并在每次调用处理方法时释放它。 handle(...)方法在FX应用程序线程上调用。 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控制台限制,那也没关系。 - eddy
1
更新了一个从UI限制后端的示例。可能有其他实现方式。 - James_D
1
谢谢提供节流示例,我没有想到可以像那样使用JFX定时器。put()期间可能出现的异常使得它在与GA一起使用时有点棘手,因此我决定现在坚持使用布尔变量。感谢您的帮助! - eddy
1
除非您中断线程,否则BlockingQueue.put(...)不会抛出异常。 - James_D
这个解决方案对我非常有帮助。它完全符合我所需,让我省去了大量查阅文档的时间。非常感谢! - konsolebox
我发现AnimationTimer实际上会减慢GUI的速度。是否有替代方案来获取消息,例如在应用程序线程中使用钩子?而且我不喜欢反复使用runLater()。在其中使用队列的本质似乎已经失去了,并且我对"runLater()"对象的执行顺序也不确定,因为它参考了其实现的实际和严格技术规范,而不仅仅是当前的规范。 - konsolebox

7

最佳的方法是使用JavaFx中的Task。这是我发现的迄今为止更新JavaFx UI控件的最佳技术。

Task task = new Task<Void>() {
    @Override public Void call() {
        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();

8
正如我在问题中所述,我正在使用Task。一年前就解决了的问题是我需要向主线程传回多个值。 Task提供了一些线程安全的更新方法,例如updateProgress,但我需要一个可扩展的解决方案,不仅仅局限于进度、消息、值和标题的更新。 - eddy
1
它现在支持JDK8中的消息、标题和值。 - kasvith

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接