Creating a GUI-based Chronometer (or Stopwatch)

断了今生、忘了曾经 提交于 2019-12-02 08:29:08

The biggest problem you would face is being able to get the various Runnables to run at a consistent rate. Basically, there's no real way to know when the Executor will actual execute the task you provide it, as it has it's own over heads.

In this particular case, I would recommend reducing the number of active Threads to one, this reduces any additional overheads involved in the creation and execution of other Threads and provides you with the best control over getting things to work as close to the time you want as possible.

Instead of using a Thread, I would instead, use a javax.swing.Timer, primary because it's simple and is executed within the context of the EDT which makes it safer to update the UI from within, for example

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();
            }
        });
    }
}

I would also stop "guessing" at the time. There simply is no guarantee that the amount of time passed between "updates" is accurate.

Instead, I would grab the current time when the stop watch is started and on each tick of the Timer, subtract it from the current time, giving you the amount of time that has passed. You can then use that to determine what the current value of the stop watch should be...

For example...

Adding the following instance fields...

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

Updating the startButton to include capturing the start time...

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

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

    }
});

And then updating the Timer as follows...

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));
    }
});

Take a look at Concurrency in Swing for more details

"There's gotta be another way to make the timer in sync with real time, just like a real stopwatch, instead of having to rely on three separate threads, which I think are too many for such a large programming project,"

You can just use a javax.swing.Timer. It's fairly easy to use. The basic constructor looks like this

Timer(int duration, ActionListener listener)

So what you could do is declare a Timer object as a class member. Then in the constructor, initialize it. Something like this

public Constructor() {
    timer = new Timer(1000, new ActionLisentener(){
        public void actionPerformed(ActionEvent e) {
            // do something
        }
    });
}

You can have a count that you increment in the timer. Also the timer has methods stop() start() and restart() you can use.

If you want a cleaner looking constructor, you could always create an inner listener class like

private class TimerListener implements ActionListener {
    public void actionPerformed(ActionEvent e) {
        // do something
    }
}

Then just initialize your timer like this

timer = new Timer(1000, new TimerListener());
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!