How can I externally update a JavaFX scene?

后端 未结 3 898
悲&欢浪女
悲&欢浪女 2021-01-15 13:24

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 do

3条回答
  •  北恋
    北恋 (楼主)
    2021-01-15 13:56

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

提交回复
热议问题