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

Posted

技术标签:

【中文标题】JavaFX UI 在事件侦听器中的 JavaFX 应用程序线程中冻结,但可与 Platform.runLater 一起使用【英文标题】:JavaFX UI Freezes in JavaFX Application Thread in Event Listener but works with Platform.runLater 【发布时间】:2019-04-10 15:14:30 【问题描述】:

在我的 JavaFX 程序中,我目前的所有代码都在 JavaFX 应用程序线程上运行(包括不一定会更新 UI 的游戏逻辑代码)。我读过你应该在单独的后台线程中运行非 UI 代码,然后使用 Platform.runLaterTasks 在 JavaFX 应用程序线程中相应地更新它,这样 UI 就不会被阻塞,但我仍然感到困惑至于为什么当我在 JavaFX 应用程序线程上调用 myFlowPane.getChildren().remove() 时 UI 会冻结。

以下代码有望提供足够的上下文

hand.getTiles().addListener((ListChangeListener<ObservableTile>) change -> 
    while (change.next()) 
        if (change.wasAdded()) 
            for (Tile t : change.getAddedSubList()) 
                var btn = tileButtonFactory.newTileButton(t);
                fpHand.getChildren().add(change.getFrom(), btn);
            
         else if (change.wasRemoved()) 
            // Player can only remove 1 tile at a time
            Tile tile = change.getRemoved().get(0);
            fpHand.getChildren().removeIf(node -> ((TileButton)node).getTile().equals(tile));
        
    
);

我有一个包含一堆按钮的 FlowPane,我正在收听一个包含一堆图块的 ObservableList。当此图块列表更改时,我想相应地更新 FlowPane,以便任何新图块,我在 FlowPane 中创建一个按钮。同样,当从该图块列表中删除图块时,我希望将包含此图块的按钮从 FlowPane 中删除。

所以根据我在网上找到的信息,事件侦听器在 JavaFX 应用程序线程上运行。这意味着上面的代码在 JavaFX 应用程序线程上运行。当我向我的 FlowPane 添加一个按钮时,UI 确实 冻结/被阻止 fpHand.getChildren().add()。另一方面,当我从 FlowPane 中删除一个按钮时,UI 冻结在 fpHand.getChildren.remove

但是,当我使用 Platform.runLater() 包装 fpHand.getChildren.remove() 调用时,UI 会响应并且不会冻结。为什么是这样?所有代码当前都在 JavaFX 应用程序线程上运行。

另外,我的代码的另一部分也有类似的问题。我有一个包含 128 个单元格的 GridPane(每个单元格都有一个 HBox),我需要更新表格。我有以下代码:

private void updateTable() 
    Platform.runLater(() -> 
        clearTable();
        table.forEach(this::addTilesToTable);
    );

private void clearTable() 
    for (int row = 0; row < gpTable.getRowCount(); row++) 
        for (int col = 0; col < gpTable.getColumnCount(); col++) 
            getCellFromGridPane(row, col).ifPresent(hbox -> hbox.getChildren().clear());
        
    

如果我删除Platform.runLater(),UI 不会完全冻结,但是我之前在现场添加的任何图块,就好像桌子上有一个不可见的图块,因此我无法添加图块在那里,即使它应该是空的。 updateTable() 函数也在 JavaFX 应用程序线程和事件侦听器中调用。如果我删除Platform.runLater(),似乎hbox.getChildren.clear() 没有被正确调用。

【问题讨论】:

要检查是否在 FX 线程上调用了侦听器,请使用 Platform.isFxApplicationThread。监听器(例如InvalidationListenerChangeListenerListChangeListener 等...)被调用无论Thread 进行了更改。也就是说,您是否尝试过调试/分析您的应用程序以查看它卡在哪里?即使是一些简单的打印语句也可能会告诉您是否进入了循环。如果您仍然无法找出问题所在,您需要提供minimal reproducible example;您提供的代码没有提供足够的上下文。 【参考方案1】:

我遇到的问题是,我将setMouseTransparent(true) 用于根窗格,其中包含用于单击和拖动的 Tile 按钮,而不是消耗鼠标事件。

    node.addEventHandler(MouseEvent.MOUSE_PRESSED, e -> 
        root.setMouseTransparent(true);
        dragContext.setMouseX(e.getSceneX());
        dragContext.setMouseY(e.getSceneY());
        dragContext.setInitialTranslateX(node.getTranslateX());
        dragContext.setInitialTranslateY(node.getTranslateY());
        e.consume();
    );

当你释放鼠标时,它会重新设置为 false

    node.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> 
        root.setMouseTransparent(false);
        node.setTranslateX(0);
        node.setTranslateY(0);
        e.consume();
    );

这里的问题是,当我从场景中移除按钮时,它会破坏所有事件处理程序,即使我释放鼠标,它也不会运行此代码。

【讨论】:

以上是关于JavaFX UI 在事件侦听器中的 JavaFX 应用程序线程中冻结,但可与 Platform.runLater 一起使用的主要内容,如果未能解决你的问题,请参考以下文章

如何在对话框中将事件侦听器绑定到JavaFX TextFields。当字段为空时,“确定”按钮将被禁用,反之亦然

JavaFX TextArea 滚动条移动事件

JavaFX窗口拖动

如何使某些JavaFX TreeView节点不可选择?

JavaFX:如何在屏幕上的任何位置检测鼠标/键事件?

JavaFX:从线程更新控制器类中的 UI 元素