小编典典

如何从外部更新JavaFX场景?

java

我正在尝试学习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!");
    }    
}

我目前将此作为我的可运行对象。

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.runLaterTask但是这些通常嵌套在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
    }    
}

阅读 326

收藏
2020-11-23

共1个答案

小编典典

NullPointerException有什么用线程做(虽然你也有你的代码线程错误)。

Application.launch()是静态方法。它创建Application子类的实例,初始化Java FX系统,启动FX
Application Thread,并start(...)在其创建的实例上调用,然后在FX Application Thread上执行。

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

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

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

您将看到创建了两个实例。

该API根本不设计为以这种方式使用。在使用JavaFX时,应将其视为该方法start(...)替代品main。(实际上,在Java
8中,您可以main完全从Application子类中省略该方法,而仍然可以从命令行启动该类。)如果您希望一个类可重用,则不要使其成为Application;
的子类。要么使其成为某个容器类型节点的子类,要么(在我看来更好)为它提供访问此类节点的方法。

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

将Swing和JavaFX混合是一个相当高级的主题,因为您必须在两个线程之间进行通信。如果您希望将对话框作为JavaFX阶段的外部控件启动,最好也将对话框设置为JavaFX窗口。

更新:

在评论中进行讨论之后,我假设JOptionPane只是一种提供延迟的机制:我将在此处修改您的示例,以便在更改按钮的文本之前只需等待五秒钟。

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

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

现在假设您要运行这两种方式。为了进行说明,我们将使用一个TestApp以文本“ Testing”开始按钮,然后五秒钟后将其更改为“ 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();

    }    
}

现在ProductionApp,使用直接初始化为“ 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);
    }
}

请注意,有一个重载形式Application.launch(...)Application子类作为参数。因此,您可以在其他地方使用main方法来决定Application要执行的方法:

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

请注意,launch(...)每次调用JVM 只能调用一次,这意味着最好只从main方法中调用它。

继续以“分而治之”为主题,如果您希望选项“无头”运行应用程序(即完全没有UI),则应从UI代码中排除正在处理的数据。
无论如何,在任何实际大小的应用程序中,这都是一个好习惯。 如果您打算在JavaFX应用程序中使用数据,则使用JavaFX属性表示数据将很有帮助。

在这个玩具示例中,唯一的数据是字符串,因此数据模型看起来非常简单:

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

Test封装了可重复使用的UI代码的修改后的类如下所示:

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

基于UI的应用程序如下所示:

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

}
2020-11-23