Как получить несколько Java потоков для приостановки и возобновления по запросу пользователя? - PullRequest
0 голосов
/ 20 апреля 2020

Я создаю 20-минутное приложение таймера обратного отсчета. Я использую JavaFX SceneBuilder для этого. Таймер состоит из двух меток (одна для минут, одна для секунд - каждая состоит из объекта класса CountdownTimer) и индикатора выполнения (таймер выглядит как this ). Каждый из этих компонентов является отдельным и работает в отдельных потоках одновременно, чтобы предотвратить зависание пользовательского интерфейса. И это работает.

Проблема:

Три потока (minutesThread, secondsThread, progressBarUpdaterThread) Мне нужно иметь возможность приостановить и возобновить регулярные. java классы. Когда пользователь нажимает кнопку воспроизведения (запуска), этот сигнал сигнализирует FXMLDocumentController (класс, который управляет обновлением компонентов в пользовательском интерфейсе) startTimer() для работы с таймером.

Вправо теперь единственная функциональность startTimer() в FXMLDocumentController: пользователь нажимает кнопку воспроизведения (запуска) -> таймер начинает обратный отсчет.

Я хочу, чтобы пользователь мог приостановить и возобновить таймер с этим та же кнопка. Я попытался использовать синхронизацию между классом FXMLDocumentController и другими тремя потоками, но безрезультатно несколькими разными способами (правда, у меня почти нет опыта программирования для параллелизма). Я просто хочу иметь возможность сделать паузу и включить таймер!

Может кто-нибудь предложить мне совет, как go об этом? Заранее спасибо.

startTimer () в FXMLDocumentController. java (используется для запуска таймера обратного отсчета):

