How can I externally update a JavaFX scene?


Question

I am trying to learn JavaFX and convert a swing application to JavaFX. What I want to do is use JavaFX to display the progress of a program.

What I was previously doing in Swing was first creating a JFrame with a custom JComponent. Then have my main program call a method of the custom JComponent that would change the colour of a shape within the JComponent and repaint().

The below gives an idea of the kind of thing I want to achieve in JavaFX:

//Run JavaFX in a new thread and continue with the main program.
public class Test_Main{
    public static void main(String[] args) {
        Test test = new Test();
        Thread t = new Thread(test);
        t.start();

        //Main Program
        JOptionPane.showMessageDialog(null, "Click 'OK' to continue.",
                "Pausing", JOptionPane.INFORMATION_MESSAGE);

        //Update Progress
        test.setText("Hello World!");
    }    
}

I currently have this as my runnable.

public class Test extends Application implements Runnable{
    Button btn;

    @Override
    public void run() {
        launch();
    }

    @Override
    public void start(Stage stage) throws Exception {
        StackPane stack = new StackPane();
        btn = new Button();
        btn.setText("Testing");
        stack.getChildren().add(btn);
        Scene scene = new Scene(stack, 300, 250);
        stage.setTitle("Welcome to JavaFX!");
        stage.setScene(scene);
        stage.show();        
    }    

    public void setText(String newText){
        btn.setText(newText);
    }
}

Everything runs fine until I try to update the text of the button in which I get a NullPointerException. I guess this has something to do with the JavaFX application thread. I cannot find anything on the net though which describes how to update things externally.

I see a lot of mention about Platform.runLater and Task but these are usually nested in the start method and run on timers.

UPDATE: Just to clarify I am hoping to achieve something like this:

public class Test_Main{
    public static void main(String[] args) {
        final boolean displayProgress = Boolean.parseBoolean(args[0]);

        Test test = null;
        if(displayProgress){    //only create JavaFX application if necessary
            test = new Test();
            Thread t = new Thread(test);
            t.start();
        }

        //main program starts here

        // ...

        //main program occasionally updates JavaFX display
        if(displayProgress){    //only update JavaFX if created
            test.setText("Hello World!");
        }

        // ...

        //main program ends here
    }    
}
1
1
10/13/2014 6:14:18 PM

Accepted Answer

The NullPointerException has nothing to do with threading (though you also have threading errors in your code).

Application.launch() is a static method. It creates an instance of the Application subclass, initializes the Java FX system, starts the FX Application Thread, and invokes start(...) on the instance which it created, executing it on the FX Application Thread.

So the instance of Test on which start(...) is invoked is a different instance to the one you created in your main(...) method. Hence the btn field in the instance you created in Test_Main.main() is never initialized.

If you add a constructor which just does some simple logging:

public Test() {
    Logger.getLogger("Test").log(Level.INFO, "Created Test instance");
}

you will see that two instances are created.

The API is simply not designed to be used this way. You should regard start(...) essentially as a replacement for the main method when you are using JavaFX. (Indeed, in Java 8, you can omit the main method entirely from your Application subclass and still launch the class from the command line.) If you want a class to be reusable, don't make it a subclass of Application; either make it a subclass of some container-type node, or (better in my opinion) give it a method that accesses such a node.

There are threading issues in your code too, though these are not causing the null pointer exception. Nodes that are part of a scene graph can only be accessed from the JavaFX Application Thread. A similar rule exists in Swing: swing components can only be accessed from the AWT event handling thread, so you really should be calling JOptionPane.showMessageDialog(...) on that thread. In JavaFX, you can use Platform.runLater(...) to schedule a Runnable to run on the FX Application Thread. In Swing, you can use SwingUtilities.invokeLater(...) to schedule a Runnable to run on the AWT event dispatch thread.

Mixing Swing and JavaFX is a pretty advanced topic, because you necessarily need to communicate between the two threads. If you are looking to launch a dialog as an external control for a JavaFX stage, it's probably better to make the dialog a JavaFX window too.

Updated:

Following discussion in the comments, I'm assuming the JOptionPane is just a mechanism to provide a delay: I'll modify your example here so it just waits five seconds before changing the text of the button.

The bottom line is that any code you want to reuse in different ways should not be in an Application subclass. Create an Application subclass solely as a startup mechanism. (In other words, Application subclasses are really not reusable; put everything except the startup process somewhere else.) Since you potentially want to use the class you called Test in more than one way, you should place it in a POJO (plain old Java object) and create a method that gives access to the UI portion it defines (and hooks to any logic; though in a real application you probably want the logic factored out into a different class):

import java.util.logging.Level;
import java.util.logging.Logger;

import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;

public class Test {

    private Button btn;
    private Pane view ;

    public Test(String text) {
        Logger.getLogger("Test").log(Level.INFO, "Created Test instance");

        view = new StackPane();
        btn = new Button();
        btn.setText(text);
        view.getChildren().add(btn);

    }   

