我正在尝试学习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应用程序线程有关。我无法在网上找到任何内容来描述如何从外部进行更新。
NullPointerException
我看到很多提及Platform.runLater,Task但是这些通常嵌套在start方法中并在计时器上运行。
Platform.runLater
Task
更新: 澄清一下,我希望实现以下目标:
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 } }
该NullPointerException有什么用线程做(虽然你也有你的代码线程错误)。
Application.launch()是静态方法。它创建Application子类的实例,初始化Java FX系统,启动FX Application Thread,并start(...)在其创建的实例上调用,然后在FX Application Thread上执行。
Application.launch()
Application
start(...)
因此,Test在其start(...)上调用的实例与您在main(...)方法中创建的实例不同。因此,btn您在其中创建的实例中的字段Test_Main.main()永远不会初始化。
Test
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; 的子类。要么使其成为某个容器类型节点的子类,要么(在我看来更好)为它提供访问此类节点的方法。
main
您的代码中也存在线程问题,尽管这些不会导致空指针异常。场景图中的节点只能从JavaFX Application Thread访问。Swing中存在类似的规则:只能从AWT事件处理线程访问swing组件,因此您实际上应该JOptionPane.showMessageDialog(...)在该线程上进行调用。在JavaFX中,您可以Platform.runLater(...)用来安排A Runnable在FX Application Thread上运行。在Swing中,您可以SwingUtilities.invokeLater(...)用来安排A Runnable在事件调度线程上运行。
JOptionPane.showMessageDialog(...)
Platform.runLater(...)
Runnable
SwingUtilities.invokeLater(...)
将Swing和JavaFX混合是一个相当高级的主题,因为您必须在两个线程之间进行通信。如果您希望将对话框作为JavaFX阶段的外部控件启动,最好也将对话框设置为JavaFX窗口。
更新:
在评论中进行讨论之后,我假设JOptionPane只是一种提供延迟的机制:我将在此处修改您的示例,以便在更改按钮的文本之前只需等待五秒钟。
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!”:
TestApp
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!”的文本立即启动它:
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子类作为参数。因此,您可以在其他地方使用main方法来决定Application要执行的方法:
Application.launch(...)
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方法中调用它。
launch(...)
继续以“分而治之”为主题,如果您希望选项“无头”运行应用程序(即完全没有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(); } }