The NullPointerException
与线程无关(尽管您的代码中也存在线程错误)。
Application.launch()
是一个静态方法。它创建了一个实例Application
子类,初始化 Java FX 系统,启动 FX 应用程序线程,并调用start(...)
在它创建的实例上,在 FX 应用程序线程上执行它。
所以实例Test
其上start(...)
被调用的实例与您在中创建的实例不同main(...)
方法。因此btn
您创建的实例中的字段Test_Main.main()
从未被初始化。
如果您添加一个仅执行一些简单日志记录的构造函数:
public Test() {
Logger.getLogger("Test").log(Level.INFO, "Created Test instance");
}
您将看到创建了两个实例。
API 根本就不是为了以这种方式使用而设计的。你应该重视start(...)
本质上作为替代品为了main
当您使用 JavaFX 时的方法。 (事实上,在 Java 8 中,您可以省略main
方法完全来自你的Application
子类并仍然从命令行启动该类。)如果您希望一个类可重用,请不要将其作为Application
;要么使其成为某些容器类型节点的子类,要么(在我看来更好)为其提供访问此类节点的方法。
您的代码中也存在线程问题,尽管这些问题不会导致空指针异常。作为场景图一部分的节点只能从 JavaFX 应用程序线程访问。 Swing 中也存在类似的规则:swing 组件只能从 AWT 事件处理线程访问,因此您确实应该调用JOptionPane.showMessageDialog(...)
在那条线上。在JavaFX中,您可以使用Platform.runLater(...)
安排一个Runnable
在 FX 应用程序线程上运行。在 Swing 中,您可以使用SwingUtilities.invokeLater(...)
安排一个Runnable
在 AWT 事件调度线程上运行。
混合 Swing 和 JavaFX 是一个非常高级的主题,因为您必然需要在两个线程之间进行通信。如果您希望启动一个对话框作为 JavaFX 阶段的外部控件,那么最好将该对话框也设置为 JavaFX 窗口。
Updated:
根据评论中的讨论,我假设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();
}
}
Now a 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
子类作为参数。所以你可以在其他地方有一个主要方法来决定哪个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
method.
继续“分而治之”主题,如果您希望选择“无头”运行应用程序(即根本没有 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();
}
}