    public Parent getView() {
        return view ;
    }

    public void setText(String newText){
        btn.setText(newText);
    }
}

Now let's assume you want to run this two ways. For illustration, we'll have a TestApp that starts the button with the text "Testing", then five seconds later changes it to "Hello World!":

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class TestApp extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {

        // launch app:

        Test test = new Test("Testing");
        primaryStage.setScene(new Scene(test.getView(), 300, 250));
        primaryStage.show();

        // update text in 5 seconds:

        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException exc) {
                throw new Error("Unexpected interruption", exc);
            }
            Platform.runLater(() -> test.setText("Hello World!"));
        });
        thread.setDaemon(true);
        thread.start();

    }    
}

Now a ProductionApp that just launches it right away with the text initialized directly to "Hello World!":

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;


public class ProductionApp extends Application {
    @Override
    public void start(Stage primaryStage) {
        Test test = new Test("Hello World!");
        primaryStage.setScene(new Scene(test.getView(), 300, 250));
        primaryStage.show();
    }

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

Note that there is an overloaded form of Application.launch(...) that takes the Application subclass as a parameter. So you could have a main method somewhere else that made a decision as to which Application was going to execute:

import javafx.application.Application;

public class Launcher {

    public static void main(String[] args) {
        if (args.length == 1 && args[0].equalsIgnoreCase("test")) {
            Application.launch(TestApp.class, args) ;
        } else {
            Application.launch(ProductionApp.class, args);
        }
    }
}

Note that you can only call launch(...) once per invocation of the JVM, which means it's good practice only to ever call it from a main method.

Continuing in the "divide and conquer" theme, if you want the option to run the application "headlessly" (i.e. with no UI at all), then you should factor out the data that is being manipulated from the UI code. In any real-sized application, this is good practice anyway. If you intend to use the data in a JavaFX application, it will be helpful to use JavaFX properties to represent it.

In this toy example, the only data is a String, so the data model looks pretty simple:

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class DataModel {
    private final StringProperty text = new SimpleStringProperty(this, "text", "");

    public final StringProperty textProperty() {
        return this.text;
    }

    public final java.lang.String getText() {
        return this.textProperty().get();
    }

    public final void setText(final java.lang.String text) {
        this.textProperty().set(text);
    }

    public DataModel(String text) {
        setText(text);
    }
}

The modified Test class encapsulating the reusable UI code looks like:

import java.util.logging.Level;
import java.util.logging.Logger;

import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;

public class Test {

    private Pane view ;

    public Test(DataModel data) {
        Logger.getLogger("Test").log(Level.INFO, "Created Test instance");

        view = new StackPane();
        Button btn = new Button();
        btn.textProperty().bind(data.textProperty());
        view.getChildren().add(btn);

    }   

    public Parent getView() {
        return view ;
    }
}

The UI-baed application looks like:

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class TestApp extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {

        // launch app:
        DataModel data = new DataModel("Testing");
        Test test = new Test(data);
        primaryStage.setScene(new Scene(test.getView(), 300, 250));
        primaryStage.show();

        // update text in 5 seconds:

        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException exc) {
                throw new Error("Unexpected interruption", exc);
            }

            // Update text on FX Application Thread:
            Platform.runLater(() -> data.setText("Hello World!"));
        });
        thread.setDaemon(true);
        thread.start();

    }    
}

and an application that just manipulates the data with no view attached looks like:

public class HeadlessApp {

    public static void main(String[] args) {
        DataModel data = new DataModel("Testing");
        data.textProperty().addListener((obs, oldValue, newValue) -> 
            System.out.printf("Text changed from %s to %s %n", oldValue, newValue));
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException exc) {
                throw new Error("Unexpected Interruption", exc);
            }
            data.setText("Hello World!");
        });
        thread.start();
    }

}
5
10/13/2014 7:06:24 PM

This code does what I think you're looking to do:

package javafxtest;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

/**
 * @author ericjbruno
 */
public class ShowJFXWindow {
    {
        // Clever way to init JavaFX once
        JFXPanel fxPanel = new JFXPanel();
    }

    public static void main(String[] args) {
        ShowJFXWindow dfx = new ShowJFXWindow();
        dfx.showWindow();
    }

    public void showWindow() {
        // JavaFX stuff needs to be done on JavaFX thread
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                openJFXWindow();
            }
        });
    }

    public void openJFXWindow() {
        Button btn = new Button();
        btn.setText("Say 'Hello World'");
        btn.setOnAction(new EventHandler<ActionEvent>() {

            @Override
            public void handle(ActionEvent event) {
                System.out.println("Hello World!");
            }
        });

        StackPane root = new StackPane();
        root.getChildren().add(btn);

        Scene scene = new Scene(root, 300, 250);
        Stage stage = new Stage();
        stage.setTitle("Hello World!");
        stage.setScene(scene);
        stage.show();
    }
}

Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon