java - 创建基于 GUI 的计时器(或秒表)

标签 java swing timer stopwatch chronometer

我花了一些时间来开发一个程序,显示从用户单击开始按钮开始的已用时间或剩余时间,就像秒表或计时器一样,它测量直到您停止并重置为止的时间。测量所用时间的其他示例包括赛车游戏中的单圈时间和其他游戏中的时间限制(以毫秒为单位)。

不过,我遇到了一些麻烦,因为我自己的秒表运行速度与实际时间不同。我的计时器向下或向上运行一秒需要超过一秒的时间。

代码就在这里:(GUI 工作完美;我更关心如何控制值以显示耗时,每经过一秒,时间就会显示在 JLabel 少了一秒。我无法修改传递给 Thread.sleep 的参数,因为它会使计时器变得更糟。)

import javax.swing.*;

import java.awt.event.*;
import java.awt.*;
import java.text.DecimalFormat;
import java.util.concurrent.*;

public class StopwatchGUI3 extends JFrame 
{

    private static final long serialVersionUID = 3545053785228009472L;

    // GUI Components
    private JPanel panel;
    private JLabel timeLabel;

    private JPanel buttonPanel;
    private JButton startButton;
    private JButton resetButton;
    private JButton stopButton;

    // Properties of Program.
    private byte centiseconds = 0;
    private byte seconds = 30;
    private short minutes = 0;

    private Runnable timeTask;
    private Runnable incrementTimeTask;
    private Runnable setTimeTask;
    private DecimalFormat timeFormatter;
    private boolean timerIsRunning = true;

    private ExecutorService executor = Executors.newCachedThreadPool();

    public StopwatchGUI3()
    {
        panel = new JPanel();
        panel.setLayout(new BorderLayout());

        timeLabel = new JLabel();
        timeLabel.setFont(new Font("Consolas", Font.PLAIN, 13));
        timeLabel.setHorizontalAlignment(JLabel.CENTER);
        panel.add(timeLabel);


        buttonPanel = new JPanel();
        buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER));

        startButton = new JButton("Start");
        startButton.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e)
            {
                if (!timerIsRunning)
                    timerIsRunning = true;

                executor.execute(timeTask);
            }
        });
        buttonPanel.add(startButton);

        resetButton = new JButton("Reset");
        resetButton.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e)
            {
                timerIsRunning = false;

                centiseconds = 0;
                seconds = 30;
                minutes = 0;

                timeLabel.setText(timeFormatter.format(minutes) + ":" 
                        + timeFormatter.format(seconds) + "." 
                        + timeFormatter.format(centiseconds));
            }
        });

        buttonPanel.add(resetButton);

        stopButton = new JButton("Stop");
        stopButton.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e)
            {
                timerIsRunning = false;
            }
        });

        buttonPanel.add(stopButton);


        panel.add(buttonPanel, BorderLayout.SOUTH);


        timeFormatter = new DecimalFormat("00");

        timeTask = new Runnable(){
            public void run()
            {
                while(timerIsRunning)
                {
                    executor.execute(incrementTimeTask);

                    try
                    {
                        Thread.sleep(10);
                    }
                    catch (InterruptedException ex)
                    {
                        ex.printStackTrace();
                    }
                 }
            }
        };

        incrementTimeTask = new Runnable(){
            public void run()
            {
                if (centiseconds > 0)
                    centiseconds--;
                else
                {
                    if (seconds == 0 && minutes == 0)
                        timerIsRunning = false;
                    else if (seconds > 0)
                    {
                        seconds--;
                        centiseconds = 99;
                    }
                    else if (minutes > 0)
                    {
                        minutes--;
                        seconds = 59;
                        centiseconds = 99;
                    }
                }

                executor.execute(setTimeTask);
            }
        };

        setTimeTask = new Runnable(){
            public void run()
            {
                timeLabel.setText(timeFormatter.format(minutes) + ":" 
                        + timeFormatter.format(seconds) + "." 
                        + timeFormatter.format(centiseconds));
            }
        };

        timeLabel.setText(timeFormatter.format(minutes) + ":" 
                + timeFormatter.format(seconds) + "." 
                + timeFormatter.format(centiseconds));

        add(panel);

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        setTitle("StopwatchGUI.java");

        pack();
        setVisible(true);
    }

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

必须有另一种方法使计时器与实时同步,就像真正的秒表一样,而不必依赖三个单独的线程,我认为这对于如此大的编程项目来说太多了,但是可以目前为入门级。 (哦,顺便说一句,DecimalFormat 类是像真正的秒表一样正确格式化数字,尽管没有要舍入的小数值。直到现在,在我发布此内容时,存在一个名为 SimpleDateFormat 的文本类。)

换句话说,我希望这个程序只是一个真正的秒表。如果情况并非如此,那么您如何在 Java 游戏中创建或使用秒表?

最佳答案

您将面临的最大问题是如何使各种 Runnable 以一致的速率运行。基本上,没有真正的方法可以知道Executor何时实际执行您提供的任务,因为它有自己的开销。

在这种特殊情况下,我建议将 Activity Thread的数量减少到一个,这可以减少创建和执行其他Thread所涉及的任何额外开销并为您提供最佳控制,使事情尽可能接近您想要的时间。

我不使用Thread,而是使用javax.swing.Timer,主要是因为它很简单并且在 EDT 的上下文中执行,这使得例如,从内部更新 UI 会更安全

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.DecimalFormat;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class StopwatchGUI3 extends JFrame {

    private static final long serialVersionUID = 3545053785228009472L;

    // GUI Components
    private JPanel panel;
    private JLabel timeLabel;

    private JPanel buttonPanel;
    private JButton startButton;
    private JButton resetButton;
    private JButton stopButton;

    // Properties of Program.
    private byte centiseconds = 0;
    private byte seconds = 30;
    private short minutes = 0;

    private DecimalFormat timeFormatter;

    private Timer timer;

    public StopwatchGUI3() {
        panel = new JPanel();
        panel.setLayout(new BorderLayout());

        timeLabel = new JLabel();
        timeLabel.setFont(new Font("Consolas", Font.PLAIN, 13));
        timeLabel.setHorizontalAlignment(JLabel.CENTER);
        panel.add(timeLabel);

        buttonPanel = new JPanel();
        buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER));

        startButton = new JButton("Start");
        startButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {

                timer.start();

            }
        });
        buttonPanel.add(startButton);

        resetButton = new JButton("Reset");
        resetButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {

                timer.stop();

                centiseconds = 0;
                seconds = 30;
                minutes = 0;

                timeLabel.setText(timeFormatter.format(minutes) + ":"
                        + timeFormatter.format(seconds) + "."
                        + timeFormatter.format(centiseconds));
            }
        });

        buttonPanel.add(resetButton);

        stopButton = new JButton("Stop");
        stopButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                timer.stop();
            }
        });

        buttonPanel.add(stopButton);

        panel.add(buttonPanel, BorderLayout.SOUTH);

        timeFormatter = new DecimalFormat("00");

        timer = new Timer(10, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (centiseconds > 0) {
                    centiseconds--;
                } else {
                    if (seconds == 0 && minutes == 0) {
                        timer.stop();
                    } else if (seconds > 0) {
                        seconds--;
                        centiseconds = 99;
                    } else if (minutes > 0) {
                        minutes--;
                        seconds = 59;
                        centiseconds = 99;
                    }
                }
                timeLabel.setText(timeFormatter.format(minutes) + ":"
                        + timeFormatter.format(seconds) + "."
                        + timeFormatter.format(centiseconds));
            }
        });

        timeLabel.setText(timeFormatter.format(minutes) + ":"
                + timeFormatter.format(seconds) + "."
                + timeFormatter.format(centiseconds));

        add(panel);

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        setTitle("StopwatchGUI.java");

        pack();
        setVisible(true);
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                }

                new StopwatchGUI3();
            }
        });
    }
}

我当时也会停止“猜测”。根本无法保证“更新”之间耗时量是准确的。

相反,我会在秒表启动时获取当前时间,并在计时器的每次滴答时,从当前时间中减去它,从而得到已经过去的时间量。然后您可以使用它来确定秒表的当前值应该是多少......

例如...

添加以下实例字段...

private long startTime;
private long runTime = 30000; // 30 seconds...

更新 startButton 以包含捕获开始时间...

startButton.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent e) {

        startTime = System.currentTimeMillis();
        timer.start();

    }
});

然后按如下方式更新计时器...

timer = new Timer(10, new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {

        long now = System.currentTimeMillis();
        long dif = now - startTime;
        if (dif >= runTime) {

            timer.stop();
            dif = runTime;

        }

        dif = runTime - dif;

        long minutes = dif / (60 * 1000);
        dif = Math.round(dif % (60 * 1000));
        long seconds = dif / 1000;
        dif = Math.round(dif % 1000);
        long centiseconds = dif / 10;

        timeLabel.setText(timeFormatter.format(minutes) + ":"
                + timeFormatter.format(seconds) + "."
                + timeFormatter.format(centiseconds));
    }
});

看看Concurrency in Swing了解更多详情

关于java - 创建基于 GUI 的计时器(或秒表),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21011914/

相关文章:

java - 如何使用原生sql获取现有表

java - 如何请求关注 JOptionPane 内的 JPasswordField?

c++ - 保留计时器集合的最佳方法(对象与指针)

android - 用 nanoTime() 循环

java - 使用 CassandraUnit 时出错 : Dataset not found

java - 无法使用 openapi-generator-gradle-plugin 生成接口(interface)

java - 带着 Spring 的烦恼 Swing

java - JPanel 获取 PaintComponent 在输入子项时被调用

c - 在 C 中运行一个任务 N 秒

java - Spring 中的 Cron 表达式