@FXML
void startTimer(MouseEvent event) throws FileNotFoundException {
    // update click count so user can switch between pause and start
    startTimerButtonClickCount++;

    // create a pause button image to replace the start button image when the user pauses the timer
    Image pauseTimerButtonImage = new Image(new 
         FileInputStream("/Users/Home/NetBeansProjects/Take20/src/Images/pause2_black_18dp.png"));
    // setting imageview to be used when user clicks on start button to pause it
    ImageView pauseTimerButtonImageView = new ImageView(pauseTimerButtonImage);
    // setting the width and height of the pause image
    pauseTimerButtonImageView.setFitHeight(31);
    pauseTimerButtonImageView.setFitWidth(28);
    // preserving the pause image ratio after resize
    pauseTimerButtonImageView.setPreserveRatio(true);

    // create a start button image to replace the pause button image when the user unpauses the timer
    Image startTimerButtonImage = new Image(new 
          FileInputStream("/Users/Home/NetBeansProjects/
          Take20/src/Images/play_arrow2_black_18dp.png"));
    ImageView startTimerButtonImageView = new ImageView(startTimerButtonImage);
    startTimerButtonImageView.setFitHeight(31);
    startTimerButtonImageView.setFitWidth(28);
    startTimerButtonImageView.setPreserveRatio(true);

    // progressBar updater
    ProgressBarUpdater progressBarUpdater = new ProgressBarUpdater();
    TimerThread progressBarThread = new TimerThread(progressBarUpdater);
    // minutes timer
    CountdownTimer minutesTimer = new CountdownTimer(19);
    TimerThread minutesThread = new TimerThread(minutesTimer);
    // seconds timer
    CountdownTimer secondsTimer = new CountdownTimer(59);
    TimerThread secondsThread = new TimerThread(secondsTimer);

    // bind our components in order to update them
    progressBar.progressProperty().bind(progressBarUpdater.progressProperty());
    minutesTimerLabel.textProperty().bind(minutesTimer.messageProperty());
    secondsTimerLabel.textProperty().bind(secondsTimer.messageProperty());

    // start the threads in order to have them run parallel when the start button is clicked
    progressBarThread.start();
    minutesThread.start();
    secondsThread.start();

    // if the start button was clicked, then we set its graphic to the pause image
    // if the button click count is divisible by 2, we pause it, otherwise, we play it (and change 
    // the button images accordingly).
    if (startTimerButtonClickCount % 2 == 0) {
        startTimerButton.setGraphic(pauseTimerButtonImageView);
        progressBarThread.pauseThread();
        minutesThread.pauseThread();
        secondsThread.pauseThread();

        progressBarThread.run();
        minutesThread.run();
        secondsThread.run();
    } else {
        startTimerButton.setGraphic(startTimerButtonImageView);
        progressBarThread.resumeThread();
        minutesThread.resumeThread();
        secondsThread.resumeThread();

        progressBarThread.run();
        minutesThread.run();
        secondsThread.run();
    }
}

TimerThread (используется для приостанавливать / возобновлять потоки таймера, когда пользователь нажимает кнопку воспроизведения / паузы в пользовательском интерфейсе):

public class TimerThread extends Thread implements Runnable {

public boolean paused = false;
public final Task<Integer> timerObject;
public final Thread thread;

public TimerThread(Task timerObject) {
    this.timerObject = timerObject;
    this.thread = new Thread(timerObject);
}

@Override
public void start() {
    this.thread.start();
    System.out.println("TimerThread started");
}

@Override
public void run() {
    System.out.println("TimerThread class run() called");
    try {
        synchronized (this.thread) {
            System.out.println("synchronized called");
            while (paused) {
                System.out.println("wait called");
                this.thread.wait();
                System.out.println("waiting...");
            }
        }
    } catch (Exception e) {
        System.out.println("exception caught in TimerThread");
    }

}

synchronized void pauseThread() {
    paused = true;
}

synchronized void resumeThread() {
    paused = false;
    notify();
}
}

CountdownTimer. java (используется для создания и обновления минут и секунд таймер обратного отсчета):

public class CountdownTimer extends Task<Integer> {

private int time;
private Timer timer;
private int timerDelay;
private int timerPeriod;
private int repetitions;

public CountdownTimer(int time) {
    this.time = time;
    this.timer = new Timer();
    this.repetitions = 1;
}

@Override
protected Integer call() throws Exception {
    // we will create a new thread for each time unit (minutes, seconds)
    // we start with whatever time is passed to the constructor
    // we have threads devoted to each case so both minutes and second cases can run parallel to each other.
    switch (time) {
        // for our minutes timer
        case 19:
            // first display should be 19 first since our starting timer time should be 19:59 
            updateMessage("19");

            // set delay and period to change every minute of the countdown
            // 60,000 milliseconds in one minute
            timerDelay = 60000;
            timerPeriod = 60000;
            System.out.println("Running minutesthread....");

            // use a timertask to loop through time at a fixed rate as set by timerDelay, until the timer reaches 0 and is cancelled
            timer.scheduleAtFixedRate(new TimerTask() {
                @Override
                public void run() {
                    //check if the flag is divisible by 2, then we sleep this thread 

                    // if time reaches 0, we want to update the minute label to 00
                    if (time == 0) {
                        updateMessage("0" + Integer.toString(time));
                        timer.cancel();
                        timer.purge();
                        // if the time is a single digit, append a 0 and reduce time by 1
                    } else if (time <= 10) {
                        --time;
                        updateMessage("0" + Integer.toString(time));
                        // otherwise, we we default to reducing time by 1, every minute
                    } else {
                        --time;
                        updateMessage(Integer.toString(time));
                    }
                }
            }, timerDelay, timerPeriod);
            // exit switch statement once we finish our work 
            break;

        // for our seconds timer
        case 59:
            // first display 59 first since our starting timer time should be 19:59 
            updateMessage("59");
            // use a counter to count repetitions so we can cancel the timer when it arrives at 0, after 20 repetitions

            // set delay and period to change every second of the countdown
            // 1000 milliseconds in one second
            timerDelay = 1000;
            timerPeriod = 1000;
            System.out.println("Running seconds thread....");

            // use a timertask to loop through time at a fixed rate as set by timerDelay, until the timer reaches 0 and is cancelled
            timer.scheduleAtFixedRate(new TimerTask() {
                @Override
                public void run() {
                    --time;
                    System.out.println("repititions: " + repetitions);
                    // Use a counter to count repetitions so we can cancel the timer when it arrives at 0, after 1200 repetitions
                    // We will reach 1200 repetitions at the same time as the time variable reaches 0, since the timer 
                    // loops/counts down every second (1000ms).
                    // 1200 seconds = 20 minutes * 60 seconds (1 minute)
                    repetitions++;

                    if (time == 0) {
                        if (repetitions == 1200) {
                            // reset repetitions if user decides to click play again
                            repetitions = 0;
                            timer.cancel();
                            System.out.println("repetitions ran");
                        }
                        updateMessage("0" + Integer.toString(time));
                        // reset timer to 60, so it will countdown again from 60 after reaching 0 (since we have to repeat the seconds timer multiple times,
                        // unlike the minutes timer, which only needs to run once
                        time = 60;
                        System.out.println("time == 00 ran");
                    } else if (time < 10 && time > 0) {
                        updateMessage("0" + Integer.toString(time));
                    } else {
                        updateMessage(Integer.toString(time));
                    }
                }
            }, timerDelay, timerPeriod);
            // exit switch statement once we finish our work
            break;
    }

    return null;
}
}

ProgressBarUpdater. java (используется для обновления индикатора выполнения при обратном отсчете таймера обратного отсчета):

public class ProgressBarUpdater extends Task<Integer> {

private int progressBarPeriod;
private Timer timer;
private double time;

public ProgressBarUpdater() {
    this.timer = new Timer();
    this.time = 1200000;
}

@Override
protected Integer call() throws Exception {
    progressBarPeriod = 10;
    System.out.println("Running progressBar thread....");

    // using a timer task, we update our progressBar by reducing the filled progressBar every 9.68 milliseconds 
    // (instead of 10s to account for any delay in program runtime) to ensure that the progressBar ends at the same time our timer reaches 0. 
    // according to its max (1200000ms or 20 minutes)
    timer.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
            time -= 9.68;
            updateProgress(time, 1200000);
            System.out.println("progressBarUpdater is running");
        }
    }, 0, progressBarPeriod);

    return null;
}

