我正在尝试学习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.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
}
}
答案 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元素。