2

So im trying to stop a single thread when I have multiple threads running, here is the code im using to initialise the threads. Basically I have multiple textFields in javafx, and when a button is clicked on the screen, it fills the textFields, one by one, with an incrementing timer. Now I also have a button for each of the textfields to clear it, but the problem is when I clear it, because the thread is still running, the timer vanishes for a second and comes back because of the line 'orderTimes.get(boxNo).setText(minute + second);' in the code.

Now what I've tried is creating a list of threads and I've tried implementing it below but it doesn't work, this is so I can call each individual thread if its button to clear has been clicked.

Does anyone know how I can close/stop only one single thread out of multiple that are running? If more info is needed just let me know, thanks.

public static void createIncrementingTimer(int boxNo, List<TextField> orderTimes) {
minutesList.set(boxNo, 0);
secondsList.set(boxNo, 0);
state = true;
new Thread(threadList.get(boxNo))  {
  int currentMinutes = 0;
  int currentSeconds = 0;
  public void run() {
    for (;;) {
      if (state = true) {
        try {
          sleep(1000);
          if (secondsList.get(boxNo) > 59) {
            secondsList.set(boxNo, 0);
            currentSeconds = 0;
            minutesList.set(boxNo, currentMinutes + 1);
            currentMinutes++;
          }            
          if (secondsList.get(boxNo) < 10) {
            second = ":0" + Integer.toString(secondsList.get(boxNo));
          } else {
            second = ":" + Integer.toString(secondsList.get(boxNo));                 
          }                            
          secondsList.set(boxNo, currentSeconds + 1);
          currentSeconds++;            
          if (minutesList.get(boxNo) < 10) {
            minute = "0" + Integer.toString(minutesList.get(boxNo));
          } else {
            minute = Integer.toString(minutesList.get(boxNo));
          }
          orderTimes.get(boxNo).setText(minute + second);             
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    }
  }
};
threadList.get(boxNo).start();
}

The code I'm using to clear the textfields is below, with orderTimes being the list of textFields that I'm trying to clear.

public static void eraseBox(int clickedButtonNumber, List<TextArea> orderContentsList, List<TextField> tableNumbers, List<TextField> orderNumbers, List<TextField> orderTimes) {
orderContentsList.get(clickedButtonNumber).setText(null);
  tableNumbers.get(clickedButtonNumber).clear();
  orderNumbers.get(clickedButtonNumber).clear();
  orderTimes.get(clickedButtonNumber).clear();
}
Gunzyboo
  • 42
  • 4
  • 1
    You could have a `HashMap` of type `String` and `Thread` and then, when you press the cancel button, you use the hashmap to resolve the right thread, which you can cancel/kill. – npinti Feb 14 '20 at 12:49
  • The thread needs to be aware that you're trying to stop it. Right now it's not. Have a variable on it or something. – M. Prokhorov Feb 14 '20 at 12:50
  • If you are using `JavaFX`, you should try to avoid using Thread when you can. In a lot of cases, you can use things from the `Animation` API to accomplish these tasks better. – SedJ601 Feb 14 '20 at 14:53
  • Also calling thing like `setText(..)` in a `Thread` will lead to unpredictable code and will most likely cause freezing. – SedJ601 Feb 14 '20 at 14:54
  • https://stackoverflow.com/questions/50626831/how-to-set-up-two-timelines-to-one-app/50627639#50627639 – SedJ601 Feb 14 '20 at 14:55
  • Does this answer your question? [Platform.runLater and Task in JavaFX](https://stackoverflow.com/questions/13784333/platform-runlater-and-task-in-javafx) – SedJ601 Feb 15 '20 at 06:47
  • 1
    Wrap that code in `Platform.runLater(()->{//Code Here!});` – SedJ601 Feb 15 '20 at 06:47

3 Answers3

2

I would suggest you try to avoid Threads. The Animation API is designed to make doing work that would normally be done in a Thread easier. In this example, the IncrementingTimer class consists of two Labels and three Buttons. The Labels are used to show the time. The Buttons are used to control the Timeline. The Timeline is used to increment the Labels value each second or every sixty seconds. I have added three IncrementingTimers to the app.

Main

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

/**
 * JavaFX App
 */
public class App extends Application {

    @Override
    public void start(Stage stage) {        
        var scene = new Scene(new VBox(new IncrementingTimer(), new IncrementingTimer(), new IncrementingTimer()), 640, 480);
        stage.setScene(scene);
        stage.show();
    }

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

IncrementingTimer

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.util.Duration;

/**
 *
 * @author blj0011
 */
final public class IncrementingTimer extends HBox
{
    IntegerProperty secondsCounter = new SimpleIntegerProperty();//Keeps up with seconds
    IntegerProperty minutesCounter = new SimpleIntegerProperty();//Keeps up with minutes

    Label lblSeconds = new Label();//Displays the seconds
    Label lblMinutes = new Label();//Displays the minutes
    Label lblColon = new Label(":");//Display the colon between minutes and seconds

    Button btnPlay = new Button("Play");//Plays the Timeline
    Button btnStop = new Button("Stop");//Stops the Timeline
    Button btnPause = new Button("Pause");//Pauses the Timeline

    Timeline timeline;//Used to run code that changes the Labels. This Timeline runs every one second.

    public IncrementingTimer()
    {
        lblSeconds.textProperty().bind(secondsCounter.asString("%02d"));//Binds the seconds label to the seconds counter. Sets the String to always show two digits. Exmaple 1 is shown as 01.
        lblMinutes.textProperty().bind(minutesCounter.asString("%02d"));//Binds the minutes label to the minutes counter. Sets the String to always show two digits. Exmaple 1 is shown as 01.

        getChildren().addAll(lblMinutes, lblColon, lblSeconds, btnPlay, btnStop, btnPause);

        timeline = new Timeline(new KeyFrame(Duration.seconds(1), (event) -> {//Replace the one with .016 to speed this up for testing purposes.
            secondsCounter.set(secondsCounter.get() + 1);
            if (secondsCounter.get() == 60) {
                secondsCounter.set(0);
                minutesCounter.set(minutesCounter.get() + 1);
                if (minutesCounter.get() == 60) {
                    minutesCounter.set(0);
                }
            }
        }));
        timeline.setCycleCount(Timeline.INDEFINITE);
        btnPlay.setOnAction((event) -> {
            timeline.play();
        });
        btnPause.setOnAction((event) -> {
            timeline.pause();
        });
        btnStop.setOnAction((event) -> {
            timeline.stop();
            secondsCounter.set(0);
            minutesCounter.set(0);
        });

        this.setAlignment(Pos.CENTER);
    }

}
SedJ601
  • 12,173
  • 3
  • 41
  • 59
  • 1
    A small suggestion to make it more concise add all children at once: `getChildren().addAll(btnPlay, btnPause,btnStop, lblMinutes, lblColon, lblSeconds );` – c0der Feb 15 '20 at 07:00
  • Will try and get this to work, in theory it looks like it should be fine with a few changes but will get back to you in a little bit, thanks! – Gunzyboo Feb 15 '20 at 18:23
  • Hey, I'm not too sure how to get this to work within my current code, since I'm using textFields for each of the timers, and I obviously can't use .setText() for timeline objects. Is there any other way to set the current textFields to contain the minutes and the seconds? – Gunzyboo Feb 15 '20 at 19:58
  • 1
    Got the timeline to work with my program so it is able to detect which button is clicked and stops that timer. Thanks! – Gunzyboo Feb 15 '20 at 23:39
2

As recommended and demonstrated by Sedric, use JavaFx Animation tools for the counters.
The following one-file mre demonstrating implementation of counters using two different animation tools.
One uses PauseTransition and uses Timeline, each with its stop button.
(copy-paste the entire code into Timers.java and run)

import java.io.IOException;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.PauseTransition;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;

public class Timers extends Application {

    @Override public void start(final Stage stage) throws IOException {
        VBox root = new VBox(new CounterPane(new TimeLineCounter()), new CounterPane(new PauseTransitionCounter()));
        stage.setScene(new Scene(root));
        stage.show();
    }

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

class CounterPane extends HBox{

    private final Counter counter;
    CounterPane(Counter counter) {
        super(5);
        this.counter = counter; //todo: check not null
        Button stopBtn = new Button("Stop");
        stopBtn.setOnAction(e->stop());
        getChildren().addAll(stopBtn, counter);
    }

    void stop(){
        counter.getAnimation().stop();
    }
}

abstract class Counter extends Label {

    protected int count = 0;
    public Counter() {
        setAlignment(Pos.CENTER); setPrefSize(25, 25);
        count();
    }

    abstract void count();
    abstract Animation getAnimation();
}

class TimeLineCounter extends Counter {

    private Timeline timeline;

    @Override
    void count() {

        timeline = new Timeline();
        timeline.setCycleCount(Animation.INDEFINITE);
        final KeyFrame keyFrame = new KeyFrame(
                Duration.seconds(1),
                event -> {  setText(String.valueOf(count++) );  }
                );
        timeline.getKeyFrames().add(keyFrame);
        timeline.play();
    }

    @Override
    Animation getAnimation() {
        return timeline;
    }
}

class PauseTransitionCounter extends Counter {

    private PauseTransition pauseTransition;

    @Override
    void count() {

        pauseTransition = new PauseTransition(Duration.seconds(1));
        pauseTransition.setOnFinished(event ->{
            setText(String.valueOf(count++) );
            pauseTransition.play();
        });
        pauseTransition.play();
    }

    @Override
    Animation getAnimation() {
        return pauseTransition;
    }
}
c0der
  • 18,467
  • 6
  • 33
  • 65
  • I've used scenebuilder to make my ui instead of coding it in myself, so it may be a little difficult to implement this with what i currently have, may try the Animation api first and if i'm having problems with that i will try this, thanks! – Gunzyboo Feb 15 '20 at 18:24
  • You can use `Counter` and `CounterPane` in an fxml. – c0der Feb 15 '20 at 18:32
0

The if(state=true) should rather be if(state==true) or just if(state), but in fact the for(;;) could do the entire thing as while(state), simply shutting down the thread when you set state=false.

Then, fully stopping the thread could happen as state=false;threadList.get(boxNo).join();, and you can clear the field only after that (since the thread will set it to something in the last step too).


With a simpler approach you could throw away the state, and revert to for(;;), with the twist of having the try-catch() around the loop, outside. This way you can use threadList.get(boxNo).interrupt();threadList.get(boxNo);.join(); to stop the thread, and on top of that it will be immediate, as the sleep() ends immediately when the thread is interrupted.

tevemadar
  • 12,389
  • 3
  • 21
  • 49