@Override
protected void updateProgress(double workDone, double maxTime) {
    super.updateProgress(workDone, maxTime);
}

}

1 Ответ

1 голос
/ 21 апреля 2020

Как я уже упоминал в комментарии, использование фонового потока для этого, пусть наряду с тремя фоновыми потоками, только усложнит реализацию и рассуждение. Было бы лучше использовать API анимации , предоставляемый JavaFX - он асинхронный, но все еще выполняется в потоке приложений JavaFX . И, как уже упоминалось другими, вам нужно только одно значение для представления оставшегося времени и другое значение, представляющее продолжительность. Оттуда вы можете отобразить минуты, секунды и прогресс.

Лично я бы использовал AnimationTimer, поскольку он дает вам метку времени текущего кадра, которую вы можете использовать для расчета сколько времени осталось. Чтобы упростить использование, я бы также обернул AnimationTimer в другом классе и сделал бы, чтобы этот последний класс представлял API, более подходящий для таймеров обратного отсчета. Например:

import java.util.concurrent.TimeUnit;
import javafx.animation.AnimationTimer;
import javafx.beans.NamedArg;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyLongProperty;
import javafx.beans.property.ReadOnlyLongWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleLongProperty;

public class CountdownTimer {

  private static long toMillis(long nanos) {
    return TimeUnit.NANOSECONDS.toMillis(nanos);
  }

  /* *********************************************************************
   *                                                                     *
   * Instance Fields                                                     *
   *                                                                     *
   ***********************************************************************/

  private final Timer timer = new Timer();
  private long cachedDuration;

  /* *********************************************************************
   *                                                                     *
   * Constructors                                                        *
   *                                                                     *
   ***********************************************************************/

  public CountdownTimer() {}

  public CountdownTimer(@NamedArg("duration") long duration) {
    setDuration(duration);
  }

  /* *********************************************************************
   *                                                                     *
   * Public API                                                          *
   *                                                                     *
   ***********************************************************************/

  public void start() {
    if (getStatus() == Status.READY || getStatus() == Status.PAUSED) {
      timer.start();
      setStatus(Status.RUNNING);
    }
  }

