How can I schedule updates (f/e, to update a clock) in tkinter?
Asked Answered
L

8

98

I'm writing a program with Python's tkinter library.

My major problem is that I don't know how to create a timer or a clock like hh:mm:ss.

I need it to update itself (that's what I don't know how to do); when I use time.sleep() in a loop the whole GUI freezes.

Lebensraum answered 8/3, 2010 at 9:16 Comment(1)
A
149

Tkinter root windows have a method called after which can be used to schedule a function to be called after a given period of time. If that function itself calls after you've set up an automatically recurring event.

Here is a working example:

# for python 3.x use 'tkinter' rather than 'Tkinter'
import Tkinter as tk
import time

class App():
    def __init__(self):
        self.root = tk.Tk()
        self.label = tk.Label(text="")
        self.label.pack()
        self.update_clock()
        self.root.mainloop()

    def update_clock(self):
        now = time.strftime("%H:%M:%S")
        self.label.configure(text=now)
        self.root.after(1000, self.update_clock)

app=App()

Bear in mind that after doesn't guarantee the function will run exactly on time. It only schedules the job to be run after a given amount of time. It the app is busy there may be a delay before it is called since Tkinter is single-threaded. The delay is typically measured in microseconds.

Acephalous answered 8/3, 2010 at 12:20 Comment(10)
Will not the recursive calls to itself cause the "maximum recursions for a python object reached" error?Jonson
@SatwikPasani: no, because it's not a recursive call. It merely puts a job on a queue.Acephalous
how to run func only once with delay?Catina
@user924: self.root.after(delay, func).Acephalous
Note @Jonson , trying to bypass .after (by hacking together some usage of threading.Timer()) will result in an increasing number of created threads. Suggest sticking with .after, especially in OOP python.Magus
Is it problematic that it gets the time after exactly 1000ms? Could it miss it by a bit and skip a second and display another one twice? Would it be better to just update more quickly to avoid the skipping seconds problem, like every 100ms or more? like in this answer from Ravikiran D: https://mcmap.net/q/216814/-how-can-i-schedule-updates-f-e-to-update-a-clock-in-tkinterConquer
@xuiqzy: it's not guaranteed to be called at exactly 1000ms, it's only guaranteed that it will be called after 1000ms.Acephalous
@BryanOakley That's clear. My question was: Is it problematic that the update times are not consistent e.g. 50ms after the new second begins everytime but could be 1ms before the next second, then 1ms after the next second ,because it's shifting based on the execution time of update_clock? Isn't it a problem if the formatting of the time just cuts off subsecond precision, because then it could skip a second if it comes too early once and too late the next time? Or am I missing something? If it would round and the schedule is roughly around where the new second begins then it would be fine.Conquer
@xuiqzy: yes, it's a problem that you would need to account for in your code.Acephalous
ok, so what would be the best fix? Just update every 100ms, 30ms or even less ms or try to be juuust after the new second (how to do that for the first update?) and trying to keep drift in check by subtracting the execution time of the callback itself and then hope it still doesn't drift too far? Or is it just not relevant in practice because of some reason?Conquer
B
12

Python3 clock example using the frame.after() rather than the top level application. Also shows updating the label with a StringVar()

#!/usr/bin/env python3

# Display UTC.
# started with https://docs.python.org/3.4/library/tkinter.html#module-tkinter

import tkinter as tk
import time

def current_iso8601():
    """Get current date and time in ISO8601"""
    # https://en.wikipedia.org/wiki/ISO_8601
    # https://xkcd.com/1179/
    return time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())

class Application(tk.Frame):
    def __init__(self, master=None):
        tk.Frame.__init__(self, master)
        self.pack()
        self.createWidgets()

    def createWidgets(self):
        self.now = tk.StringVar()
        self.time = tk.Label(self, font=('Helvetica', 24))
        self.time.pack(side="top")
        self.time["textvariable"] = self.now

        self.QUIT = tk.Button(self, text="QUIT", fg="red",
                                            command=root.destroy)
        self.QUIT.pack(side="bottom")

        # initial time display
        self.onUpdate()

    def onUpdate(self):
        # update displayed time
        self.now.set(current_iso8601())
        # schedule timer to call myself after 1 second
        self.after(1000, self.onUpdate)

root = tk.Tk()
app = Application(master=root)
root.mainloop()
Berck answered 1/12, 2015 at 14:21 Comment(5)
This is a good answer, with one important thing - the time displayed is really the system time, and not some accumulated error time (if you wait "about 1000 ms" 60 times, you get "about a minute" not 60 senconds, and the error grows with time). However - your clock can skip seconds on display - you can accumulate sub-second errors, and then e.g. skip 2 s forward. I would suggest: self.after(1000 - int(1000 * (time.time() - int(time.time()))) or 1000, self.onUpdate). Probably better to save time.time() to a variable before this expression.Maize
I aspire to be awesome enough to embed xkcd's into my comments :)Totality
What is the benefit of using frame.after() instead of root.after()?Brahms
To avoid weird behavior on timezone changes (such as daylight saving time), you should even do time.monotonic() - start_time_monotonic. See https://mcmap.net/q/74048/-how-to-repeatedly-execute-a-function-every-x-seconds for nice and accurate scheduling.Conquer
@TomaszGandor Would it be better to just update more quickly to avoid the skipping seconds problem, like every 100ms or more often? like in this answer from Ravikiran D: https://mcmap.net/q/216814/-how-can-i-schedule-updates-f-e-to-update-a-clock-in-tkinterConquer
S
7
from tkinter import *
import time
tk=Tk()
def clock():
    t=time.strftime('%I:%M:%S',time.localtime())
    if t!='':
        label1.config(text=t,font='times 25')
    tk.after(100,clock)
