Run an infinite loop in the backgroung in Tkinter - python

I would like the code to run in the background and to update my GUI periodically. How can I accomplish this?
For example, suppose I want to execute something like this in the background of the GUI code you can see below:
x = 0
while True:
print(x)
x = x + 1
time.sleep(1)
This is the GUI code:
class GUIFramework(Frame):
def __init__(self,master=None):
Frame.__init__(self,master)
self.master.title("Volume Monitor")
self.grid(padx=10, pady=10,sticky=N+S+E+W)
self.CreateWidgets()
def CreateWidgets(self):
textOne = Entry(self, width=2)
textOne.grid(row=1, column=0)
listbox = Listbox(self,relief=SUNKEN)
listbox.grid(row=5,rowspan=2,column=0,columnspan=4,sticky=N+W+S+E,pady=5)
listbox.insert(END,"This is an alert message.")
if __name__ == "__main__":
guiFrame = GUIFramework()
guiFrame.mainloop()

It is a little unclear what your code at the top is supposed to do, however, if you just want to call a function every second (or every the amount of seconds you want), you can use the after method.
So, if you just want to do something with textOne, you'd probably do something like:
...
textOne = Entry(self, width=2)
textOne.x = 0
def increment_textOne():
textOne.x += 1
# register "increment_textOne" to be called every 1 sec
self.after(1000, increment_textOne)
You could make this function a method of your class (in this case I called it callback), and your code would look like this:
class Foo(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.x = 0
self.id = self.after(1000, self.callback)
def callback(self):
self.x += 1
print(self.x)
#You can cancel the call by doing "self.after_cancel(self.id)"
self.id = self.after(1000, self.callback)
gui = Foo()
gui.mainloop()

If you truly want to run a distinct infinite loop you have no choice but to use a separate thread, and communicate via a thread safe queue. However, except under fairly unusual circumstances you should never need to run an infinite loop. Afte all, you already have an infinite loop running: the event loop. So, when you say you want an infinite loop you are really asking how to do an infinite loop inside an infinite loop.
#mgilson has given a good example on how to do that using after, which you should consider trying before trying to use threads. Threading makes what you want possible, but it also makes your code considerably more complex.

Related

Issue synchronizing multiple countdown timers with Python Tkinter

I'll start by saying that I'm completely new to tkinter and I've been trying to wrap my head around a minor issue I'm experiencing - it might be very simple/I'm approaching it incorrectly, but some insight would be very appreciated!
Essentially I have an app with multiple items, each item has a button. When you click a button for an item, it starts a countdown timer by calculating the duration in a function, then using .after(1000, ...) to refresh my label every second.
The issue is, when you click on a button and start a timer, it counts down every second from when the button is clicked. So if you click the next button "off cycle" from the last countdown, the countdown is NOT SYNCED up the way it should be.
Currently it counts down a second from when the button is pressed, instead of updating every "true second", if that makes sense. Any idea how I can approach this?
def handle_growth_time(n, harvest_time):
time_now = datetime.datetime.now()
time_now = time_now.replace(microsecond=0)
crop = allotments[n]
if not crop["reset"]:
if harvest_time >= time_now:
remaining_time = harvest_time - time_now
crop["remaining_label"].configure(text=remaining_time, font='ariel 12', foreground="black")
crop["remaining_label"].after(1000, handle_growth_time, n, harvest_time)
else:
crop["label_name"].configure(background="#86d474")
crop["active"] = False
else:
crop["reset"] = False
crop["active"] = False
I have tried a couple different approaches, such as when I capture time_now I grab time_now.microseconds/1000 and waiting for that before starting the countdown but I think working with time in that way is unreliable because I still can't really capture true :00.00
I also tried resetting all the timers when a new one is started, refreshing their 1 sec cycle, but I did so by going through an array and restarting them - as you can imagine this causes them to be offset still.
I recommend having only a single timekeeper that keeps track of passing seconds. Each timer can have a tick function that is called by this single timer. It is then responsible for updating all timers at the same time so that the update at the same time.
The following example illustrates the point. It's not production quality code, but it illustrates the point: multiple timer objects that all "tick" during a single time loop.
import tkinter as tk
class Timer(tk.Label):
def __init__(self, parent, seconds):
super().__init__(parent, text=str(seconds), width=8)
self.time = seconds
self.running = False
def tick(self):
self.time -= 1
self.configure(text=self.time)
if self.time == 0:
self.running = False
def start(self):
self.running = True
def stop(self):
self.running = False
class Timekeeper:
def __init__(self):
self.timers = []
def new_timer(self, seconds):
t = Timer(root, seconds)
t.pack(side="top", fill="x")
self.timers.append(t)
t.start()
def tick(self):
root.after(1000, self.tick)
# update existing timers
for timer in self.timers:
if timer.running:
timer.tick()
timekeeper = Timekeeper()
root = tk.Tk()
root.geometry("200x600")
button_15 = tk.Button(root, text="New 15 second timer", command=lambda: timekeeper.new_timer(15))
button_30 = tk.Button(root, text="New 30 second timer", command=lambda: timekeeper.new_timer(30))
button_15.pack(side="top")
button_30.pack(side="top")
# add a few timers as an example
timekeeper.new_timer(15)
timekeeper.new_timer(30)
timekeeper.new_timer(45)
timekeeper.new_timer(60)
root.after(1000, timekeeper.tick)
root.mainloop()

Disable click events from queuing on a widget while another function runs

I have a function (A) that is bound to a click event on a Canvas. At some point, function A calls another function (B). The canvas contents may change in function B, so I want it to ignore canvas click events while it's running. I've tried setting a bool variable that is checked when function A is called to False before the function B is called and then resetting it to True when the function B is done, but the event still queues with bad mouse coordinates for the new contents. I've tried re-binding canvas click events to a function (C) that just returns "break" then calling the function B then re-binding canvas click events back to function A. Still queues with bad mouse coordinates for the new contents. I've tried setting the canvas state to "disabled" calling function B then setting the state to "normal". I've tried returning "break" from function A, function B, and from both. I get the same result it still queues with bad mouse coordinates for the new contents. Any suggestions?
the code I'm using for testing options in its current state:
import time
from tkinter import *
class test_form(object):
def __init__(self, master):
self.master = master
self._CreateGUI()
def _CreateGUI(self):
geom = "200x175+0+0"
self.master.geometry(geom)
self.xx=Canvas(bg='#ff0000',width=100,height=75)
self.xx.bind('<ButtonRelease>',self.test_button)
self.xx.pack()
def waste_time(self):
print('time wasted')
time.sleep(10)
return "break"
def test_button(self,event=None):
self.waste_time()
return "break"
def start_gui():
root = Tk()
form_load = test_form(root)
root.mainloop()
if __name__ == "__main__":
start_gui()
The following approach seems to fix your problem. It's likely similar to one you've already tried but with an after() thrown in:
class test_form(object):
def __init__(self, master):
self.master = master
self._CreateGUI()
def _CreateGUI(self):
geom = "200x175+0+0"
self.master.geometry(geom)
self.xx = Canvas(bg='#ff0000', width=100, height=75)
self.xx.pack()
self.notice()
def waste_time(self):
print('time wasted')
time.sleep(10)
def notice(self):
self.xx.bind('<ButtonRelease>', self.test_button)
def ignore(self):
self.xx.bind('<ButtonRelease>', lambda event: "break")
def test_button(self, event):
self.ignore()
self.waste_time()
self.master.after(500, self.notice)

How to interrupt a thread/process by making a correct "Stop" button in Tkinter (Python)?

I want some basics on the problem of making some sort of "Stop" button that in my case terminates the series of beeps:
from tkinter import *
import winsound
from random import randint
class App(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
self.widgets()
def widgets(self):
self.beep = Button(self, text = "Beep", command = play_beep)
self.beep.pack()
self.stop = Button(self, text = "Stop", command = stop_beep)
self.stop.pack()
go_on = True
def play_beep():
count = 10
while go_on == True and count != 0:
winsound.Beep(randint(100, 2500), 200)
count -= 1
def stop_beep():
go_on = False
root = Tk()
app = App(root)
root.mainloop()
When I press the "Beep" button it gets stuck as well as all the GUI until the beeps end. Could anyone tell me how to fix it?
I don't use TKinter, but I believe your button press is not creating a separate thread or process. The reason why your button gets stuck is because your play_beep loop is blocking your GUI execution loop. So we use threading. The thread executes at the same time as your GUI, so you can basically do two things at once (listen for GUI events and play beep noises).
import threading
class App(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.is_playing = False
self.pack()
self.widgets()
def widgets(self):
self.beep = Button(self, text = "Beep", command = self.play_beep)
self.beep.pack()
self.stop = Button(self, text = "Stop", command = self.stop_beep)
self.stop.pack()
def play_beep(self):
self.is_running = True
self.beep_th = threading.Thread(target=self.run)
self.beep_th.start()
def run(self):
count = 10
while self.is_running == True and count != 0:
winsound.Beep(randint(100, 2500), 200)
count -= 1
def stop_beep(self):
try:
self.is_running = False
self.beep_th.join(0)
self.beep_th = None
except (AttributeError, RuntimeError): # beep thread could be None
pass
def closeEvent(self, event): # This is a pyside method look for a TKinter equivalent.
"""When you close the App clean up the thread and close the thread properly."""
self.stop_beep()
super().closeEvent(event)
First off, your question has nothing to do about threads or processes. Tkinter is single-threaded.
If you want to run some function periodically in a tkinter program, you must give the event loop a chance to process events. The typical solution is to do it like this:
def play_beep(count=10):
if go_on and count != 0:
winsound.Beep(randint(100, 2500), 200)
root.after(1000, play_beep, count=1)
This will cause the beep to play every second (1000ms) for ten iterations. In between each call, the event loop will have a chance to process other events.
Now, if the code you are running takes a long time, you're going to have to run that code in a separate thread or process. I know nothing about winsound.Beep so I don't know if that's necessary or not.
Second, to be able to interrupt it, you need to make go_on global, otherwise you're simply setting a local variable that never gets used.
def stop_beek():
global go_on
go_on = False

How to slow down the timer using tkinter

I'm trying to make the timer slow down in a function or even stop the timer when a function is called.
Tried doing .remove .forget but nothing would work.
Any ways to stop/slow down the timer?
from tkinter import *
import time
class App():
def __init__(self):
self.window = Tk()
self.root = Frame(self.window, height=200,width=200)
self.root.pack()
self.root.pack_propagate(0)
self.window.title('Timer')
self.label = Label(text="")
self.label.pack()
self.sec = 11
self.timerupdate()
self.root.mainloop()
def timerupdate(self):
if self.sec!=0:
self.sec-=1
self.label.configure(text=self.sec)
self.root.after(1000, self.timerupdate)
if self.sec == 0:
self.sec = 11
self.slow_time()
def slow_time(self):
self.after.configure(1000000000,self.counting)
app=App()
app.mainloop()
The only timer in the code is when you call self.root.after(1000, self.timerupdate). The 1000 is what controls the speed. It designates how far in the future to call a function, and is represented as a number of milliseconds. If you want to run something every second, use the value 1000. If you want to run something every two seconds, the value is 2000, and every half second would be 500.
So, store the speed in a variable, use that variable when you call self.after, and then you simply change that variable whenever you want to speed up or slow down the timer.

Get rid of a label inside a thread?

so i have this code:
import thread
from Tkinter import *
import random
import time
Admin=Tk()
def moveit(number):
songas=Label(Admin,text=number,bg='red')
def ji():
plad=0.0
recount=0
times=0
while 1:
plad-=0.1
recount+=1
times+=1
time.sleep(0.5)
pls=0.0
pls+=plad
if recount==4:
pls=0
plad=0.0
recount=0
songas.place(relx=pls,rely=0.7)
thread.start_new_thread(ji,())
za=random.random()
button=Button(Admin,text='Press',command=lambda:moveit(str(za)))
button.place(relx=0.2)
Admin.mainloop()
And it starts to move to the left but if you press the 'press' button again it puts some more numbers on top of the old ones.
does any one know how to erase the old numbers to make it so there are only the knew ones?
Tkinter isn't thread safe -- you can't manipulate widgets in any thread except the main one or you'll get undefined results.
You don't need threads for this. Your code adds an infinite loop, but the application already has an infinite loop (the event loop) that you can take advantage of.
If you want to move some item create a function that does two things. First, it does whatever it is you want, such as move the item. Second, it uses the standard after method to call itself again in a short amount of time (for example, half a second or 500ms). This way you let your event loop drive the animation, you don't need threads, and your UI stays responsive.
Here's an example. I doubt it does exactly what you want because I'm not certain of exactly what you want.
import Tkinter as tk
import random
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
self._running = False
self._relx = None
tk.Tk.__init__(self, *args, **kwargs)
self.pack_propagate(False)
self.configure(width=400, height=400)
self.label = tk.Label(self, text="hello, world", background="red")
self.button = tk.Button(self, text="Start", command=self.toggle)
self.button.pack(side="top")
def toggle(self):
'''toggle animation on or off'''
self._running = not self._running
if self._running:
self.button.configure(text="Stop")
self.moveit()
else:
self.button.configure(text="Start")
def moveit(self):
'''Animate the label'''
if not self._running:
# animation has been stopped
# hide the label from view.
self.label.place_forget()
if self._running:
if not self.label.winfo_viewable():
# not visible; establish future locations
self._relx = [.5, .4, .3, .2, .1, 0]
relx = self._relx.pop(0)
self._relx.append(relx)
self.label.place(relx=relx, rely=0.7)
self.after(1000, self.moveit)
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
You must signal the old thread to exit somehow. It is probably easiest to perform with locks - you create a lock when creating a new thread and acquire it. And you release it when the thread is no longer needed. The thread then only needs to check in the main loop whether its lock is still locked - if it isn't it will remove the label and exit. Here the modified version of your code (replace "Remove label here" comment by suitable code):
import thread
from Tkinter import *
import random
import time
Admin=Tk()
lock = None
def moveit(number):
global lock
songas=Label(Admin,text=number,bg='red')
def ji(lock):
plad=0.0
recount=0
times=0
while 1:
plad-=0.1
recount+=1
times+=1
time.sleep(0.5)
pls=0.0
pls+=plad
if recount==4:
pls=0
plad=0.0
recount=0
songas.place(relx=pls,rely=0.7)
if not lock.locked():
# Remove label here
break
if lock:
# Signal old thread to exit
lock.release()
lock = thread.allocate_lock()
lock.acquire()
thread.start_new_thread(ji,(lock,))
za=random.random()
button=Button(Admin,text='Press',command=lambda:moveit(str(za)))
button.place(relx=0.2)
Admin.mainloop()

Categories