  public void pause() {
    if (getStatus() == Status.RUNNING) {
      timer.pause();
      setStatus(Status.PAUSED);
    }
  }

  public void stopAndReset() {
    timer.stopAndReset();
    setStatus(Status.READY);
  }

  /* *********************************************************************
   *                                                                     *
   * Properties                                                          *
   *                                                                     *
   ***********************************************************************/

  private final ReadOnlyObjectWrapper<Status> status = new ReadOnlyObjectWrapper<>(this, "status", Status.READY) {
    @Override protected void invalidated() {
      if (get() == Status.READY) {
        cachedDuration = Math.abs(getDuration());
        setTimeRemaining(cachedDuration);
      }
    }
  };
  private void setStatus(Status status) { this.status.set(status); }
  public final Status getStatus() { return status.get(); }
  public final ReadOnlyObjectProperty<Status> statusProperty() { return status.getReadOnlyProperty(); }

  private final LongProperty duration = new SimpleLongProperty(this, "duration") {
    @Override protected void invalidated() {
      if (getStatus() == Status.READY) {
        cachedDuration = Math.abs(get());
        setTimeRemaining(cachedDuration);
      }
    }
  };
  public final void setDuration(long duration) { this.duration.set(duration); }
  public final long getDuration() { return duration.get(); }
  public final LongProperty durationProperty() { return duration; }

  private final ReadOnlyLongWrapper timeRemaining = new ReadOnlyLongWrapper(this, "timeRemaining") {
    @Override protected void invalidated() {
      setProgress((double) (cachedDuration - get()) / (double) cachedDuration);
    }
  };
  private void setTimeRemaining(long timeRemaining) { this.timeRemaining.set(timeRemaining); }
  public final long getTimeRemaining() { return timeRemaining.get(); }
  public final ReadOnlyLongProperty timeRemainingProperty() { return timeRemaining.getReadOnlyProperty(); }

  private final ReadOnlyDoubleWrapper progress = new ReadOnlyDoubleWrapper(this, "progress");
  private void setProgress(double progress) { this.progress.set(progress); }
  public final double getProgress() { return progress.get(); }
  public final ReadOnlyDoubleProperty progressProperty() { return progress.getReadOnlyProperty(); }

  /* *********************************************************************
   *                                                                     *
   * Static Classes                                                      *
   *                                                                     *
   ***********************************************************************/

  public enum Status {
    READY,
    RUNNING,
    PAUSED,
    FINISHED
  }

  /* *********************************************************************
   *                                                                     *
   * Classes                                                             *
   *                                                                     *
   ***********************************************************************/

  private class Timer extends AnimationTimer {

    private long triggerTime = Long.MIN_VALUE;
    private long pauseTime = Long.MIN_VALUE;
    private boolean pausing;

    @Override
    public void handle(long now) {
      if (pausing) {
        pauseTime = toMillis(now);
        pausing = false;
        stop();
      } else {
        if (triggerTime == Long.MIN_VALUE) {
          triggerTime = toMillis(now) + cachedDuration;
        } else if (pauseTime != Long.MIN_VALUE) {
          triggerTime += toMillis(now) - pauseTime;
          pauseTime = Long.MIN_VALUE;
        }

        long timeRemaining = Math.max(0, triggerTime - toMillis(now));
        setTimeRemaining(timeRemaining);
        if (timeRemaining == 0) {
          setStatus(Status.FINISHED);
          stop();
        }
      }
    }

    @Override
    public void start() {
      pausing = false;
      super.start();
    }

    void pause() {
      if (triggerTime != Long.MIN_VALUE) {
        pausing = true;
      } else {
        stop();
      }
    }

    void stopAndReset() {
      stop();
      triggerTime = Long.MIN_VALUE;
      pauseTime = Long.MIN_VALUE;
      pausing = false;
    }
  }
}

Предупреждение: Во время работы AnimationTimer экземпляр CountdownTimer не может быть собран сборщиком мусора.

Эта реализация интерпретирует значения продолжительности и оставшегося времени как миллисекунды. Кроме того, изменение продолжительности после запуска таймера не действует до тех пор, пока таймер не будет сброшен (т. Е. Вызов stopAndReset()).