label1=Label(tk,justify='center')
label1.pack()
clock()
tk.mainloop()
Skeen answered 26/9, 2017 at 13:2 Comment(4)
It would be helpful if you could add some description. Just copy/pasting code is rarely useful ;-)Pris
this code gives the the exact time of the locality.it also serves as a timer.Skeen
It seems to me, it would be better to use "%H" instead of "%I", because "%I" shows only the hours from 0 till 12 and doesn't show whether the time is AM or PM. Or another way is to use both "%I" and "%p" ("%p" indicates AM/PM).Region
Using a lower number than 1s to schedule the updates but then only printing the real time in seconds precision probably avoids some of the problems of other answers if you would slightly miss the second and then skip one, or am I missing something which makes scheduling the next call 1s in the future safe against that kind of bug in other answers? As long as 100 is reasonably high (updated with less than the fps you want in general, updated with 10fps here), I also cannot imagine that this answer has performance problems.Conquer
R
4

You should call .after_idle(callback) before the mainloop and .after(ms, callback) at the end of the callback function.

Example:

import tkinter as tk
import time


def refresh_clock():
    clock_label.config(
        text=time.strftime("%H:%M:%S", time.localtime())
    )
    root.after(1000, refresh_clock)  # <--


root = tk.Tk()

clock_label = tk.Label(root, font="Times 25", justify="center")
clock_label.pack()

root.after_idle(refresh_clock)  # <--
root.mainloop()
Region answered 19/2, 2020 at 8:50 Comment(1)
... just a side note, after is a universal widget method, so it could be called on timer_label as well.Miguelmiguela
C
1

I just created a simple timer using the MVP pattern (however it may be overkill for that simple project). It has quit, start/pause and a stop button. Time is displayed in HH:MM:SS format. Time counting is implemented using a thread that is running several times a second and the difference between the time the timer has started and the current time.

Source code on github

Coachandfour answered 3/6, 2017 at 17:46 Comment(0)
K
1

I have a simple answer to this problem. I created a thread to update the time. In the thread i run a while loop which gets the time and update it. Check the below code and do not forget to mark it as right answer.

from tkinter import *
from tkinter import *
import _thread
import time


def update():
    while True:
      t=time.strftime('%I:%M:%S',time.localtime())
      time_label['text'] = t



win = Tk()
win.geometry('200x200')

time_label = Label(win, text='0:0:0', font=('',15))
time_label.pack()


_thread.start_new_thread(update,())

win.mainloop()
Kasper answered 28/9, 2019 at 9:10 Comment(3)
This code has multitude of problems. The while loop in the update() function is a busy loop. To access the global variable time_label from multiple threads is not great.Blackfellow
but i feel , this is the best way to do it. because this do not reduce the performance of the application.Kasper
tkinter is not threadsafe and you might have weird bugs later with this pattern, see #14168846 Better use scheduling funtcions such as .after().Conquer
C
0
from tkinter import *

from tkinter import messagebox

root = Tk()

root.geometry("400x400")

root.resizable(0, 0)

root.title("Timer")

seconds = 21

def timer():

    global seconds
    if seconds > 0:
        seconds = seconds - 1
        mins = seconds // 60
        m = str(mins)

        if mins < 10:
            m = '0' + str(mins)
        se = seconds - (mins * 60)
        s = str(se)

        if se < 10:
            s = '0' + str(se)
        time.set(m + ':' + s)
        timer_display.config(textvariable=time)
        # call this function again in 1,000 milliseconds
        root.after(1000, timer)

    elif seconds == 0:
        messagebox.showinfo('Message', 'Time is completed')
        root.quit()


frames = Frame(root, width=500, height=500)

frames.pack()

time = StringVar()

timer_display = Label(root, font=('Trebuchet MS', 30, 'bold'))

timer_display.place(x=145, y=100)

timer()  # start the timer

root.mainloop()
Continent answered 18/3, 2020 at 14:9 Comment(0)
W
0

You can emulate time.sleep with tksleep and call the function after a given amount of time. This may adds readability to your code, but has its limitations:

def tick():
    while True:
        clock.configure(text=time.strftime("%H:%M:%S"))
        tksleep(0.25) #sleep for 0.25 seconds
    

root = tk.Tk()
clock = tk.Label(root,text='5')
clock.pack(fill=tk.BOTH,expand=True)
tick()
root.mainloop()
Weapon answered 22/10, 2022 at 9:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.