0
votes

everybody. I have a program, which should automatically control some machinery. I need javaFX to show the temprorary state of the workshop. There are several processes executing one after another and for each of them i need to update an image on the screen (let's make it simplier and say we need to update a label).

So, there is a main thread, which controls the machinery and there is an FX application Thread, which controls the GUI.

     public static void main(String[] args) {
//some processes in the main thread before launching  GUI (like connecting to the database)
     Thread guiThread = new Thread() {
                @Override
                public void run() {
                    DisplayMain.launchGUI();
                }
            };
            guiThread.start();
//some processes after launching the GUI, including updating the image on the screen
            }

I've read a whole bunch of materials here on SO and on the Oracle's docs and now I can't make sense out of all these bindings, observable properties, Platform.runLater, Tasks, retrieving the controller, passing controller as a parameter to some class etc

I have an fxml file, let's say it only shows a label:

    <?xml version="1.0" encoding="UTF-8"?>

    <?import javafx.scene.control.*?>
    <?import javafx.scene.layout.*?>
    <?import javafx.scene.control.Label?>


    <GridPane alignment="center" 
              hgap="10" vgap="10" 
              xmlns:fx="http://javafx.com/fxml/1" 
              xmlns="http://javafx.com/javafx/8" 
              fx:controller="sample.Controller">
       <columnConstraints>
          <ColumnConstraints />
       </columnConstraints>
       <rowConstraints>
          <RowConstraints />
       </rowConstraints>
       <children>
          <Pane prefHeight="200.0" prefWidth="200.0">
             <children>
                 <Label fx:id="label" text="Label" />
             </children>
          </Pane>
       </children>
    </GridPane>

There is a controller attached to it. I assume that is the place where we should listen for the change of the image or something.

package sample;

import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;

import java.net.URL;
import java.util.ResourceBundle;

public class Controller implements Initializable {

  @FXML
  public void initialize(URL location, ResourceBundle resources) {
    //some listeners?
  }

  @FXML
  private Label label;

  public void setlabel(String s) {
    label.setText(s);
  }
}

And there is a Display.java which is used as a start up mechanism.

package sample;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Display extends Application {
  @Override
  public void start(Stage primaryStage) throws Exception {
    FXMLLoader loader = new FXMLLoader(getClass().getResource("sample.fxml"));
    primaryStage.setTitle("Hello World");
    primaryStage.setScene(new Scene(loader.load(), 800, 400));
    primaryStage.show();

  }

  static void launchGUI() {
    Application.launch();
  }
}

And, finally, the question is: how to update the label in the controller from the main()? There is a whole lot of info how to pass data between controllers, how to invoke methods in the controller, but I'm totally lost with my question.

2

2 Answers

2
votes

You should think of the start() method as the application entry point, not the main(...) method, and so you should launch your other threads (that control the "machinery") from start(), not from main(). That way you don't even have the issue of retrieving the reference to the controller in main() (which you basically cannot do, since you can't get the reference to the Application subclass instance there). The main() method should simply bootstrap the launch of the JavaFX toolkit by calling Application.launch(), and do nothing else. (Note that in some deployment scenarios, your main(...) method is not even called, and the Application subclass's start() method is invoked by other mechanisms.)

So refactor as follows:

public class Main { // or whatever you called it...
    public static void main(String[] args) {
        Application.launch(Display.class, args);
    }
}

Then in start:

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Display extends Application {
  @Override
  public void start(Stage primaryStage) throws Exception {
    FXMLLoader loader = new FXMLLoader(getClass().getResource("sample.fxml"));
    primaryStage.setTitle("Hello World");
    primaryStage.setScene(new Scene(loader.load(), 800, 400));
    primaryStage.show();

    Controller controller = loader.getController();

    Thread machineryThread = new Thread(() -> {
        // some processes launching after the GUI, including updating the label
        // which you can now easily do with
        Platform.runLater(() -> controller.setLabel("Some new text"));
    });
    machineryThread.start();
  }


}

If you want to separate the machinery completely from the UI (which is probably a good idea), it's not too hard to do this. Put the machinery in another class. The update of the label is effectively something that consumes (processes) a String (and for updating the image, it might consume some other kind of data). You can make this abstraction by representing it as a java.util.Consumer<String>. So you could do

public class Machinery {

    private final Consumer<String> textProcessor ;

    public Machinery(Consumer<String> textProcessor) {
        this.textProcessor = textProcessor ;
    }

    public void doMachineryWork() {
        // all the process here, and to update the label you do
        textProcessor.accept("Some new text");
        // etc etc
    }
}

Note this class is completely independent of the UI. Your start(..) method would now be

  public void start(Stage primaryStage) throws Exception {
    FXMLLoader loader = new FXMLLoader(getClass().getResource("sample.fxml"));
    primaryStage.setTitle("Hello World");
    primaryStage.setScene(new Scene(loader.load(), 800, 400));
    primaryStage.show();

    Controller controller = loader.getController();

    Machinery machinery = new Machinery(text ->
        Platform.runLater(() -> controller.setLabel(text)));

    Thread machineryThread = new Thread(machinery::doMachineryWork);
    machineryThread.start();
  }

Depending on other aspects of how your application is structured, it might also make sense to start the machinery thread from the controller's initialize() method, instead of from the start() method.

1
votes

Here is an executable sample you can try. It follows some of the principles outlined in James's answer, so I won't add much extra in terms of comments. If you have any further questions, just ask them in comments below the answer.

There are many ways to solve this problem, this just illustrates a quick example I came up with (for example the Consumer mechanism in James's answer is more elegant than the event notification mechanism in this answer). It may not be an optimal structure for your case, but hopefully it provides you with some insight on how you might go about solving your problem.

The sample program provides a factory view where the factory consists of four machines. Each machine can either be in an IDLE state or a BAKING state and the state of each machine changes independently, with each machine in the factory running on its own thread. A graphical view of the entire factory is provided. A view of the machines in the factory lists the machine id and current machine state for each machine. A notification interface is provided so that the graphical view can be dynamically aware of any changes to the state of an underlying machine and update itself appropriately. To ensure that the graphical view is updated on the JavaFX application thread, Platform.runLater is used to run the view update on the JavaFX application thread when a machine state change notification event is received.

factory

import javafx.application.*;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.stage.Stage;

import java.util.*;
import java.util.concurrent.*;

public class FactoryConsole extends Application {
    private Factory factory = new Factory();

    @Override
    public void start(Stage stage) throws Exception {
        VBox layout = new VBox(10);
        layout.setPadding(new Insets(10));
        layout.setPrefSize(100, 100);

        for (Machine machine: factory.getMachines()) {
            MachineView machineView = new MachineView(machine);
            layout.getChildren().add(machineView);
        }

        factory.start();

        stage.setScene(new Scene(layout));
        stage.show();
    }

    @Override
    public void stop() throws Exception {
        factory.stop();
    }

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

enum MachineState {
    IDLE, BAKING
}

class MachineStateChangeEvent {
    private final Machine machine;
    private final MachineState machineState;

    public MachineStateChangeEvent(Machine machine, MachineState machineState) {
        this.machine = machine;
        this.machineState = machineState;
    }

    public Machine getMachine() {
        return machine;
    }

    public MachineState getMachineState() {
        return machineState;
    }
}

interface MachineStateListener {
    void notifyStateChange(MachineStateChangeEvent machineState);
}

class MachineView extends HBox implements MachineStateListener {
    private final Machine machine;
    private final Label label;

    public MachineView(Machine machine) {
        super();
        this.label = new Label();
        this.machine = machine;
        machine.setMachineStateListener(this);

        getChildren().add(label);
    }

    @Override
    public void notifyStateChange(MachineStateChangeEvent event) {
        if (event.getMachine() != machine) {
            return;
        }

        if (!Platform.isFxApplicationThread()) {
            Platform.runLater(() -> updateState(event.getMachineState()));
        } else {
            updateState(event.getMachineState());
        }
    }

    private void updateState(MachineState machineState) {
        label.setText(machine.getId() + ": " + machineState.toString());
    }
}

class Factory {
    private static final int N_MACHINES = 4;
    private ExecutorService pool = Executors.newFixedThreadPool(N_MACHINES);
    private List<Machine> machines = new ArrayList<>();

    public Factory() {
        for (int i = 0; i < N_MACHINES; i++) {
            machines.add(new Machine());
        }
    }

    public void start() {
        for (Machine machine: machines) {
            pool.submit(machine);
        }
    }

    public void stop() {
        // Disable new tasks from being submitted
        pool.shutdown();
        try {
            // Wait a while for existing tasks to terminate
            if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
                pool.shutdownNow();
                // Cancel currently executing tasks
                // Wait a while for tasks to respond to being cancelled
                if (!pool.awaitTermination(5, TimeUnit.SECONDS))
                    System.err.println("Pool did not terminate");
            }
        } catch (InterruptedException ie) {
            // (Re-)Cancel if current thread also interrupted
            pool.shutdownNow();
            // Preserve interrupt status
            Thread.currentThread().interrupt();
        }
    }

    public List<Machine> getMachines() {
        return machines;
    }
}

class Machine implements Runnable {
    private static final Random random = new Random();
    private static int nextMachineId = 1;

    private int id = nextMachineId++;

    private MachineState state = MachineState.IDLE;
    private MachineStateListener stateListener;

    public void setMachineStateListener(MachineStateListener stateListener) {
        this.stateListener = stateListener;
    }

    @Override
    public void run() {
        try {
            updateState(MachineState.IDLE);
            while (true) {
                Thread.sleep(1000 * random.nextInt(2));
                updateState(MachineState.BAKING);
                Thread.sleep(1000 * random.nextInt(3));
                updateState(MachineState.IDLE);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    private void updateState(MachineState state) {
        this.state = state;
        if (stateListener != null) {
            stateListener.notifyStateChange(new MachineStateChangeEvent(this, state));
        }
    }

    public int getId() {
        return id;
    }

    public MachineState getState() {
        return state;
    }
}