如何在外部更新JavaFX场景? [英] How can I externally update a JavaFX scene?

查看:131
本文介绍了如何在外部更新JavaFX场景?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I am trying to learn JavaFX and convert a swing application to JavaFX. What I want to do is use JavaFX to display the progress of a program.

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

What I was previously doing in Swing was first creating a JFrame with a custom JComponent. Then have my main program call a method of the custom JComponent that would change the colour of a shape within the JComponent and repaint().

下面给出了一个关于我希望在JavaFX中实现的东西:

The below gives an idea of the kind of thing I want to achieve in 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!");
    }    
}

我目前将此作为我的runnable。

I currently have this as my runnable.

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应用程序线程有关。我在网上找不到任何东西,虽然它描述了如何在外部更新东西。

Everything runs fine until I try to update the text of the button in which I get a NullPointerException. I guess this has something to do with the JavaFX application thread. I cannot find anything on the net though which describes how to update things externally.

我看到很多提及 Platform.runLater 任务但这些通常嵌套在start方法中并在计时器上运行。

I see a lot of mention about Platform.runLater and Task but these are usually nested in the start method and run on timers.

更新:
只是为了澄清我希望实现这样的目标:

UPDATE: Just to clarify I am hoping to achieve something like this:

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 无关使用线程(虽然你的代码中也有线程错误)。

The NullPointerException has nothing to do with threading (though you also have threading errors in your code).

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

Application.launch() is a static method. It creates an instance of the Application subclass, initializes the Java FX system, starts the FX Application Thread, and invokes start(...) on the instance which it created, executing it on the FX Application Thread.

所以的实例测试 start(...)的$ c>与您在 main(...)<中创建的实例不同/ code>方法。因此,您在 Test_Main.main()中创建的实例中的 btn 字段永远不会被初始化。

So the instance of Test on which start(...) is invoked is a different instance to the one you created in your main(...) method. Hence the btn field in the instance you created in Test_Main.main() is never initialized.

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

If you add a constructor which just does some simple logging:

public Test() {
    Logger.getLogger("Test").log(Level.INFO, "Created Test instance");
}

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

you will see that two instances are created.

API并非设计为以这种方式使用。您应该将 start(...)视为 main 方法的替换当您使用JavaFX时。 (实际上,在Java 8中,您可以完全从 Application 子类中省略 main 方法,并且仍然可以从命令行。)如果你想让一个类可以重用,不要把它变成 Application的子类;要么使它成为某个容器类型节点的子类,要么(在我看来更好)给它一个访问这样一个节点的方法。

The API is simply not designed to be used this way. You should regard start(...) essentially as a replacement for the main method when you are using JavaFX. (Indeed, in Java 8, you can omit the main method entirely from your Application subclass and still launch the class from the command line.) If you want a class to be reusable, don't make it a subclass of Application; either make it a subclass of some container-type node, or (better in my opinion) give it a method that accesses such a node.

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

There are threading issues in your code too, though these are not causing the null pointer exception. Nodes that are part of a scene graph can only be accessed from the JavaFX Application Thread. A similar rule exists in Swing: swing components can only be accessed from the AWT event handling thread, so you really should be calling JOptionPane.showMessageDialog(...) on that thread. In JavaFX, you can use Platform.runLater(...) to schedule a Runnable to run on the FX Application Thread. In Swing, you can use SwingUtilities.invokeLater(...) to schedule a Runnable to run on the AWT event dispatch thread.

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

Mixing Swing and JavaFX is a pretty advanced topic, because you necessarily need to communicate between the two threads. If you are looking to launch a dialog as an external control for a JavaFX stage, it's probably better to make the dialog a JavaFX window too.

更新:

在评论中讨论后,我假设 JOptionPane 只是一种提供延迟的机制:我将在此处修改您的示例,以便在更改按钮文本之前等待五秒钟。

Following discussion in the comments, I'm assuming the JOptionPane is just a mechanism to provide a delay: I'll modify your example here so it just waits five seconds before changing the text of the button.

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

The bottom line is that any code you want to reuse in different ways should not be in an Application subclass. Create an Application subclass solely as a startup mechanism. (In other words, Application subclasses are really not reusable; put everything except the startup process somewhere else.) Since you potentially want to use the class you called Test in more than one way, you should place it in a POJO (plain old Java object) and create a method that gives access to the UI portion it defines (and hooks to any logic; though in a real application you probably want the logic factored out into a different class):

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!:

Now let's assume you want to run this two ways. For illustration, we'll have a TestApp that starts the button with the text "Testing", then five seconds later changes it to "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!即可立即启动:

Now a ProductionApp that just launches it right away with the text initialized directly to "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 将要执行:

Note that there is an overloaded form of Application.launch(...) that takes the Application subclass as a parameter. So you could have a main method somewhere else that made a decision as to which Application was going to execute:

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时,启动(...),这意味着只有从 main 方法。

Note that you can only call launch(...) once per invocation of the JVM, which means it's good practice only to ever call it from a main method.

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

Continuing in the "divide and conquer" theme, if you want the option to run the application "headlessly" (i.e. with no UI at all), then you should factor out the data that is being manipulated from the UI code. In any real-sized application, this is good practice anyway. If you intend to use the data in a JavaFX application, it will be helpful to use JavaFX properties to represent it.

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

In this toy example, the only data is a String, so the data model looks pretty simple:

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

修改后的测试类如下所示:

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的应用程序如下所示:

The UI-baed application looks like:

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

    }    
}

以及只是操纵没有附加视图的数据如下所示:

and an application that just manipulates the data with no view attached looks like:

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

}

这篇关于如何在外部更新JavaFX场景?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