更改根节点时后台线程不会关闭 [javaFX]

Background Threads not closing when root node is changed [javaFX]

我正在创建一个 javafx 程序,我的程序导航菜单通过更改场景的根来工作。根全部继承自 Pane class。一些窗格具有后台线程,它们 运行。但是,当事件处理程序更改根窗格时,窗格会切换,但后台线程不会停止。这会导致线程读取 NFC 时出现问题,并导致多个线程尝试从 NFC 读取 reader。
如何关闭后台线程? (从创建它们的窗格外)还是我需要以不同的方式设置线程。 (线程设置为守护进程)。 线程是在 Pane 构造函数中创建的,如下所示: (我假设因为它们属于该窗格,所以当切换窗格时线程将停止。事实并非如此)。

  Runnable r = new Runnable() {
        @Override
         public void run () {
             boolean cont = true;
             while(cont){

                try {
                    NFCcard create1 = new NFCcard();

                    String staffID=create1.getCardID().toString();
                    staffID = staffID.replaceAll("\D+","");
                    signIn.setText("Welcome "+getUserName(staffID)+getPhotoSrc(create1.getCardID().toString()));

                    Thread.sleep(1000);

                } catch (CardException e) {


                } catch (InterruptedException e) {

                }
                signIn.setText("Scan your card to sign in/out");
                if(getScene().getRoot().isDisable());
                    cont=false;
             }

            }};

     Thread nfcCheckThread = new Thread(r);
     nfcCheckThread.setDaemon(true);
     nfcCheckThread.start();

我像这样以静态方式切换窗格:(这些方法在它们自己的 class 中)。

   public static void homeButtonhandler(Stage stage){ 
     HomePane mainPane1=new HomePane(stage, new HomeContent(stage));
     stage.getScene().setRoot(mainPane1);
     }
 public static void adminButtonhandler(Stage stage){

      DialogBox dialog = new  DialogBox();

      try{
        Optional<String> result = dialog.showAndWait();
        if (result.get().equals("115")){
            AdminPane adminPane1 = new AdminPane(stage,new Content(stage));
            stage.getScene().setRoot(adminPane1);
            }}

        catch(NoSuchElementException Exception){

        }


 }

public static void workingTodayButtonhandler(Stage stage){
     //TODO trying to make the content change when when buttons are clicked

     HomePane mainPane2=new HomePane(stage,new WorkingTodayContent(stage));

     stage.getScene().setRoot(mainPane2); 
//  System.out.println(mainPane2.content);

 }

第一次来电是:

HomePane myPane = new HomePane(primaryStage,new HomeContent(primaryStage));

    Scene homeScene = new Scene (myPane);

    primaryStage.setMinHeight(1000);
    primaryStage.setMinWidth(1700);

    primaryStage.setScene(homeScene);


    primaryStage.show();

您应该添加一种方法来向负责替换场景根的 class 注册侦听器。这允许您收到此类更改的通知并通过终止线程来做出反应。

例子

@FunctionalInterface
public interface NodeReplaceListener {

    public void onNodeReplace();

}
private Parent root;
private NodeReplaceListener listener;
private Scene scene;

public void setRoot(Parent root, NodeReplaceListener listener) {
    if (root != this.root && this.listener != null) {
        this.listener.onNodeReplace();
    }
    this.root = root;
    this.listener = listener;

    scene.setRoot(root);
}

@Override
public void start(Stage primaryStage) {
    Button btn = new Button("Next Scene");
    btn.setOnAction((ActionEvent event) -> {
        // replace root
        setRoot(new StackPane(new Rectangle(100, 100)), null);
    });

    StackPane root = new StackPane();
    root.getChildren().add(btn);

    scene = new Scene(new Group(), 100, 100);

    class MyRunnable implements Runnable {

        // running volatile to guarantee that visibility of written values to all threads
        volatile boolean running = true;

        int i;

        @Override
        public void run() {
            while (running) {
                System.out.println(i++);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ex) {
                }
            }
        }

        public void cancel() {
            running = false;
        }

    }

    MyRunnable r = new MyRunnable();
    Thread t = new Thread(r);
    t.setDaemon(true);
    t.start();

    primaryStage.setScene(scene);

    // cancel runnable when root is replaced
    setRoot(root, r::cancel);
    primaryStage.show();
}

顺便说一句:请注意,不应从应用程序线程以外的任何线程对场景进行修改。使用 Platform.runLater 来自不同线程的更新:

Platform.runLater(() -> signIn.setText("Scan your card to sign in/out"));

正如你所说:

Some of the Panes have background threads which they run.

我假设您在自己的 Pane 实现中创建这些线程。我将向您展示一个带有抽象 class 的解决方案,您的每个窗格(或至少作为根插入的窗格)都应该扩展。如果您没有,我强烈建议您这样做。虽然您没有提供有关这些窗格的太多信息,所以我将把我的答案集成到您的代码中(如果您决定遵循它)。

public abstract class OwnPane extends Pane {
    protected volatile boolean isRoot = false;

    public void setAsRoot(){
        isRoot = true;
    }

    public void unsetAsRoot(){
        isRoot = false;
    }
}

稍后我会回到 volatile 关键字。

现在您可以创建线程,最好是在 OwnPane 或其子 class 中,例如在 activateThread 方法中:

public void activateNFCThread(){
    Runnable r = new Runnable(){
            @Override
            public void run () {
                 while(isRoot){
                      // what the thread has to do ...
                 }
            }
    };
    Thread nfcCheckThread = new Thread(r);
    nfcCheckThread.setDaemon(true);
    nfcCheckThread.start();
}

现在我可以解释为什么必须使用 volatile 关键字了:字段 isRoot 将被不同的线程使用。使用 volatile 关键字,我们确保所有线程都将访问相同的 "variable"(否则出于性能原因,每个线程都会有自己的版本)。
事实上,线程是在 OwnPane(或子class)中的方法中创建的,允许从 Runnable 对象中访问 isRoot 字段。在 OwnPane 的子 classes 中,您甚至可以覆盖 setAsRoot 方法,使 NFS 线程在调用 setAsRoot 方法时直接启动(如果需要):

public class PaneWithNFCReader extends OwnPane {
    @Override
    public void setAsRoot(){
         super.setAsRoot();
         activateNFCThread();
    }
}

最后,您可以使用这些方法更改舞台中场景的根窗格:

// All your methods regarding stage changes are static, so I'll leave this one static too
public static void changeRoot(Stage stage, OwnPane newRoot){
    OwnPane oldStage = (OwnPane)stage.getScene().getRoot();
    oldStage.unsetAsRoot();
    Platform.runLater(() -> {   //Platform.runLater to be sure the main thread that will execute this 
                                //(only main thread is allowed to change something in the JavaFX nodes)
        stage.getScene().setRoot(newRoot);
        newRoot.setAsRoot();
    });
}