How can I update a text value of Label in non-controller class of Javafx?

102 Views Asked by At

another class is the controller, in a non-controller class of javafx, I can get the Label, but can't update the text value, and there is no any error... any one have the experience, pls help

trying to set the Label text value from a non-controller class

fxml:

<AnchorPane  maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.ning.fx1.HelloController">
   <children>
      <TextField fx:id="tf1" layoutX="66.0" layoutY="52.0" />
      <TextField fx:id="tf2" layoutX="67.0" layoutY="102.0" />
      <TextField fx:id="tf3" layoutX="68.0" layoutY="151.0" />
      <Button fx:id="btn1" layoutX="117.0" layoutY="228.0" mnemonicParsing="false" onAction="#handlerBtn1" text="Button" />
      <Label fx:id="label1" layoutX="402.0" layoutY="106.0" text="" />
      <Label fx:id="label2" layoutX="429.0" layoutY="293.0" text="init" />
   </children>
</AnchorPane>

this is the controller:

public class HelloController{
    @FXML
    public Label label1;
    @FXML
    public Label label2;
    @FXML
    private TextField tf1;
    @FXML
    private TextField tf2;
    @FXML
    private TextField tf3;
    @FXML
    private Button btn1;



    @FXML
    private void handlerBtn1(ActionEvent actionEvent) throws IOException, SchedulerException {
        double num1 = Double.parseDouble(tf1.getText());
        double num2 = Double.parseDouble(tf2.getText());
        tf3.setText(String.valueOf(num1+num2));

        Timer timer = new Timer();
        label1.textProperty().bind(timer.messageProperty());
        new Thread(timer).start();

        JobDetail job1 = JobBuilder.newJob(QuartzJob.class)
                .withIdentity("job1", "group1").build();
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("job1", "group1")
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(4)
                        .repeatForever())
                .build();
        Scheduler scheduler = new StdSchedulerFactory().getScheduler();
        scheduler.start();
        scheduler.scheduleJob(job1,trigger);
        label2.setText("handler");
    }


}

I want update label2.

public class QuartzJob implements Job, Initializable {





    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("jobbbbbbbbbb");
        FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
        Parent root = null;
        try {
            root = fxmlLoader.<Parent>load();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        HelloController controller = fxmlLoader.getController();
        controller.label2.setText("2222222");


    }

    @Override
    public void initialize(URL url, ResourceBundle resourceBundle) {

        System.out.println("init");

    }
}

from the quartz task class, I use "controller.label2.setText("2222222");" I debug it, it can get the Label object, but have no effect

another uncorrelated class:

public class Timer extends Task<String> {
    @Override
    protected String call() throws Exception {
        for (int i=0; i<10; i++){
            System.out.println("calling "+i);
            Thread.sleep(1000);
            updateMessage(String.valueOf(i));
        }

        return null;
    }

    @Override
    protected void updateMessage(String s) {
        super.updateMessage(s);
    }


}

sorry, my code is nonsence


after

now I can update the element value by set the scene as a static object. but it looks weird ...

public class HelloApplication extends Application {

    static Scene scene;
    @Override
    public void start(Stage stage) throws IOException {

        FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
        scene = new Scene(fxmlLoader.load(), 500, 500);
        stage.setTitle("Hello!");
        stage.setScene(scene);
        stage.show();
//        Thread.setDefaultUncaughtExceptionHandler((t, e) ->
//                {
//                    System.out.println("ttttttttttttt"+t);
//                    System.out.println("eeeeeeeeeeeeeeeeeeeee"+e.getStackTrace());
//                    Alert alert = new Alert(Alert.AlertType.ERROR);
//                    alert.setTitle("Information Dialog");
//                    alert.setHeaderText("Look, an Information Dialog");
//                    alert.setContentText("I have a great message for you!");
//
//                    alert.showAndWait();
//
//                }
//
//        );


    }

    public static void main(String[] args) {
        launch();

    }
}

in a task class :

Label label = (Label)HelloApplication.scene.lookup("#label1");
        label.textProperty().unbind();
        label.setText("success");

------ in the end-----

actually, when I load the FXML again, then I get the Scene , but the scene is not the original scene.

I found there is many ways to finish it, such as us construsctor to pass the Label, thanks guys, this is not a good question.

1

There are 1 best solutions below

0
Slaw On

Every time you call FXMLLoader::load you are creating a new object graph and controller. That means the controller instance you "get" in your job is not the same instance attached to the UI. In other words, the label you are updating is not the one visible to the user. You need to find a way to pass the controller, or at least the label, to your job.

