从服务线程更新JavaFX GUI

4
如何在JavaFX服务内安全地更新JavaFX GUI上的小部件?我记得在使用Swing进行开发时,我通常会使用“invoke later”和其他各种Swing工具来确保所有UI更新都在Java事件线程中安全处理。这是一个处理数据报消息的简单服务线程示例。缺少的部分是解析数据报消息和更新对应UI小部件的部分。可以看到,服务类非常简单。 我不确定是否需要使用简单绑定属性(例如消息),或者是否应该将小部件传递给我的StatusListenerService的构造函数(这可能不是最好的选择)。能否给我一个类似的好示例供参考。
public class StatusListenerService extends Service<Void> {
    private final int mPortNum;

    /**
     *
     * @param aPortNum server listen port for inbound status messages
     */
    public StatusListenerService(final int aPortNum) {
        this.mPortNum = aPortNum;
    }

    @Override
    protected Task<Void> createTask() {
        return new Task<Void>() {
            @Override
            protected Void call() throws Exception {
                updateMessage("Running...");
                try {
                    DatagramSocket serverSocket = new DatagramSocket(mPortNum);
                    // allocate space for received datagrams
                    byte[] bytes = new byte[512];
                    //message.setByteBuffer(ByteBuffer.wrap(bytes), 0);
                    DatagramPacket packet = new DatagramPacket(bytes, bytes.length);                    
                    while (!isCancelled()) {                    
                        serverSocket.receive(packet);
                        SystemStatusMessage message = new SystemStatusMessage();
                        message.setByteBuffer(ByteBuffer.wrap(bytes), 0);                         
                    }
                } catch (Exception ex) {
                    System.out.println(ex.getMessage());
                }
                updateMessage("Cancelled");
                return null;
            } 
        };
    }
}
1个回答

5
"低级别"的方法是使用Platform.runLater(Runnable r)来更新UI。这将在FX应用程序线程上执行r,相当于Swing的SwingUtilities.invokeLater(...)。因此,一种方法是从call()方法内部简单地调用Platform.runLater(...)并更新UI。不过,正如你指出的那样,这基本上需要服务知道UI的详细信息,这是不可取的(尽管有模式可以解决这个问题)。 Task定义了一些属性和相应的updateXXX方法,例如您在示例代码中调用的updateMessage(...)方法。这些方法可以从任何线程安全地调用,并会导致对应属性的更新在FX应用程序线程上执行。 (因此,在您的示例中,您可以安全地将标签的文本绑定到服务的messageProperty)。除了确保更新在正确的线程上执行外,这些updateXXX方法还会限制更新速度,因此您可以根据需要随便调用它们,而不会使FX应用程序线程被太多事件淹没:在单个UI帧内发生的更新将被合并,以便只有最后一个此类更新(在给定帧内)是可见的。
如果适合您的用例,您可以利用这一点来更新任务/服务的valueProperty。因此,如果您有一些(最好是不可变的)表示解析数据包结果的类(我们称其为PacketData;但可能只是一个简单的String),您可以在Taskcall()方法中创建并返回该类的实例,并使用updateXXX方法将其属性更新到FX应用程序线程上。然后,您可以在UI代码中绑定这些属性,以便在处理完成后自动更新UI。
public class StatusListener implements Service<PacketData> {

   // ...

   @Override
   protected Task<PacketData> createTask() {
      return new Task<PacketData>() {
          // ...

          @Override
          public PacketData call() {
              // ...
              while (! isCancelled()) { 
                  // receive packet, parse data, and wrap results:
                  PacketData data = new PacketData(...);
                  updateValue(data);
              }
              return null ;
          }
      };
   }
}

现在你可以做的事情
StatusListener listener = new StatusListener();
listener.valueProperty().addListener((obs, oldValue, newValue) -> {
    // update UI with newValue...
});
listener.start();

