如何从外部更新JavaFX场景?

如何从外部更新JavaFX场景?,java,swing,javafx,Java,Swing,Javafx,我正在尝试学习JavaFX并将swing应用程序转换为JavaFX。 我想做的是使用JavaFX显示程序的进度 我之前在Swing中所做的是首先创建一个带有自定义JComponent的JFrame。然后让我的主程序调用自定义JComponent的一个方法,该方法将更改JComponent和repaint()中形状的颜色 下面给出了我希望在JavaFX中实现的一种方法: //Run JavaFX in a new thread and continue with the main program.

我正在尝试学习JavaFX并将swing应用程序转换为JavaFX。 我想做的是使用JavaFX显示程序的进度

我之前在Swing中所做的是首先创建一个带有自定义JComponent的JFrame。然后让我的主程序调用自定义JComponent的一个方法,该方法将更改JComponent和repaint()中形状的颜色

下面给出了我希望在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!");
    }    
}
我目前将此作为我的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);
    }
}
在我尝试更新按钮的文本之前,一切都正常运行,在该按钮中我获得了一个
NullPointerException
。我想这与JavaFX应用程序线程有关。我在网上找不到任何描述如何从外部更新内容的东西

我看到很多关于
Platform.runLater
Task
的内容,但它们通常嵌套在start方法中,并在计时器上运行

更新: 我只是想澄清一下,我希望实现这样的目标:

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

尝试从UI线程内调用,如下所示:

public void setText(final String newText) {
    Platform.runLater(new Runnable() {
        @Override
        public void run() {
            btn.setText(newText);
        }
    });
}
无论何时,只要您想对UI的某个元素进行更改,都必须在UI线程中进行<代码>平台.runLater(新的Runnable())就可以做到这一点。这可以防止阻塞和其他奇怪的与UI相关的错误和异常的发生


提示:您在
平台上读到的内容。runLater
在应用程序启动时被调用,而on timer通常是一种立即加载大部分UI的方法,然后在一两秒钟后填充其他部分(计时器),以便在启动时不会阻塞。但是,
Platform.runLater
不仅用于启动,还可用于您需要更改/使用/与UI元素交互的任何时候。

NullPointerException与线程无关(尽管您的代码中也存在线程错误)

Application.launch()
是一种静态方法。它创建
应用程序
子类的实例,初始化Java FX系统,启动FX应用程序线程,并在创建的实例上调用
启动(…)
,在FX应用程序线程上执行

因此,调用
start(…)
Test
实例与在
main(…)
方法中创建的实例不同。因此,在
Test\u Main.Main()
中创建的实例中的
btn
字段永远不会初始化

如果您添加了一个只执行一些简单日志记录的构造函数:

public Test() {
    Logger.getLogger("Test").log(Level.INFO, "Created Test instance");
}
您将看到创建了两个实例

API根本不是设计成这样使用的。在使用JavaFX时,应该将
start(…)
本质上看作是
main
方法的替代方法。(事实上,在Java8中,您可以从
应用程序
子类中完全省略
main
方法,并且仍然从命令行启动该类。)如果希望类可重用,请不要将其设为
应用程序
的子类;要么让它成为某个容器类型节点的子类,要么(在我看来更好)给它一个访问这样一个节点的方法

代码中也存在线程问题,尽管这些问题不会导致空指针异常。作为场景图一部分的节点只能从JavaFX应用程序线程访问。Swing中也存在类似的规则:只能从AWT事件处理线程访问Swing组件,因此您确实应该在该线程上调用
JOptionPane.showMessageDialog(…)
。在JavaFX中,您可以使用
Platform.runLater(…)
来安排
Runnable
在FX应用程序线程上运行。在Swing中,您可以使用
SwingUtilities.invokeLater(…)
调度
Runnable
在AWT事件调度线程上运行

混合使用Swing和JavaFX是一个非常高级的主题,因为您需要在两个线程之间进行通信。如果您希望启动一个对话框作为JavaFXStage的外部控件,那么最好将该对话框也设置为JavaFX窗口

更新:

在评论中讨论之后,我假设
JOptionPane
只是一种提供延迟的机制:我将在这里修改您的示例,使其在更改按钮文本之前只需等待五秒钟

底线是,您希望以不同方式重用的任何代码都不应位于
应用程序
子类中。创建一个
应用程序
子类作为启动机制。(换句话说,
Application
子类实际上是不可重用的;将启动过程之外的所有东西都放在其他地方。)因为您可能希望以多种方式使用调用的类
Test
,所以应该将其放在POJO(普通的旧Java对象)中并创建一个方法,该方法允许访问它定义的UI部分(并钩住任何逻辑;尽管在实际应用程序中,您可能希望将逻辑分解到不同的类中):

现在让我们假设您希望以两种方式运行它。为了便于说明,我们将有一个
TestApp
,它以文本“Testing”开始按钮,然后五秒钟后将其更改为“Hello World!”:

现在是一个
ProductionApp
,它立即启动,文本直接初始化为“Hello World!”:

请注意,
Application.launch(…)
有一种重载形式,它将
Application
子类作为参数。因此,您可以在其他地方使用一个main方法来决定将执行哪个
应用程序

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);
        }
    }
}
请注意,每次调用JVM只能调用
launch(…)
一次,这意味着最好只从
main
方法调用它

继续“分而治之”主题,如果你想选择r
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();

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

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

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