如何从外部更新 JavaFX 场景?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/26344172/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 02:18:05  来源:igfitidea点击:

How can I externally update a JavaFX scene?

javaswingjavafx

提问by sharp-spark

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.

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

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

我之前在 Swing 中所做的首先是创建一个带有自定义 JComponent 的 JFrame。然后让我的主程序调用自定义 JComponent 的方法,该方法将更改 JComponent 和 repaint() 中形状的颜色。

The below gives an idea of the kind of thing I want to achieve in JavaFX:

下面给出了我想在 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!");
    }    
}

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

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.

一切运行正常,直到我尝试更新我得到NullPointerException. 我想这与 JavaFX 应用程序线程有关。我在网上找不到任何描述如何从外部更新内容的内容。

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

我看到很多关于Platform.runLaterand的提及,Task但这些通常嵌套在 start 方法中并在计时器上运行。

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

采纳答案by James_D

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

NullPointerException与线程无关(尽管您的代码中也有线程错误)。

Application.launch()is a static method. It creates an instance of the Applicationsubclass, 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.

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

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

因此,调用Teston which 的start(...)实例与您在main(...)方法中创建的实例不同。因此,btn您创建的实例中的字段Test_Main.main()永远不会被初始化。

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.

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

The API is simply not designed to be used this way. You should regard start(...)essentially as a replacementfor the mainmethod when you are using JavaFX. (Indeed, in Java 8, you can omit the mainmethod entirely from your Applicationsubclass 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.

API 根本就不是为了以这种方式使用而设计的。当您使用 JavaFX 时,您应该start(...)基本上将其视为该方法的替代品main。(实际上,在 Java 8 中,您可以main完全从Application子类中省略该方法,而仍然从命令行启动该类。)如果您希望一个类可重用,请不要将其设为Application; 要么让它成为某个容器类型节点的子类,要么(在我看来更好)给它一个访问这样一个节点的方法。

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 Runnableto run on the FX Application Thread. In Swing, you can use SwingUtilities.invokeLater(...)to schedule a Runnableto run on the AWT event dispatch thread.

您的代码中也存在线程问题,尽管这些不会导致空指针异常。作为场景图一部分的节点只能从 JavaFX 应用程序线程访问。Swing 中存在类似的规则:只能从 AWT 事件处理线程访问 Swing 组件,因此您确实应该调用JOptionPane.showMessageDialog(...)该线程。在 JavaFX 中,您可以使用Platform.runLater(...)调度一个Runnable在 FX 应用程序线程上运行。在 Swing 中,您可以使用SwingUtilities.invokeLater(...)调度一个Runnable在 AWT 事件调度线程上运行。

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.

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

Updated:

更新:

Following discussion in the comments, I'm assuming the JOptionPaneis 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.

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

The bottom line is that any code you want to reuse in different ways should not be in an Applicationsubclass. Create an Applicationsubclass solely as a startup mechanism. (In other words, Applicationsubclasses are really not reusable; put everything except the startup process somewhere else.) Since you potentially want to use the class you called Testin 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):

最重要的是,您想要以不同方式重用的任何代码都不应位于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);
    }
}

Now let's assume you want to run this two ways. For illustration, we'll have a TestAppthat starts the button with the text "Testing", then five seconds later changes it to "Hello World!":

现在让我们假设您想以两种方式运行。例如,我们将有一个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 ProductionAppthat just launches it right away with the text initialized directly to "Hello World!":

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

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

请注意,有一种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);
        }
    }
}

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 mainmethod.

请注意,launch(...)每次调用 JVM只能调用一次,这意味着最好只从main方法中调用它。

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.

继续“分而治之”主题,如果您希望选择“无头”运行应用程序(即根本没有 UI),那么您应该从 UI 代码中提取出正在操作的数据。在任何实际大小的应用程序中,这无论如何都是很好的做法。如果您打算在 JavaFX 应用程序中使用数据,使用 JavaFX 属性来表示它会很有帮助。

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

The modified Testclass encapsulating the reusable UI code looks like:

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

The UI-baed application looks like:

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

    }    
}

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

}

回答by SnakeDoc

Try calling from within the UI thread as so:

尝试从 UI 线程中调用,如下所示:

public void setText(final String newText) {
    Platform.runLater(new Runnable() {
        @Override
        public void run() {
            btn.setText(newText);
        }
    });
}

Anytime you want to make changes to an element of the UI, it must be done from within the UI thread. Platform.runLater(new Runnable());will do just that. This prevents blocking, and other strange obscure UI-related bugs and exceptions from occurring.

任何时候您想对 UI 的元素进行更改,都必须在 UI 线程内完成。Platform.runLater(new Runnable());会这样做。这可以防止阻塞和其他奇怪的与 UI 相关的错误和异常的发生。

Hint: what you have read/seen with the Platform.runLaterbeing called in the startup of the app, and on timers is usually a way to load most of the UI immediately, then fill in the other parts after a second or two (the timer) so that not to block at startup. But Platform.runLateris not only for startup, it's for any time you need to change/use/interact-with a UI element.

提示:您Platform.runLater在应用程序启动时调用的内容和计时器通常是一种立即加载大部分 UI 的方法,然后在一两秒钟(计时器)后填充其他部分,因此您已阅读/看到的内容不要在启动时阻止。但Platform.runLater不仅适用于启动,它适用于您需要更改/使用/与 UI 元素交互的任何时间。

回答by Eric Bruno

This code does what I think you're looking to do:

这段代码做了我认为你想要做的事情:

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