How to make a count down timer on button when triggered - python

So Im trying to create a count down timer in the form of mm:ss and start counting down in an interval of 1 second when the button is being pressed. Im trying to make the timer show on the button too when counting down. This is what I have so far and I'm not sure what to do.
import tkinter as tk
root = tk.Tk()
monsterTimer = '00:02'
def converter(sec):
m, s = map(int, sec.split(":"))
sec = m * 60 + s
sec -= 1
m, s = divmod(sec, 60)
sec = (f'{m:02d}:{s:02d}')
if sec != '00:00':
sec = 'end'
root.after(1000, converter)
return sec
def update_btn_text():
btn_text.set(converter(monsterTimer))
btn_text = tk.StringVar()
btn_text.set(monsterTimer)
btn = tk.Button(root, textvariable=btn_text, command=lambda: update_btn_text())
btn.place(x=10, y=10)
root.mainloop()

Your solution was close, and using after instead of threads is the right way to go.
The first problem is that when you use root.after, you are calling converter but you aren't passing in an argument. You need to change the line to look like the following, which will call converter(sec) every second.
root.after(1000, converter, sec)
Another problem you have is that you are setting set to 'end' if it is not 00:00. You need to set it only if it is 00:00:
if sec == '00:00':
sec = 'end'
Third, you need to update the button text inside of converter, sometime before calling after:
...
btn_text.set(sec)
root.after(1000, converter, sec)
Finally, you don't need update_btn at all. It can be removed, and you can call converter from your button command.
btn = tk.Button(root, textvariable=btn_text, command=lambda: converter(monsterTimer))
Putting it all together, it looks like this:
import tkinter as tk
root = tk.Tk()
monsterTimer = '00:10'
def converter(sec):
m, s = map(int, sec.split(":"))
sec = m * 60 + s
sec -= 1
m, s = divmod(sec, 60)
sec = (f'{m:02d}:{s:02d}')
if sec == '00:00':
sec = 'end'
btn_text.set(sec)
if sec != "end":
root.after(1000, converter, sec)
btn_text = tk.StringVar()
btn_text.set(monsterTimer)
btn = tk.Button(root, textvariable=btn_text, command=lambda: converter(monsterTimer))
btn.place(x=10, y=10)
root.mainloop()

Try this code:
import tkinter as tk
import time
import threading
root = tk.Tk()
monsterTimer = '00:00'
run_tm = 5
def update_btn_text(sec):
while sec:
m, s = divmod(sec, 60)
m, s = str(m).zfill(2), str(s).zfill(2)
tm = f'{m}:{s}'
btn_text.set(tm)
time.sleep(1)
sec -= 1
btn_text.set("00:00")
def startThread(t):
th1 = threading.Thread(target=update_btn_text, args=(t, ))
th1.start()
btn_text = tk.StringVar()
btn_text.set(monsterTimer)
btn = tk.Button(root, textvariable=btn_text, command=lambda: startThread(run_tm))
btn.place(x=50, y=50)
root.mainloop()

Related

Is there a way to make a function run after a specified amount of time in Python without the after method?

