Создание основанного на графическом интерфейсе хронометра (или секундомера)

Я потратил некоторое время на то, чтобы поработать с программой, показывающей истекшее или оставшееся время, с которого пользователь нажимает кнопку запуска, подобно секундомеру или хронометру, который измеряет время до тех пор, пока вы не остановитесь и не выполните сброс. Другими примерами измерения прошедшего времени являются время прохождения круга в гоночных играх и ограничения по времени в миллисекундах в других играх.

Однако у меня возникли некоторые проблемы, потому что мой собственный секундомер работает не с той же частотой, что и фактическое время. Мой таймер работает дольше, чем одна секунда, чтобы запустить одну секунду вниз или вверх.

Код прямо здесь: (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?

2 ответа

Решение

Самая большая проблема, с которой вы столкнетесь, это возможность получить различные Runnableчтобы работать с постоянной скоростью. В принципе, нет никакого реального способа узнать, когда Executor будет фактически выполнять задачу, которую вы предоставляете, так как она имеет свои собственные над головой.

В этом конкретном случае я бы порекомендовал уменьшить количество активных Threadс одним, это уменьшает любые дополнительные накладные расходы, связанные с созданием и выполнением других Threads и предоставляет вам лучший контроль над тем, чтобы заставить вещи работать как можно ближе к желаемому времени.

Вместо использования ThreadЯ бы вместо этого использовал javax.swing.Timerпервичный, потому что он прост и выполняется в контексте EDT, что делает более безопасным обновление пользовательского интерфейса, например, изнутри

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

Я бы тоже перестал "гадать" в то время. Просто нет гарантии, что количество времени, прошедшее между "обновлениями", является точным.

Вместо этого я бы взял текущее время, когда запущен секундомер, и на каждом тике Timerвычтите это из текущего времени, давая вам количество времени, которое прошло. Затем вы можете использовать это, чтобы определить текущее значение секундомера...

Например...

Добавление следующих полей экземпляра...

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 следующее...

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 для более подробной информации

"Должен быть другой способ синхронизировать таймер с реальным временем, как настоящий секундомер, вместо того, чтобы полагаться на три отдельных потока, которых я считаю слишком большим для такого большого проекта программирования".

Вы можете просто использовать javax.swing.Timer, Это довольно легко использовать. Основной конструктор выглядит так

Timer(int duration, ActionListener listener)

Итак, что вы могли бы сделать, это объявить Timer Объект как член класса. Затем в конструкторе инициализируйте его. Что-то вроде этого

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

Вы можете иметь счетчик, который вы увеличиваете в таймере. Также у таймера есть методы stop()start() а также restart() ты можешь использовать.

Если вам нужен более чистый конструктор, вы всегда можете создать внутренний класс слушателя, например

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

Затем просто инициализируйте свой таймер, как это

timer = new Timer(1000, new TimerListener());
Другие вопросы по тегам