[英]How can I externally update a JavaFX scene?
我正在尝试学习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.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
}
}
NullPointerException
与线程无关(尽管您的代码中也存在线程错误)。
Application.launch()
是静态方法。 它创建Application
子类的实例,初始化Java FX系统,启动FX Application线程,并在其创建的实例上调用start(...)
,然后在FX Application线程上执行它。
因此,在其上调用start(...)
的Test
实例与您在main(...)
方法中创建的实例不同。 因此,您在Test_Main.main()
创建的实例中的btn
字段永远不会初始化。
如果添加仅执行一些简单日志记录的构造函数:
public Test() {
Logger.getLogger("Test").log(Level.INFO, "Created Test instance");
}
您将看到创建了两个实例。
该API根本不设计为以这种方式使用。 在使用JavaFX时,应将start(...)
实质上替代 main
方法。 (实际上,在Java 8中,您可以完全从Application
子类中省略main
方法,而仍然可以从命令行启动该类。)如果您希望一个类可重用,请不要使其成为Application
的子类; 要么使其成为某个容器类型节点的子类,要么(在我看来更好)为它提供访问此类节点的方法。
您的代码中也存在线程问题,尽管这些不会导致空指针异常。 场景图中的节点只能从JavaFX Application Thread访问。 Swing中存在类似的规则:只能从AWT事件处理线程访问swing组件,因此,您实际上应该在该线程上调用JOptionPane.showMessageDialog(...)
。 在JavaFX中,可以使用Platform.runLater(...)
安排Runnable
在FX Application Thread上运行。 在Swing中,可以使用SwingUtilities.invokeLater(...)
安排Runnable
在AWT事件分发线程上运行。
将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);
}
}
}
请注意,每次调用JVM只能调用一次launch(...)
,这意味着最好仅从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();
}
}
这段代码可以满足您的需求:
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();
}
}
尝试从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元素进行更改/使用/交互的时间使用。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.