I am trying to create a simple program that tracks a user's clicks per second in Tkinter, but I have no idea how to make the program wait without freezing the program using the after method. The problem is that I need to log the high score after the time finishes, but using this method, the score logs before the click counter goes up. Here is my code:
from tkinter import *
import time
root = Tk()
root.geometry('600x410')
screen = Canvas(root)
h = 6 #button height
w = 12 #button width
c = 0 #counts amount of times clicked
start_btn = 0 #logs clicks of the start button
high_score = 0 #logs the highest score
time = 0
def count_hs():
high_score = c
def remove_time():
global time
time -= 1
def countdown(n):
for i in range(n):
time = n
root.after(1000, remove_time())
#alternatively i tried this:
#time.sleep(1)
#remove_time()
if time <= 0:
b["text"] = "Test done."
break
def start_test():
global start_btn
b["text"] = "Click to begin."
start_btn += 1
print("start button: " + str(start_btn))
def button_click():
global start_btn
global c
c+=1
print("click counter: " + str(c))
#resets the amount of clicks on the large button when the start button is pressed
if c >= 1 and start_btn >= 1:
print("test1")
c = 1
start_btn = 0
if b["text"] == "Click to begin.":
print("test2")
b["text"] = "Click!"
countdown(6)
count_hs()
print("hs: " +str(high_score))
#primary button
b = Button(root, text=" ", font=("Arial", 40), height = h, width = w, command = lambda: button_click())
b.grid(row=0, column=0)
#start button
start = Button(root, text="Start.", command = lambda: start_test())
start.grid(row=0, column=1)
root.mainloop()
Give it a try
from tkinter import *
root = Tk()
root.geometry('600x410')
screen = Canvas(root)
h = 6 # button height
w = 12 # button width
c = 0 # counts amount of times clicked
start_btn = 0 # logs clicks of the start button
high_score = 0 # logs the highest score
time = 0
def count_hs():
global high_score
if c > high_score:
high_score = c
return high_score
def remove_time():
global time
time -= 1
if time > 0:
root.after(1000, remove_time)
else:
show_score()
def start_test():
global start_btn
global c
global time
b["text"] = "Click to begin."
start_btn += 1
print("start button: " + str(start_btn))
# Reset your timer and counter
time = 6
c = 0
def button_click(*args):
global start_btn
global c
# resets the amount of clicks on the large button when the start button is pressed
if c == 0 and start_btn >= 1:
start_btn = 0
b["text"] = "Click!"
root.after(1000, remove_time)
print("hs: " + str(high_score))
else:
c += 1
print("click counter: " + str(c))
def show_score():
global c
score_label.configure(text=str(c))
high_score_label.configure(text=str(count_hs()))
c = 0
b['text'] = ""
# primary button
b = Button(root, text="", font=("Arial", 40), height=h, width=w, command=button_click)
b.grid(row=0, column=0, rowspan=5)
# start button
start = Button(root, text="Start.", command=lambda: start_test())
start.grid(row=0, column=1)
Label(root, text="Your score").grid(row=1, column=1)
score_label = Label(root, text="")
score_label.grid(row=2, column=1)
Label(root, text="High score").grid(row=3, column=1)
high_score_label = Label(root, text="")
high_score_label.grid(row=4, column=1)
root.mainloop()
Few changes:
In count_hs I assume you would update the highscore only if current score beats it.
You can use remove_time as a timer by making it calling itself until time <= 0, in which case you should end your game.
I've used the start button as a resetter, so that when it is clicked it will reset c and time.
On button_click you can now only bother with updating c (and change text at the beginning).
Finally I've added few labels to show the final results, both current and high scores.
Few suggestions to move on:
Instead of global variables you could create a class for the app, it should make it easier for you to exchange info and avoid subtle errors.
You could improve the layout, especially for the newly added labels.
You could make your original timer a parameter (currently, it is set within start_test).
Instead of importing from tkinter import *, I'd suggest you to do something like import tkinter as tk or from tkinter import ... since it increases readability and reduces the sources of errors.

Unable to label text using config in tkinter

So, I have been trying to create a simple stopwatch in tkinter in which I created a loop to update text to new time i.e., the next second in timer label as I click button_1. I tried to do this with StringVar() as well as .config method but none of them are updating the text in label. The code is below
from datetime import *
from time import *
init_time = datetime(100, 1, 1, 0, 0, 0)
running = True
def clock():
while running == True:
sleep(1)
global init_time
a = init_time.strftime("%H:%M:%S")
mtime.set(a)
init_time = init_time + timedelta(seconds=1)
def stop():
global running
running = False
main = Tk()
main.geometry("500x200")
mtime = StringVar()
timer = Label(main, textvariable = mtime, width=30, bg="black", fg="white", font=(25))
timer.place(x=90, y=20)
button_1 = Button(main, text = "Start", command = clock()).place(x=170, y=120)
button = Button(main, text = "Stop", command = stop).place(x=250, y=120)
main.mainloop()
I even tried to convert the init_time to a string because I thought maybe the updates of text work only for strings. The initial GUI window shows but as I click button_1 it doesn't work.
You did common mistake, look at these two lines
button_1 = Button(main, text = "Start", command = clock()).place(x=170, y=120)
button = Button(main, text = "Stop", command = stop).place(x=250, y=120)
Note that you have clock() and stop. First is function invocation, second is function. You should provide function as command. Replace clock() using clock.
Also if you are interested in executing function every n miliseconds, please take look at .after, consider following simple timer
import tkinter as tk
elapsed = 0
def update_timer():
global elapsed
elapsed += 1
timer['text'] = str(elapsed)
root.after(1000, update_timer) # 1000 ms = 1 second
root = tk.Tk()
timer = tk.Label(root, text="0")
btn = tk.Button(root, text="Go", command=update_timer)
timer.pack()
btn.pack()
root.mainloop()

