倒数计时器 Java JavaFX

Countdown timer Java JavaFX

提问人:MsLaus 提问时间:11/8/2023 最后编辑:MsLaus 更新时间:11/8/2023 访问量:80

问:

有人可以帮我在 JavaFX 中制作倒数计时器吗?我需要从标签中获取时间(时间由滑块设置),并且我想查看每秒剩余的分钟和秒数。按下按钮时应开始倒计时。我搜索了 Stack Overflow,并尝试使用 ChatGPT 来做到这一点,但我仍然有问题。


import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.io.IOException;
import java.net.URL;
import java.util.*;

public class MainController implements Initializable{

@FXML
private Button button;

@FXML
private Slider slider;

@FXML
private Label timerLabel;

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

//the initial value of the label is set with the slider

slider.valueProperty().addListener((ObservableValue<? extends Number> num, Number oldVal, Number newVal) -> {
            Integer value = Integer.valueOf(String.format("%.0f", newVal));
            timeLabel.setText(String.valueOf(value));

        });

}

//the action of the button

@FXML
private void start(){

//i tried another way and still does not work
timer = seconds;
Timer timerA = new Timer();
TimerTask task = new TimerTask() {
      @Override
      public void run() {
           if(timer >0){
                timeLabel.setText(String.valueOf(timer));
                timer--;
           }
           if(timer == -1){
                 timeLabel.setText(String.valueOf(timer));
                 timerA.cancel();
           }
        }
};
timerA.schedule(task,0,1000);
}
}

Java JavaFX 倒计时 计时器

评论

1赞 deHaar 11/8/2023
如果您编辑问题并包含 ChatGPT 生成的代码,也许我们可以解决问题......
2赞 MsLaus 11/8/2023
@deHaar我刚刚编辑了这篇文章。chatgpt 生成的代码的问题是“时间线可能未初始化”,即使我尝试初始化,我仍然遇到问题
3赞 James_D 11/8/2023
ChatGPT 生成的代码是垃圾(这并不奇怪)。不能保证时间线的事件处理程序每秒都精确执行;它只会在渲染帧时执行,如果已经过去了一秒或更长时间。所以这个计时器往往会运行得很慢。按下开始按钮时,找到当前时间(),计算计时器应该完成的时间()。然后启动更新标签。LocalTime startTime = LocalTime.now()LocalTime endTime = startTime.plusHours(...).plusMinutes(...).plusSeconds(...);AnimationTimer

答:

2赞 VGR 11/8/2023 #1

消息“时间线可能未初始化”表示您正在尝试在构造它之前使用。您正在调用一个事件处理程序,该处理程序正在传递给 Timeline 构造函数;由于构造函数尚未完成,因此编译器不能保证在事件处理程序运行时实际上已为其分配了值。换句话说,可能被初始化。timelinetimeline.stop()timelinetimeline

解决此问题的一种方法是单独添加关键帧。这样,Timeline 构造函数就完全完成了:

Timeline timeline = new Timeline();
// Construction complete;  now timeline can be used in an event handler.

timeline.getKeyFrames().add(new KeyFrame(Duration.seconds(1), event -> {
    // Decrease one second
    if (seconds > 0) {
        seconds--;
    } else {
        // If seconds reach zero, decrement minutes and set seconds to 59
        if (minutes > 0) {
            minutes--;
            seconds = 59;
        } else {
            // If minutes reach zero, decrement hours and set minutes and seconds to 59
            if (hours > 0) {
                hours--;
                minutes = 59;
                seconds = 59;
            } else {
                // If hours, minutes, and seconds reach zero, stop the timer
                hours = 0;
                minutes = 0;
                seconds = 0;
                timeline.stop();
            }
        }
    }
    // Update the label with the new values
    timerLabel.setText(String.format("Time Left: %02d:%02d:%02d", hours, minutes, seconds));
}));