Вот пример использования вышеуказанного CountdownTimer в F XML на основе приложения. Обратите внимание, что в примере используются отдельные кнопки для запуска, приостановки, возобновления и сброса таймера. Это отличается от того, что вы описали в своем вопросе, но вы должны быть в состоянии переделать вещи в соответствии с вашими потребностями. Кроме того, в примере представлен способ переключения показа миллисекунды текущей секунды.

App.f xml:

<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.control.CheckBox?>
<?import CountdownTimer?>
<?import CountdownTimer.Status?>

<VBox xmlns="http://javafx.com/javafx/14.0.1" xmlns:fx="http://javafx.com/fxml/1"
      fx:controller="Controller" prefHeight="300" prefWidth="500">

  <fx:define>
    <!-- 90,000ms == 1m 30s -->
    <CountdownTimer fx:id="timer" duration="90000"/>
    <CountdownTimer.Status fx:id="READY" fx:value="READY"/>
    <CountdownTimer.Status fx:id="RUNNING" fx:value="RUNNING"/>
    <CountdownTimer.Status fx:id="PAUSED" fx:value="PAUSED"/>
  </fx:define>

  <ToolBar style="-fx-font: 10pt 'Monospaced';">
    <Button text="Start" disable="${timer.status != READY}" focusTraversable="false"
            onAction="#handleStartOrResumeTimer"/>
    <Button text="Resume" disable="${timer.status != PAUSED}" focusTraversable="false"
            onAction="#handleStartOrResumeTimer"/>
    <Button text="Pause" disable="${timer.status != RUNNING}" focusTraversable="false" onAction="#handlePauseTimer"/>
    <Button text="Reset" disable="${timer.status == READY || timer.status == RUNNING}" focusTraversable="false"
            onAction="#handleResetTimer"/>
    <Separator/>
    <CheckBox fx:id="showMillisBox" text="Show Millis" focusTraversable="false"/>
  </ToolBar>

  <ProgressBar progress="${timer.progress}" maxWidth="Infinity"/>

  <StackPane VBox.vgrow="ALWAYS">
    <Label fx:id="timerLabel" style="-fx-font: bold 48pt 'Monospaced';"/>
  </StackPane>
</VBox>

Контроллер. java:

import java.time.Duration;
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.paint.Color;

public class Controller {

  @FXML private CountdownTimer timer;
  @FXML private CheckBox showMillisBox;
  @FXML private Label timerLabel;

  @FXML
  private void initialize() {
    timerLabel
        .textProperty()
        .bind(
            Bindings.createStringBinding(
                this::formatTimeRemaining,
                timer.timeRemainingProperty(),
                showMillisBox.selectedProperty()));
    timerLabel
        .textFillProperty()
        .bind(
            Bindings.when(timer.statusProperty().isEqualTo(CountdownTimer.Status.FINISHED))
                .then(Color.FIREBRICK)
                .otherwise(Color.FORESTGREEN));
  }

  private String formatTimeRemaining() {
    Duration d = Duration.ofMillis(timer.getTimeRemaining());
    if (showMillisBox.isSelected()) {
      return String.format("%02d:%02d:%03d", d.toMinutes(), d.toSecondsPart(), d.toMillisPart());
    }
    return String.format("%02d:%02d", d.toMinutes(), d.toSecondsPart());
  }

  @FXML
  private void handleStartOrResumeTimer(ActionEvent event) {
    event.consume();
    timer.start();
  }

  @FXML
  private void handlePauseTimer(ActionEvent event) {
    event.consume();
    timer.pause();
  }

  @FXML
  private void handleResetTimer(ActionEvent event) {
    event.consume();
    timer.stopAndReset();
  }
}

Main. java:

import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {

  @Override
  public void start(Stage primaryStage) throws IOException {
    Parent root = FXMLLoader.load(getClass().getResource("/App.fxml"));
    primaryStage.setScene(new Scene(root));
    primaryStage.setTitle("Countdown Timer Example");
    primaryStage.show();
  }
}
...