Why does using the time module with tkinter make the app lag so much when going into a new function?

I'm kind of new to stackoverflow so I'm not exactly sure if I'm violating any rules right now but I imagine I'm living close to the edge. Regardless, here is my problem.
I'm trying to create a tkinter countdown timer. Everything was going smoothly and I had an app that should be working but the problem is the app begins lagging crazily once I move to a new function.
This is the code:
import time
import tkinter as tk
root = tk.Tk()
root.geometry("453x170")
def setTimer():
hourEntry = tk.Entry(root, width = 10)
hourEntry.grid(row = 1, column = 1, padx = 20, pady = 30)
col1 = tk.Label(root, text = ':')
col1.grid(row = 1, column = 2)
minuteEntry = tk.Entry(root, width =10)
minuteEntry.grid(row = 1, column = 3, padx = 20, pady = 30)
col2 = tk.Label(root, text = ':')
col2.grid(row = 1, column = 4)
secEntry = tk.Entry(root, width = 10)
secEntry.grid(row = 1, column = 5, padx = 20, pady = 30)
def timer():
try:
hours = int(hourEntry.get())
except ValueError:
hours = 0
try:
minutes = int(minuteEntry.get())
except ValueError:
minutes = 0
try:
seconds = int(secEntry.get())
except ValueError:
seconds = 0
for widget in root.winfo_children():
widget.destroy()
totalsecs = seconds + minutes*60 + hours*60*60
for i in range(totalsecs + 1):
print(hours, ':', minutes, ':', seconds)
if seconds == 0 and minutes > 0:
minutes = minutes - 1
seconds = 60
elif seconds == -1 and minutes == 0 and hours > 0:
hours = hours - 1
seconds = 60
seconds = seconds - 1
result = hours, ':', minutes, ':', seconds
for widget in root.winfo_children():
widget.destroy()
if seconds == 0 and minutes == 0 and hours == 0:
time.sleep(1)
label = tk.Label(root, text = result)
label.grid()
print(hours, ':', minutes, ':', seconds)
return None
label = tk.Label(root, text = result)
label.grid()
time.sleep(1)
button = tk.Button(root, text = 'Start', command = timer)
button.grid(row = 2, column = 3, pady = 30)
setTimer()
root.mainloop()
The setTimer function runs fine and allows you enter the amount of seconds minutes and hours. However, when you click the button to go to the new function, the app lags until the timer is done then it shows the result which of course is "0:0:0". I suspected that constantly clearing all the widgets on the screen causes this but even after removing the widget.destroy() code, it lagged the same just that this time, it would end up on a screen showing every number it went through.
I spent hours removing everything one by one to check its effect on the lag but everything yielded the same lag. However, when I removed the time.sleep(1), there was no lag.
Printing the results into the console doesn't lag. It only lags when the labels are on the tkinter app. All print statements in the code are just me showing that it doesn't lag that way.
Because of this, I'm thinking there is some sort of incompatibility between the time module and tkinter since another project I worked on also lagged a lot when using tkinter and time to the point where it was unusable. So I'm completely stumped.
I'm thinking it could be a case of processing power since I'm programming on a Macbook Air but I don't think the time module should be having such an effect.
Here's how to do something like you want without using the time.sleep() function which interferes with tkinter's mainloop() and doesn't use separate threads which can get tricky because tkinter isn't thread-safe.
Instead it uses the universal widget method after() to schedule periodic calls to a function that counts down the time.
Note also that I've made a number of changes to your code so it follows the PEP 8 - Style Guide for Python Code guidelines and so it's more readable and maintainable.
import tkinter as tk
from tkinter.constants import *
from tkinter.messagebox import showerror, showinfo
def start_timer():
try:
hours = int(hourEntry.get())
except ValueError:
hours = 0
try:
minutes = int(minuteEntry.get())
except ValueError:
minutes = 0
try:
seconds = int(secEntry.get())
except ValueError:
seconds = 0
totalsecs = hours*3600 + minutes*60 + seconds
if totalsecs == 0:
showerror(title='Error', message=f'Please enter some amount of time.')
return
for widget in root.winfo_children():
widget.destroy()
clockLabel = tk.Label(root, text=f'{hours:02}:{minutes:02}:{seconds:02}')
clockLabel.place(relx=0.5, rely=0.5, anchor=CENTER)
countdown(clockLabel, totalsecs) # Start count down process.
def countdown(clockLabel, totalsecs):
if totalsecs == 0:
clockLabel.bell()
clockLabel.config(text='Finished')
return
hours, minutes = divmod(totalsecs, 3600)
minutes, seconds = divmod(minutes, 60)
clockLabel.config(text=f'{hours:02}:{minutes:02}:{seconds:02}')
root.after(1000, countdown, clockLabel, totalsecs-1) # Repeat in 1000 ms.
root = tk.Tk()
root.geometry("453x170")
hourEntry = tk.Entry(root, width=10, justify=RIGHT)
hourEntry.grid(row=1, column=1, padx=20, pady=30)
col1 = tk.Label(root, text=':')
col1.grid(row=1, column=2)
minuteEntry = tk.Entry(root, width =10, justify=RIGHT)
minuteEntry.grid(row=1, column=3, padx=20, pady=30)
col2 = tk.Label(root, text=':')
col2.grid(row=1, column=4)
secEntry = tk.Entry(root, width=10, justify=RIGHT)
secEntry.grid(row=1, column=5, padx=20, pady=30)
startButton = tk.Button(root, text='Start', command=start_timer)
startButton.grid(row=2, column=3, pady=30)
root.mainloop()

How can we use an Entry to set Time on Timer? (Python, tkinter)

I started coding just a few days ago, so I hope you can help me :)
I'm trying to connect an entry with my timer. So user can choose which time they want to set. They can set the value in minutes. I already got it to get the value of the entry in my timer, but the timer won't start. I'm using tkinter to code this.
So if the user enters e.g. "3" my timer will display "03:00" but time won't start running. The console is printing no errors either. Here is my code:
from tkinter import *
from PIL import ImageTk, Image
import math
window = Tk()
window.title("u'Clock")
window.config(padx=50, pady=50, bg="#1a1c20", highlightthickness=0)
timer = None
def start_timer():
count_down(0)
def get_time():
time = input_time_do.get()
int_time = int(time)
return int_time
def count_down(count):
count = get_time()
count_min = count
count_sec = count*60 % 60
if count_min < 10:
count_min = f"0{count_min}"
if count_sec < 10:
count_sec = f"0{count_sec}"
canvas.itemconfig(timer_text, text=f"{count_min}:{count_sec}")
if count > 0:
global timer
window.after(1000, count_down, count - 1)
input_time_do = Entry(width="20", background="DarkSeaGreen")
input_time_do.insert(END, string="Set Workout Time")
input_time_do.grid(column=1, row=2, padx=10, pady=10)
start_button = Button(text="START", command=start_timer)
start_button.grid(column=1, row=6, padx=10, pady=10)
canvas = Canvas(width=400, height=266, bg="#fbf7f0", highlightthickness=0)
gym_img = ImageTk.PhotoImage(Image.open("gym1.jpg")) # PIL solution
canvas.create_image(200, 133, image=gym_img)
timer_text = canvas.create_text(200, 135, text="00:00", fill="#fbf7f0", font=
("Roboto", 30))
canvas.grid(column=1, row=1)
window.mainloop()

How do I make time.sleep() work with tkinter?

I'm trying to show a sequence of numbers on the screen at regular intervals.
I'm new to python so it may be something obvious but I have tried .after and pygame.time.wait, but neither worked.
this is the code:
from tkinter import*
from random import *
import time
my_list = []
def Create_NUM(event):
x = 0
for x in range(level + 2):
button1.destroy()
num = randint(1, 100)
my_list.append(num)
Label(root, text=num,fg="red").pack()
one.pack()
time.sleep(2)
root=Tk()
num = 0
level = 1
bottomFrame = Frame(root)
bottomFrame.pack(side=BOTTOM)
button1 = Button(bottomFrame, text="Click to start game",fg="red")
button1.bind("<Button-1>", Create_NUM)
button1.pack()
root.mainloop()
I assume you want to show new number in place of old number, not below it.
import tkinter as tk
import random
def start():
# hide button
button.pack_forget()
# run `add_number` first time
add_number(level+2)
def add_number(x):
num = random.randint(1, 100)
my_list.append(num)
label['text'] = num
if x > 0:
# repeat after 2000ms (2s)
root.after(2000, add_number, x-1)
else:
# show button again after the end
button.pack()
# --- main ---
my_list = []
level = 1
root = tk.Tk()
label = tk.Label(root)
label.pack()
button = tk.Button(root, text="Click to start game", command=start)
button.pack()
root.mainloop()
Just use this simple command in your function root.update()

Categories