The_Fishy
The_Fishy

Reputation: 143

Python tkinter: Countdown Timer not accurate

I made a countdown timer using Tkinter in python, but my only problem is that one second in the timer is a bit longer than a real second.

I used the after() function to, every millisecond, remove one millisecond (0.001 second) from the clock.

I don't know if it's doing that because the code of the clock takes some extra time to execute, if that's the case how could I make a clock with the exact same UI that takes less time to execute?

Here's a Video showing the problem

The program:

from tkinter import *

class root(Tk):
    def __init__(self):
        super(root, self).__init__()

        self.title("Timer")
        
        self.buttonplay = Button(self, text = "Play", fg= 'green', command = self.play)
        self.buttonplay.pack()

        self.buttonpause = Button(self, text = "Pause", fg = "red", command=self.pause)
        self.buttonpause.pack()

        self.createTimers()

    def play(self):
        self.timeit=True
        self.timer1.configure(bg='#1C953D')
        self.doTimer()

    def pause(self):
        self.timeit=False
        self.timer1.configure(bg='#454545')
        
    def reset(self):
        self.timer1.destroy()
        self.createTimers()

    def createTimers(self):
        self.minute = 0
        self.second = 5
        self.ms = 0
        self.total = self.second + self.minute *60 + self.ms*0.001
        self.time1 = StringVar()
        self.time1.set(str(self.minute).rjust(2, '0') + ':' + str(self.second).rjust(2, '0') +'.'+ str(self.ms).rjust(3, '0'))
        self.timer1 = Label(self, textvariable=self.time1, bg='#454545', fg='white', font ="Gadugi 40 bold")
        self.timer1.pack()
        self.timer1.configure(bg='#454545')

    def doTimer(self):
        self.time = self.second + self.minute *60 + self.ms*0.001
        if self.time !=0: #Checks if the timer ended
            if self.timeit:
                self.timer1.configure(bg='#1C953D')
                
                self.ms = self.ms -1
                if self.ms <0:
                    self.second = self.second -1
                    self.ms = 999

                if self.second == -1:
                    self.minute = self.minute -1
                    self.second = 59

                self.time1.set(str(self.minute).rjust(2, '0') + ':' + str(self.second).rjust(2, '0') +'.'+ str(self.ms).rjust(3, '0'))
                
                if self.timeit:
                    self.after(1, self.doTimer)
        else:
            self.ended = 1
            self.timer1.configure(bg='#FF0000')
            self.after(3000, self.reset)


root = root()
root.mainloop()

Upvotes: 3

Views: 387

Answers (1)

LBJ
LBJ

Reputation: 151

I don't know if it's doing that because the code of the clock takes some extra time to execute

In this case you are right, the tempo of your timer is dependent on the runtime of your code. So making this script require more resources from your computer would also slow down the timer, and vice versa.

"Don't reinvent the wheel." - Programming proverb.

Use the time module to get a more accurate time in your timer. More specifically time.time() and format it to make it readable.

Edit:

Here's an example to show your problem.

import time

time_zone = 0 # UTC
while True:
    current_time = time.time()

    ms = (current_time * 1000) % 1000
    s = (current_time % 60)
    m = (current_time % (60*60))//60
    h = (current_time % (60*60*24))//(60*60)

    print(f"h:{int(h+time_zone)} - m:{int(m)} - s:{int(s)} - ms:{int(ms)}")
    time.sleep(1)

In theory this script should print the time exactly every second. However a couple of milliseconds are added due to the code runtime. Removing time.sleep(1) should get you pretty close to a live clock.

So if you want something close to a timer or stopwatch, you get a timestamp for the current epoch time (time.time()) update your timer about every x-amount of ticks per second to get the effect of counting ms, when the button is pressed again you get a new timestamp, and then you compare it to the first timestamp and subtract to get the exact time between each button press.


I've made a template for you to tinker a bit around with:

import tkinter as tk
import time

time_zone = 0  # UTC

# Convert epoch time to readable format
def readable_epoch(time_epoch):
    ms = (time_epoch * 1000) % 1000
    s = (time_epoch % 60)
    m = (time_epoch % (60*60))//60
    h = (time_epoch % (60*60*24))//(60*60)
    return (h, m, s, ms)


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.time1 = 0
        self.time2 = 0

        self.geometry("300x200")
        self.button1 = tk.Button(
            self, text="Click me first", command=self.button1, bg="teal")
        self.button1.pack(ipadx=10, ipady=10, expand=True, side=tk.LEFT)
        self.button2 = tk.Button(
            self, text="Click me second", command=self.button2, bg="pink")
        self.button2.pack(ipadx=10, ipady=10, expand=True, side=tk.LEFT)

    # Get current time
    def button1(self):
        self.current_time = time.time()
        h, m, s, ms = readable_epoch(self.current_time)
        print(f"h:{int(h+time_zone)}, m:{int(m)}, s:{int(s)}, ms:{int(ms)}")

    # Elapsed time since button 1 was pressed:
    def button2(self):
        new_time = time.time()
        new_time = new_time - self.current_time
        h, m, s, ms = readable_epoch(new_time)
        print(f"h:{int(h+time_zone)}, m:{int(m)}, s:{int(s)}, ms:{int(ms)}")


if __name__ == "__main__":
    app = App()
    app.mainloop()

Upvotes: 2

Related Questions