JavaFX ChangeListener 并不总是有效

JavaFX ChangeListener not always working

我有一个 JavaFX 应用程序,其中有一个并发任务。 虽然任务是 运行,但我想将来自 updateMessage() 的消息附加到 TextArea

因为绑定不会将新文本附加到 TextArea,所以我使用了 ChangeListener

worker.messageProperty().addListener((observable, oldValue, newValue) -> {
    ta_Statusbereich.appendText("\n" + newValue);
});

这是有效的,但不是每次更改都有效。 我用 System.out.println() 检查了它,并在任务中从 1 数到 300

for (Integer i = 1; i <= 300; i++) {
    updateMessage(i.toString());
    System.out.println(i.toString());
}

任务中的这个 println() 给了我想要的 1,2,3,4,5,6,7,8 等等,但是我的 TextArea 显示 1,4,5,8,9 然后我在 ChangeListener 中添加了一个 println 并得到相同的结果,1,4,5,8,9 (结果是随机的,并不总是 1,4,5...)

为什么?是否有其他方法可以将消息文本附加到 TextAres,也许使用绑定?

message 属性 被设计为一个 属性 ,其中包含 task 的 "current message" :即目标用例类似于状态消息。在此用例中,仅在 属性 中存储很短时间的消息是否从未被拦截并不重要。事实上,documentation for updateMessage() 声明:

Calls to updateMessage are coalesced and run later on the FX application thread, so calls to updateMessage, even from the FX Application thread, may not necessarily result in immediate updates to this property, and intermediate message values may be coalesced to save on event notifications.

(我的重点)。因此,简而言之,如果传递给 updateMessage(...) 的某些值很快被另一个值取代,则它们实际上可能永远不会被设置为 messageProperty 的值。通常,每次将一帧渲染到屏幕时(每秒 60 次或更少),您可以期望只观察到一个值。如果您有一个用例,其中您想要观察每个值很重要,那么您需要使用另一种机制。

一个非常天真的实现会只使用 Platform.runLater(...) 并直接更新文本区域。我不推荐这种实现方式,因为您可能会因过多的调用而淹没 FX 应用程序线程(updateMessage(...) 合并调用的确切原因),从而导致 UI 无响应。然而,这个实现看起来像:

for (int i = 1 ; i <= 300; i++) {
    String value = "\n" + i ;
    Platform.runLater(() -> ta_Statusbereich.appendText(value));
}

另一种选择是使每个操作成为一个单独的任务,并在某个执行器中并行执行它们。附加到每个任务的 onSucceeded 处理程序中的文本区域。在这个实现中,结果的顺序不是预先确定的,所以如果顺序很重要,这不是一个合适的机制:

final int numThreads = 8 ;
Executor exec = Executors.newFixedThreadPool(numThreads, runnable -> {
    Thread t = Executors.defaultThreadFactory().newThread(runnable);
    t.setDaemon(true);
    return t ;
});

// ...

for (int i = 1; i <= 300; i++) {
    int value = i ;
    Task<String> task = new Task<String>() {
        @Override
        public String call() {
            // in real life, do real work here...
            return "\n" + value ; // value to be processed in onSucceeded
        }
    };
    task.setOnSucceeded(e -> ta_Statusbereich.appendText(task.getValue()));
    exec.execute(task);
}

如果你想从一个任务中完成所有这些,并控制顺序,那么你可以将所有消息放入一个BlockingQueue,从阻塞队列中取出消息并将它们放入文本区域在 FX 应用程序线程上。为确保您不会用过多的调用淹没 FX 应用程序线程,您应该在每帧渲染到屏幕时使用队列中的消息不超过一次。您可以为此目的使用 AnimationTimer:它的 handle 方法保证每帧渲染调用一次。这看起来像:

BlockingQueue<String> messageQueue = new LinkedBlockingQueue<>();

Task<Void> task = new Task<Void>() {
    @Override
    public Void call() throws Exception {
        final int numMessages = 300 ;
        Platform.runLater(() -> new MessageConsumer(messageQueue, ta_Statusbereich, numMessages).start());
        for (int i = 1; i <= numMessages; i++) {
            // do real work...
            messageQueue.put(Integer.toString(i));
        }
        return null ;
    }
};
new Thread(task).start(); // or submit to an executor...

// ...

public class MessageConsumer extends AnimationTimer {
    private final BlockingQueue<String> messageQueue ;
    private final TextArea textArea ;
    private final numMessages ;
    private int messagesReceived = 0 ;
    public MessageConsumer(BlockingQueue<String> messageQueue, TextArea textArea, int numMessages) {
        this.messageQueue = messageQueue ;
        this.textArea = textArea ;
        this.numMessages = numMessages ;
    }
    @Override
    public void handle(long now) {
        List<String> messages = new ArrayList<>();
        messagesReceived += messageQueue.drainTo(messages);
        messages.forEach(msg -> textArea.appendText("\n"+msg));
        if (messagesReceived >= numMessages) {
            stop();
        }
    }
}

替代解决方案,但我认为这是不好的做法。

可以在updateMessage()

后使用sleep方法
void showUpdateMessage(String msg) throws InterruptedException {
    updateMessage(msg);
    TimeUnit.MILLISECONDS.sleep(100);
}

慎重使用!