JavaFX的ChangeListener并非总是有效

6
我有一个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...)
为什么会这样?还有其他将消息文本附加到TextArea的方法吗?也许可以使用bind?

为什么?在文档中有详细解释(http://docs.oracle.com/javase/8/javafx/api/javafx/concurrent/Task.html#updateMessage-java.lang.String-)。你实际想做什么?(我猜你不只是要在文本区域中显示1-300的值,因为你不需要一个任务来完成这件事。) - James_D
该任务在网络上重命名所选文件夹中的文件,我尝试在文本区域中列出所有已处理的文件。就像一个打印已复制文件的设置。 - Garog
2个回答

15
message 属性被设计成一个保存“当前消息”的属性,用于 task:即目标用例类似于状态消息。在这种情况下,如果仅在该属性中存储了一条消息并且没有被截获,则不会产生影响。实际上,updateMessage() 的文档 表明:

对 updateMessage 的调用将被合并并稍后在 FX 应用程序线程上运行,因此,即使从 FX 应用程序线程调用 updateMessage,也可能不一定会立即更新此属性,而是可以合并中间的消息值以节省事件通知。

(我强调)。因此,简言之,如果某些传递给 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();
        }
    }
}

2
这样的回答正是我在这个平台上提问的原因!哇,太棒了,非常感谢。对于像我这样只有一些浅薄知识的人来说,它包含了如此多的信息和知识...当你不得不从API描述中挖掘这些东西时...天哪...我尝试了runlater,但在本地计算机上重命名文件而不是通过网络时,确实得到了你预测的结果。顺序也很重要,因为我要计算文件数量并将其打印到屏幕上。所以最终我必须选择第三个编号。 :) - Garog
这是一个很棒的回答!恰好帮助了我解决了当前的情况!选项3是最佳选择! - WillZ
1
是的,这是一个很好的答案。对我来说,选项3是最好的选择。然而,对于我来说,使用messages.forEach(msg -> textArea.appendText("\"+msg));有点慢。相反,我在将它们附加到文本区域之前连接了所有行,即textArea.appendText(String.join("", messages))。我不需要添加换行符,因为我的消息已经有了换行符。 - Thylossus
2
@Thylossus 对于一些 UI 任务,ListView 可能比 TextArea 更合适,因为 ListView 是一个虚拟化控件。请参见 Most efficient way to log messages to JavaFX ... via threads 获取有关基于 ListView 的方法的更多信息。ListView 可以帮助解决这种情况的原因是它只需要呈现可见行,而不是文本区域中的所有文本。 - jewelsea

-2

另一种解决方案,但我认为这是不好的做法。

您可以在updateMessage()之后使用sleep方法。

void showUpdateMessage(String msg) throws InterruptedException {
    updateMessage(msg);
    TimeUnit.MILLISECONDS.sleep(100);
}

小心使用!


如果你已经知道这是不好的做法,为什么还要在这里发布呢?这被认为是有充分理由的不良做法。 - mipa
但是,在某些情况下,它可以正常运行。只要确保不会发生InterruptedException异常,那么您可以使用此解决方案。 - Angga Arya S
不,那是完全错误的 - 你绝不能让UI线程睡眠,没有例外! - kleopatra
好的...但是这样我们又回到了极其混乱的状态(至少在未记录的情况下,必须从后台线程调用,这就是任务本身)。为什么要将“混合”隐藏在调用者之外(而不是在调用方法本身中完成)? - kleopatra

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