如何从外部更新JavaFX场景?

时间:2014-10-13 16:00:20

标签: java swing javafx

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

我之前在Swing中所做的是首先使用自定义JComponent创建JFrame。然后让我的主程序调用自定义JComponent的方法,该方法将改变JComponent中的形状颜色并重绘()。

下面给出了我想在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
    }    
}

3 个答案:

答案 0 :(得分:4)

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

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

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

如果你添加一个只做一些简单记录的构造函数:

Test_Main.main()

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

API并非设计为以这种方式使用。在使用JavaFX时,您应该将public Test() { Logger.getLogger("Test").log(Level.INFO, "Created Test instance"); } 基本上视为{{​​1}}方法的替换。 (实际上,在Java 8中,您可以完全从start(...)子类中省略main方法,并仍然从命令行启动该类。)如果您希望类可重用,请不要它是main的子类;要么使它成为某个容器类型节点的子类,要么(在我看来更好)给它一个访问这样一个节点的方法。

您的代码中也存在线程问题,尽管这些问题不会导致空指针异常。只能从JavaFX应用程序线程访问属于场景图的节点。 Swing中存在类似的规则:只能从AWT事件处理线程访问swing组件,所以你真的应该在该线程上调用Application。在JavaFX中,您可以使用Application来安排JOptionPane.showMessageDialog(...)在FX应用程序线程上运行。在Swing中,您可以使用Platform.runLater(...)来安排Runnable在AWT事件派发线程上运行。

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

<强>更新

在评论中讨论后,我假设SwingUtilities.invokeLater(...)只是一种提供延迟的机制:我将在这里修改你的例子,这样它只需要等待五秒钟才能更改按钮的文本。

底线是,您希望以不同方式重用的任何代码都不应位于Runnable子类中。仅创建JOptionPane子类作为启动机制。 (换句话说,Application子类实际上是不可重用的;除了启动过程之外的其他所有内容。)因为您可能想要以多种方式使用您调用Application的类,所以应该放置它在一个POJO(普通的旧Java对象)中创建一个方法,可以访问它定义的UI部分(并挂钩到任何逻辑;虽然在实际的应用程序中你可能希望将逻辑分解到不同的类中):

Application

现在我们假设您想要以这两种方式运行。为了说明,我们将有Test启动按钮,文本为“Testing”,然后五秒后将其更改为“Hello World!”:

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,只需将文本直接初始化为“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的重载形式将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

请注意,每次调用JVM时,您只能调用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); } } } 方法调用它。

继续“分而治之”主题,如果您希望选项以“无头”运行应用程序(即根本没有UI),那么您应该将从UI代码中操作的数据分解出来。 在任何实际大小的应用程序中,无论如何这都是很好的做法。如果您打算在JavaFX应用程序中使用数据,那么使用JavaFX属性来表示它将会很有帮助。

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

launch(...)

封装可重用UI代码的修改后的main类如下所示:

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

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

Test

并且只是操作没有附加视图的数据的应用程序如下所示:

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

答案 1 :(得分:1)

此代码执行我认为您要执行的操作:

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

答案 2 :(得分:0)

尝试从UI线程中调用:

public void setText(final String newText) {
    Platform.runLater(new Runnable() {
        @Override
        public void run() {
            btn.setText(newText);
        }
    });
}

只要您想要更改UI的元素,就必须在UI线程中完成。 Platform.runLater(new Runnable());会做到这一点。这可以防止阻塞和其他奇怪的与UI相关的错误和异常发生。

提示:在应用程序启动时调用Platform.runLater时已阅读/看到的内容,并且在计时器上通常是一种立即加载大部分UI的方法,然后填写其他部分后秒或两个(计时器),以便在启动时不阻止。但Platform.runLater不仅适用于启动,也适用于您需要更改/使用/交互的UI元素。