I do not have much experience with Quartz, but from browsing the documentation and reading a few other Stack Overflow Q&As, there are three main ways to do this:

  1. Put the FXML controller instance in the job's data map.

    If you do this, make sure the scheduler's data store does not support persistence. Even if the FXML controller could be easily serialized, which should not be attempted, you would not get the correct FXML controller instance back when it is deserialized.

    From what I can tell, the RAMDataStore implementation, which I believe is the default, is the best for your use case.

  2. Put the FXML controller instance in the scheduler's context.

  3. Set your own job factory on the scheduler so that you can manually pass the FXML controller instance to your job when the factory is invoked.

For approaches 1 and 2, you can get the stored object in the job via the JobExecutionContext.

I also recommend you use a proper application architecture such as MVC or MVVM. Ideally, Quartz should not know about JavaFX and your view classes should not know about Quartz. And if you use MVC, MVVM, or similar, then you would pass the model to the Quartz job, not the FXML controller (nor any other view class). Additionally, a Quartz job is not an FXML controller, meaning it makes no sense for the job class to implement Initializable.

That all said, Quartz seems like overkill for what you are trying to do. It would probably be much easier to use JavaFX's ScheduledService or even standard Java's ScheduledExecutorService. Though in both cases, you will still need a way to reference the correct FXML controller (or model) instance.

And finally, regardless of which technology you use, you must ensure the label's text is updated on the JavaFX Application Thread.


Example

Here is an example of passing an FXML controller to a Quartz job via the job's data map. Note it is only meant to demonstrate passing "custom" objects to a job, not necessarily how to structure an application.

MainView.java

package sample;

import java.io.IOException;
import java.io.UncheckedIOException;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;

public class MainView extends VBox {

  @FXML private Label label;
  @FXML private Button button;

  private Runnable onScheduleQuartzJob;

  public MainView() {
    var loader = new FXMLLoader(MainView.class.getResource("/MainView.fxml"));
    loader.setController(this);
    loader.setRoot(this);
    try {
      loader.load();
    } catch (IOException ex) {
      throw new UncheckedIOException(ex);
    }
  }

  public void setLabelText(String text) {
    label.setText(text);
  }

  public void setOnScheduleQuartzJob(Runnable onScheduleQuartzJob) {
    this.onScheduleQuartzJob = onScheduleQuartzJob;
  }

  @FXML
  private void handleScheduleQuartzJob(ActionEvent event) {
    event.consume();
    if (onScheduleQuartzJob != null) {
      button.setDisable(true);
      onScheduleQuartzJob.run();
    }
  }
}

MainView.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Tooltip?>
<?import javafx.scene.layout.VBox?>

<fx:root type="VBox" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
         alignment="CENTER" spacing="10">
    <Label fx:id="label" text="Hello, World!"/>
    <Button fx:id="button" text="Schedule Quartz job!" onAction="#handleScheduleQuartzJob">
        <tooltip>
            <Tooltip text="Schedules a Quartz job to change the label's text."/>
        </tooltip>
    </Button>
</fx:root>

UpdateLabelJob.java

package sample;

import javafx.application.Platform;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class UpdateLabelJob implements Job {

  public static final String MAIN_VIEW_KEY = MainView.class.getName();

  @Override
  public void execute(JobExecutionContext context) throws JobExecutionException {
    var view = (MainView) context.getMergedJobDataMap().get(MAIN_VIEW_KEY);
    Platform.runLater(() -> view.setLabelText("Text set from Quartz job!"));
  }
}

Main.java

package sample;

import java.time.Instant;
import java.util.Date;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.quartz.JobBuilder;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;

public class Main extends Application {

  private Scheduler scheduler;

  @Override
  public void init() throws Exception {
    scheduler = new StdSchedulerFactory().getScheduler();
    scheduler.start();
  }

  @Override
  public void start(Stage primaryStage) throws Exception {
    var root = new MainView();
    root.setOnScheduleQuartzJob(() -> scheduleUpdateLabelJob(root));

    primaryStage.setScene(new Scene(root, 500, 300));
    primaryStage.setTitle("Quartz Example");
    primaryStage.show();
  }

  private void scheduleUpdateLabelJob(MainView view) {
    var details =
        JobBuilder.newJob(UpdateLabelJob.class)
            .withIdentity("update-label-job", "example")
            .build();
    // Add MainView instance to the job's data map
    details.getJobDataMap().put(UpdateLabelJob.MAIN_VIEW_KEY, view);

    var trigger =
        TriggerBuilder.newTrigger()
            .forJob(details)
            .withIdentity("update-label-trigger", "example")
            .startAt(computeJobDelay())
            .build();

    try {
      scheduler.scheduleJob(details, trigger);
    } catch (SchedulerException ex) {
      throw new RuntimeException("Unable to schedule job", ex);
    }
  }

  private Date computeJobDelay() {
    var instant = Instant.now().plusSeconds(3);
    return Date.from(instant);
  }

  @Override
  public void stop() throws Exception {
    scheduler.shutdown();
  }
}