请注意,当服务被取消时,代码会更新值为null,因此使用我概述的实现方法时,您需要确保在valueProperty()上的监听器处理此情况。
另请注意,如果在同一帧渲染中连续调用updateValue(),则它们将合并。因此,如果您需要确保在处理程序中处理每个数据,则这不是一个合适的方法(尽管通常这种功能无需在FX应用程序线程上执行)。如果您的UI只需要显示后台进程的“最新状态”,那么这是一个很好的方法。
SSCCE演示此技术:
import java.util.Random;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class LongRunningTaskExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        CheckBox enabled = new CheckBox("Enabled");
        enabled.setDisable(true);
        CheckBox activated = new CheckBox("Activated");
        activated.setDisable(true);
        Label name = new Label();
        Label value = new Label();

        Label serviceStatus = new Label();

        StatusService service = new StatusService();
        serviceStatus.textProperty().bind(service.messageProperty());

        service.valueProperty().addListener((obs, oldValue, newValue) -> {
            if (newValue == null) {
                enabled.setSelected(false);
                activated.setSelected(false);
                name.setText("");
                value.setText("");
            } else {
                enabled.setSelected(newValue.isEnabled());
                activated.setSelected(newValue.isActivated());
                name.setText(newValue.getName());
                value.setText("Value: "+newValue.getValue());
            }
        });

        Button startStop = new Button();
        startStop.textProperty().bind(Bindings
                .when(service.runningProperty())
                .then("Stop")
                .otherwise("Start"));

        startStop.setOnAction(e -> {
            if (service.isRunning()) {
                service.cancel() ;
            } else {
                service.restart();
            }
        });

        VBox root = new VBox(5, serviceStatus, name, value, enabled, activated, startStop);
        root.setAlignment(Pos.CENTER);
        Scene scene = new Scene(root, 400, 400);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private static class StatusService extends Service<Status> {
        @Override
        protected Task<Status> createTask() {
            return new Task<Status>() {
                @Override
                protected Status call() throws Exception {
                    Random rng = new Random();
                    updateMessage("Running");
                    while (! isCancelled()) {

                        // mimic sporadic data feed:
                        try {
                            Thread.sleep(rng.nextInt(2000));
                        } catch (InterruptedException exc) {
                            Thread.currentThread().interrupt();
                            if (isCancelled()) {
                                break ;
                            }
                        }

                        Status status = new Status("Status "+rng.nextInt(100), 
                                rng.nextInt(100), rng.nextBoolean(), rng.nextBoolean());
                        updateValue(status);
                    }
                    updateMessage("Cancelled");
                    return null ;
                }
            };
        }
    }

    private static class Status {
        private final boolean enabled ; 
        private final boolean activated ;
        private final String name ;
        private final int value ;

        public Status(String name, int value, boolean enabled, boolean activated) {
            this.name = name ;
            this.value = value ;
            this.enabled = enabled ;
            this.activated = activated ;
        }

        public boolean isEnabled() {
            return enabled;
        }

        public boolean isActivated() {
            return activated;
        }

        public String getName() {
            return name;
        }

        public int getValue() {
            return value;
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

感谢您详细的回答。我的服务是长时间运行的,因此每当一个数据包到达时(它可能会影响4或5个复选框状态和几个文本字段),我应该为每个小部件创建自定义属性(类似于Value),并将其与我的StatusListener服务联系起来。这听起来熟悉吗?还是我完全错了?我喜欢更新被合并的事实-当网络流量很大时非常有用-我曾经在使用swing开发时使用过SwingWorker的类似功能。 - johnco3
不太确定我理解了。使用我建议的结构,您只需将所有数据(即4或5个布尔值和可能几个字符串)封装在我称之为PacketData的类中,虽然您显然会为其取一个更有意义的名称。然后只需在侦听器中解包数据并相应地更新UI即可。 - James_D
在我的情况下,我的任务返回一个 <void>,基本上它会一直运行 - 在你展示的例子中,它返回一个 PacketData - 大概每个数据报一次 - 我的想法是服务将一直运行,直到应用程序完成 - 也许我这样做是错误的。 - johnco3
我的示例也只有在取消时才退出,并在那时返回 null(更新值)。请注意,我仍然拥有相同的 while 循环:它可以运行任意长时间。但是,随着数据包的处理,它也会更新值,并且该值仍然可以从 UI 中观察到。实际上,您可能根本不需要 Service,而只需使用单个 Task,尽管您可能希望在用户取消后重新启动。这基本上是 Task 文档 中“返回部分结果的任务”的示例。 - James_D
2
@johnco3 推荐阅读 ServiceTask 的广泛文档,其中包含大量示例。后者甚至有一个名为“修改场景图的任务”的部分。 - VGR
已更新,附带示例。 - James_D

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