另一种方法是声明为私有字段,而不是局部变量。这是有效的,因为一旦封闭类完成了自己的构造函数,就可以保证每个字段都被初始化。(如果您的代码从未为某个字段赋值,则该字段将自动初始化为 null。timeline

评论

0赞 James_D 11/8/2023
不过,此计时器无法正常工作。不能保证何时调用该事件处理程序:不能假设将倒计时减少一秒是正确的。
0赞 VGR 11/8/2023
@James_D 什么意思?你的意思是它不是一个完美的实时时钟?我认为很多都是可以理解的。
0赞 MsLaus 11/8/2023
@VGR首先,我要感谢你的努力,但它仍然无法正常工作,标签的文字保持不变
0赞 James_D 11/8/2023
我不确定它被理解到什么程度。如果 UI 忙于呈现 UI 的其他部分,则此计时器可能会变慢。即使时机不完美,也不应该发生这种情况。即使在理想情况下,事件处理程序每秒也只调用 60 次,因此在理想情况下,你可以很容易地看到 1-2% 的不准确。
2赞 VGR 11/9/2023
@MsLaus 我很欣赏这种尝试,但您希望在问题中保留原始代码。没有它,所有现有的评论和答案都没有意义。如果你想添加另一个程序,那也没关系。也就是说,请远离 Timer 和 TimerTask。与大多数 GUI 框架一样,JavaFX 要求在一个特定线程中调用其方法。在任何其他线程中调用它们(如 Timer)将导致不可预知的行为。
4赞 James_D 11/8/2023 #2

眼前的问题(“时间线可能未初始化”)由 @VGR 的回答回答。但是,使用这样的方法根本不是创建倒数计时器的方法。问题在于时间线不能保证每秒调用一次事件处理程序;它只会在呈现 UI 时执行,如果至少经过了一秒钟。Timeline

当倒计时开始时,您应该计算它应该结束的时间。然后通过计算距离结束时间的剩余时间来“定期”更新标签。您可以使用时间轴或(每次渲染 UI 时都会调用其方法)来执行此部分。这将增加一些额外的计算时间,但代价是尽可能精确 JavaFX 系统;在这里,我认为计算足够小,这不是问题,并且更喜欢这种方法。AnimationTimerhandle()AnimationTimer

以下是使用此方法的方法:startComputation()

import javafx.animation.Animation;
import javafx.animation.AnimationTimer;
import javafx.animation.KeyFrame;
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.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.time.Duration;
import java.time.LocalTime;

public class CountdownTimer extends Application {
    private int hours;
    private int minutes;
    private int seconds;

    private Label timerLabel = new Label();

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

    @Override
    public void start(Stage primaryStage) {
        VBox root = new VBox(10);
        root.setAlignment(Pos.CENTER);

        TextField hoursField = new TextField();
        hoursField.setPromptText("Hours");
        TextField minutesField = new TextField();
        minutesField.setPromptText("Minutes");
        TextField secondsField = new TextField();
        secondsField.setPromptText("Seconds");

        Button startButton = new Button("Start Timer");
        startButton.setOnAction(event -> {
            // Parse user input for hours, minutes, and seconds
            hours = Integer.parseInt(hoursField.getText());
            minutes = Integer.parseInt(minutesField.getText());
            seconds = Integer.parseInt(secondsField.getText());

            // Start the countdown timer
            startCountdown();
        });

        root.getChildren().addAll(hoursField, minutesField, secondsField,   startButton, timerLabel);

        Scene scene = new Scene(root, 300, 200);
        primaryStage.setTitle("Countdown Timer");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void startCountdown() {
        LocalTime end = LocalTime.now()
                .plusHours(hours)
                .plusMinutes(minutes)
                .plusSeconds(seconds);
        AnimationTimer timer = new AnimationTimer() {
            @Override
            public void handle(long l) {
                Duration remaining = Duration.between(LocalTime.now(), end);
                if (remaining.isPositive()) {
                    timerLabel.setText(format(remaining));
                } else {
                    timerLabel.setText(format(Duration.ZERO));
                    stop();
                }
            }

            private String format(Duration remaining) {
                return String.format("%02d:%02d:%02d",
                        remaining.toHoursPart(),
                        remaining.toMinutesPart(),
                        remaining.toSecondsPart()
                );
            }
        };

        timer.start();
    }
}

如果您更喜欢 ,请将该方法替换为TimelinestartCountdown()

    private void startCountdown() {
        LocalTime end = LocalTime.now()
                .plusHours(hours)
                .plusMinutes(minutes)
                .plusSeconds(seconds);
        Timeline timeline = new Timeline();
        timeline.getKeyFrames().add(new KeyFrame(Duration.seconds(1), e -> {
            java.time.Duration remaining = java.time.Duration.between(LocalTime.now(), end);
            if (remaining.isPositive()) {
                timerLabel.setText(format(remaining));
            } else {
                timerLabel.setText(format(java.time.Duration.ZERO));
                timeline.stop();
            }
        }));
        timeline.setCycleCount(Animation.INDEFINITE);
        timeline.play();
    }

    private String format(java.time.Duration remaining) {
        return String.format("%02d:%02d:%02d",
                remaining.toHoursPart(),
                remaining.toMinutesPart(),
                remaining.toSecondsPart()
        );
    }

评论

0赞 MsLaus 11/8/2023
感谢您的努力,但我仍然有问题,remaining.isPositive() 不起作用。我是否因为这是一个 JavaFX 项目而一直遇到问题?
0赞 James_D 11/9/2023
@MsLaus 第一个代码块包含一个完整的可执行示例(它也是一个 JavaFX 项目,与您的项目相同),用于编译和运行。你说的“不起作用”到底是什么意思?确保您使用的是最新的 JDK(在版本 18 中引入;当前版本为 21)Duration.isPositive()
0赞 MsLaus 11/9/2023
谢谢!最后,代码按照我想要的方式工作。我感谢你的努力!我需要使用 JDK21(我用了 17,因为到目前为止我没有任何问题)并放弃我为按钮编写的方法。