简体   繁体   中英

Updating JavaFX GUI from a service thread

How can I safely update the widgets on a JavaFX GUI from within a JavaFX Service. I remember when I was developing with Swing, I used to 'invoke later' and other various swing worker utilities to ensure that all updates to the UI were handled safely in the Java Event Thread. Here is an example of a simple service thread that handles datagram messages. The bit that is missing is where the datagram message is parsed and corresponding UI widgets are updated. As you can see the service class is very simplistic.

I'm not sure if I need to use simple binding properties (like message) or alternatively should I should pass widgets to the constructor of my StatusListenerService (which is probably not the best thing to do). Can someone give me a good similar example that I would work from.

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;
            } 
        };
    }
}

The "low-level" approach is to use Platform.runLater(Runnable r) to update the UI. This will execute r on the FX Application Thread, and is the equivalent of Swing's SwingUtilities.invokeLater(...) . So one approach is simply to call Platform.runLater(...) from inside your call() method and update the UI. As you point out, though, this essentially requires the service knowing details of the UI, which is undesirable (though there are patterns that work around this).

Task defines some properties and has corresponding updateXXX methods, such as the updateMessage(...) method you call in your example code. These methods are safe to call from any thread, and result in an update to the corresponding property to be executed on the FX Application Thread. (So, in your example, you can safely bind the text of a label to the messageProperty of the service.) As well as ensuring the updates are performed on the correct thread, these updateXXX methods also throttle the updates, so that you can essentially call them as often as you like without flooding the FX Application Thread with too many events to process: updates that occur within a single frame of the UI will be coalesced so that only the last such update (within a given frame) is visible.

You could leverage this to update the valueProperty of the task/service, if it is appropriate for your use case. So if you have some (preferably immutable) class that represents the result of parsing the packet (let's call it PacketData ; but maybe it is as simple as a String ), you make

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 ;
          }
      };
   }
}

Now you can do

StatusListener listener = new StatusListener();
listener.valueProperty().addListener((obs, oldValue, newValue) -> {
    // update UI with newValue...
});
listener.start();

Note that the value is updated to null by the code when the service is cancelled, so with the implementation I outlined you need to make sure that your listener on the valueProperty() handles this case.

Also note that this will coalesce consecutive calls to updateValue() if they occur within the same frame rendering. So this is not an appropriate approach if you need to be sure to process every data in your handler (though typically such functionality would not need to be performed on the FX Application Thread anyway). This is a good approach if your UI is only going to need to show the "most recent state" of the background process.

SSCCE showing this technique:

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);
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM