Tkinter event based background label updating - python

My program is doing complex calculation that last for around 30 minutes. I would like to be updated about the values of some variables in a Tkinter GUI with ideally certain variables linked to certain labels so that they update automatically whenever they change their values. While I understand that I could create 2 threads (one for the main program and one for TKinter that calls itself every second or so), I'm wondering if there's a more elegant way to handle this. Any suggestions are appreciated.
The below example shows how it could possibly look like, but the example doesn't work.
import Tkinter as tk
import time
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.counter = tk.Label(self, text="")
self.counter.pack()
# start the clock "ticking"
self.mainLoop()
def mainLoop(self):
for i in range (10000):
j=i^2
self.counter.configure(text=str(j))
time.sleep(0.1)
if __name__== "__main__":
app = SampleApp()
app.mainloop()
However, it does work when the mainLoop is replaced with this:
def mainLoop(self):
now = time.strftime("%H:%M:%S" , time.gmtime())
self.counter.configure(text=now)
# call this function again in one second
self.after(1000, self.mainLoop)
My questions:
Why does it work with "self.after" but not with a for loop
Is there a more elegant way to update labels event driven (not based on a button, but on a calculation that changes certain variables)

To answer first question -
Why does it work with "self.after" but not with a for loop
The __init__() method of a class is called when an object of that class in created. Now in your case, the class gets created in line -
app = SampleApp()
And in your __init__() , you call - self.mainLoop() .
In your for loop case, all this is running in the main thread. And so it would not return from the self.mainLoop() untill it has completed, since you are not start self.mainLoop() as a separate thread, you are doing that in the same thread. Hence, the control would reach the root.mainloop() line only after self.mainLoop()` has completed and returned the control back.
In case of using after() method, it behaves something like it registers an event to be fired after some amount of time, and immediately returns, it does not wait for that much amount of time and neither for the function to return. Hence the control immediately gets returned and it calls root.mainloop() to show the GUI.

Related

Python: How can I delay the execution of second function by one second

So I want to know how I can make a delay between executing two functions. The goal is to replace regular, blank button by black after it was on screen for one second. My current program, simplified looks like this, and it just delays the the execution of CreateInterface():
class Program(Frame):
def __init__(self,root):
self.root=root
self.root.title('Test')
super().__init__(self.root)
self.grid()
self.Start()
return
def Start(self):
startbtn=Button(self,width=5, font=('Calibri',16,'bold'), height=2, text='start',command=lambda:self.CreateInterface())
startbtn.grid(row=1,column=1)
def CreateInterface(self):
time.import
btn1=Button()
btn1.grid(row=1,column=1)
time.sleep(10)
self.Function2(self)
return
def Function2(self):
btn2=Button(bg='black')
btn2.grid(row=1,column=1)
return
In a GUI interface, calling time.sleep makes the whole process wait, so the application appears to freeze. With Tk in Python, a way to do is to use the Tk after method on a window or frame, and then call a function that makes the necessary change to your Button. There are examples of how to do this at How to create a timer using tkinter
Use time.sleep to pause program execution for a certain amount of time. If you wanted to pause for 1 second after calling CreateInterface, change it to this:
def CreateInterface(self):
btn1=Button()
btn1.grid(row=1,column=1)
time.sleep(10)
self.Function2(self)
time.sleep(1)
Don't forget to import time when you do this.

Python method strangely on pause until tkinter root is closed

I am creating a tkinter application using Python 3.4 that collects posts from an API, filter them and allow the user to review them and make a decision for each one (ignore, delete, share, etc.)
The user is expected to pick a date and some pages and then click on the 'Collect' button. The program then fetch the posts from the pages and stock them in 'wholeList'.
When the user clicks on the second button 'Review', the posts must be filtered and passed to the Reviewer.
My problem is that the Reviewer receives no posts at all, and neither does the Filterer. I have added some debugging print() statements at some places, notably to handlerCollect(), and the result baffled me, hence this post.
Instead of finishing the handlerCollect() callback method when I click on 'Collect', the program puts it on hold somewhere between "DEBUG->1" and "DEBUG->2". The main window does not freezes or anything, for I can click on 'Review' and have it print "DEBUG->4" and open up the Reviewer. When I close the main window, "DEBUG->0" "DEBUG->2" and "DEBUG->3" finaly print, along with the rest of the handlerCollect() method executing.
The same behavior happens with handlerChoosePage(), with "DEBUG->0" being delayed until the tkinter root (TK()) is destroyed. My knowledge of structural programming tells me it should be the very first one printed. Instead, it is the very last. My best conclusion is that I must not be ending my Toplevel mainloop()s correctly. I have to admit I have never encountered something like this before. I thought the proper way of ending mainloop()s on Toplevels was with destroy() and I am very confused as to why methods calling mainloop()s get put on hold until the Tk root is destroyed; not really practical.
from GUICollector import GUICollector as Collector
class Launcher(tk.Frame):
def __init__(self, *args, **kwargs):
...
self.allPagesCB = Checkbutton(self.dateFrame, text="Collect for all pages",
variable = self.allPagesVar, command=self.handlerChoosePage)
self.collectBtn = Button(self, text="Collect", command=self.handlerCollect)
self.reviewBtn = Button(self, text="Review", command=self.handlerReview)
def handlerChoosePage(self):
if self.allPagesVar.get() == 0:
child = tk.Toplevel(self)
selector = PageSelector(self.toCollect, child)
selector.pack(side="top", fill="both", expand=True)
selector.mainloop()
print("DEBUG->0")
def handlerCollect(self):
print("DEBUG->1")
self.collect()
print("DEBUG->4")
for post in self.collector.getPosts():
if post not in self.wholeList:
print("...")
self.wholeList.append(post.copy())
self.collector = None
print(len(self.wholeList), "posts in wholeList")
def collect(self):
window = tk.Toplevel(self)
self.collector = Collector(self.toCollect, self.sinceEpoch, window)
self.collector.grid(row=0,column=0)
self.collector.after(500, self.collector.start)
print("DEBUG->2")
self.collector.mainloop() # This is what seems to hang indefinetly
print("DEBUG->3")
def handlerReview(self):
print("DEBUG->5")
print(len(self.wholeList), "posts in wholeList")
filterer = Filterer(self.wholeList)
self.wholeList = filterer.done[:]
window = tk.Toplevel()
reviewer = Reviewer(self.wholeList[:], window)
reviewer.grid(row=0,column=0)
reviewer.mainloop()
The GUICollector module requires no interaction from the user at all.
This module seems to work perfectly: doing its job, displaying it is done and then closing after the specified delay.
Since the GuiCollector mainloop() seems to be the culprit of the hanging, here is how I end it:
class GUICollector(tk.Frame):
def __init__(self, pagesList, since, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
def start(self, event=None):
if some_logic:
self.after(250,self.start)
else:
self.done() # Does get called.
def done(self):
# Some StringVar update to display we are done on screen
self.after(1250, self.validate)
def validate(self):
self.master.destroy()
The PageSelector module is destroyed with the same call on the press of a button: self.master.destroy()
Here is the revelant output of the program:
DEBUG->1
DEBUG->2
=> collected data of page [PageName]
=> Found 3 posts in page
DEBUG->5
0 posts in wholeList
[The main window (Launcher) is manually closed at this point]
DEBUG->3
DEBUG->4
...
...
...
3 posts in wholeList
DEBUG->0
The concept of mainloop assumes that you first create and initialize objects (well, at least these that are required at application start, i.e. not used dynamically), set event handlers (implement interface logic) and then go into infinite event handling (what User Interface essentially is), i.e. main loop. So, that is why you see it as it "hangs". This is called event-driven programming
And the important thing is that this event handling is done in one single place, like that:
class GUIApp(tk.Tk):
...
app = GUIApp()
app.mainloop()
So, the mainloop returns when the window dies.
Until I have some time to refactor my code, I solved the problem by adding the following line to my destroy() calls:
self.quit() # Ends mainloop
self.master.destroy() # Destroys master (window)
I understand this doesn't not solve the bad structure of my code, but it answers my specific question. destroy() doesn't end the mainloop of TopLevels, but quit() does. Adding this line makes my code execute in a predictable way.
I will be accepting #pmod 's answer as soon as I have refactored my code and verified his claim that the Tk() mainloop will cover all child TopLevels.

python time.sleep() delays previous commands

When trying to interrupt a tkinter application, it seems that time.sleep() puts some previous command on the hold. According to my understanding and previous experiences, label1's text should be set to "before" for one second and then changed to "after". However, the "before" value never shows up and "after" gets printed normally one second after execution.
from tkinter import *
import time
class Display(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
label1 = Label(text = "before")
label1.grid()
self.after(1000, label1.config(text = "after"))
def main():
root = Tk()
Display(root)
root.mainloop()
if __name__ == '__main__':
main()
Note that using time.sleep(...) yeilds the same result as the tkinter after(...)
from tkinter import *
import time
class Display(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
label1 = Label(text = "before")
label1.grid()
time.sleep(1)
label1.config(text = "after")
def main():
root = Tk()
Display(root)
root.mainloop()
if __name__ == '__main__':
main()
I suppose tkinter is waiting for something to execute the graphical work (console don't have the problem) but I don't see what and the tkinter doc dosen't tackle that issue.
Is there a simple way to get the obvious expected result?
time.sleep(...) does not give the same result as the after(...) call.
The time.sleep method runs, but does not show anything before the second has passed. You can see this by putting print('before') before your sleep call. You'll see the print statement is executed one second before the window is created. This is due to Tkinter not updating anything during a sleep. So nothing happens until the sleep is over, after which the label is immediately updated. Moreover, the mainloop isn't called before the sleep is over, so Tkinter isn't in its mainloop yet. You can force Tkinter to update by using self.parent.update() right before your sleep call. You will see that the window shows, having the label 'before', waits one second and the label changes to 'after'. However, during this second the window is unresponsive, which is why using sleep together with Tkinter is a bad idea.
Instead of sleep, after is almost always the better option since it returns immediately, so it doesn't block further execution, and schedules the specified function to be executed after the specified amount of time. However, after expects a function name to be passed, but you pass a function call. This means that at the time that the after call is evaluated, the function is run and the return value (which is None) is passed as function name. Therefore, you see that the label is changed at the time the window opens, because the change is made at the time the after call is evaluated. What you should do is pass a function name to after:
def update_label:
label1.config(text = "after")
self.after(1000, update_label)
Or shorter, by creating an anonymous function:
self.after(1000, lambda: label1.config(text = "after"))
This will give you the expected result of showing a label with 'before', which changes to 'after' after a second, without blocking Tkinter's mainloop.
It's not surprising that doesn't work. Think about what the Tk framework is doing: it's creating your windows, display = Display(), and at that moment the thread stops. The object is not fully created. Do you expect Tk to render an object that's half way through its constructor?
First, try moving any thread-related code out of the constructor. The constructor should be allowed to finish normally, and thread-code should be in another function.
Second, it's bad practice to use thread.sleep in the middle of your normal logic. Normally, you start a separate thread, and that thread can wait and sleep if it wants to. (Though I don't know whether Tk has a special way of doing things.)

Destroying a Toplevel in a thread locks up root

I have a program I've been writing that began as a helper function for me to find a certain report on a shared drive based on some information in that report. I decided to give it a GUI so I can distribute it to other employees, and have ran into several errors on my first attempt to implement tkinter and threading.
I'm aware of the old adage "I had one problem, then I used threads, now I have two problems." The thread did, at least, solve the first problem -- so now on to the second....
My watered down code is:
class GetReport(threading.Thread):
def __init__(self,root):
threading.Thread.__init__(self)
# this is just a hack to get the StringVar in the new thread, HELP!
self.date = root.getvar('date')
self.store = root.getvar('store')
self.report = root.getvar('report')
# this is just a hack to get the StringVar in the new thread, HELP!
self.top = Toplevel(root)
ttk.Label(self.top,text="Fooing the Bars into Bazes").pack()
self.top.withdraw()
def run(self):
self.top.deiconify()
# a function call that takes a long time
self.top.destroy() #this crashes the program
def main():
root = Tk()
date,store,report = StringVar(),StringVar(),StringVar()
#####
## labels and Entries go here that define and modify those StringVar
#####
def launchThread(rpt):
report.set(rpt)
# this is just a hack to get the StringVar in the new thread, HELP!
root.setvar('date',date.get())
root.setvar('store',store.get())
root.setvar('report',report.get())
# this is just a hack to get the StringVar in the new thread, HELP!
reportgetter = GetReport(root)
reportgetter.start()
ttk.Button(root,text="Lottery Summary",
command=lambda: launchThread('L')).grid(row=1,column=3)
root.mainloop()
My expected output is for root to open and populate with Labels, Entries, and Buttons (some of which are hidden in this example). Each button will pull data from the Entries and send them to the launchThread function, which will create a new thread to perform the foos and the bars needed to grab the paperwork I need.
That thread will launch a Toplevel basically just informing the user that it's working on it. When it's done, the Toplevel will close and the paperwork I requested will open (I'm using ShellExecute to open a .pdf) while the Thread exits (since it exits its run function)
What's ACTUALLY happening is that the thread will launch its Toplevel, the paperwork will open, then Python will become non-responsive and need to be "end processed".
As far as I know you cannot use Threading to alter any GUI elements. Such as destroying a Toplevel window.
Any Tkinter code needs to be done in the main loop of your program.
Tkinter cannot accept any commands from threads other than the main thread, so launching a TopLevel in a thread will fail by design since it cannot access the Tk in the other thread. To get around this, use the .is_alive method of threads.
def GetReport(threading.Thread):
def __init__(self,text):
self.text = text
super().__init__()
def run(self):
# do some stuff that takes a long time
# to the text you're given as input
def main():
root = Tk()
text = StringVar()
def callbackFunc(text):
top = Toplevel(root)
ttk.Label(top,text="I'm working here!").pack()
thread = GetReport(text)
thread.start()
while thread.is_alive():
root.update() # this keeps the GUI updating
top.destroy() # only when thread dies.
e_text = ttk.Entry(root,textvariable=text).pack()
ttk.Button(root,text="Frobnicate!",
command = lambda: callbackFunc(text.get())).pack()

Using Python Tk as a front-end for threaded code

I know that Python’s Tk interface has some problems when using threads, and I’ve already run into problems with it. My idea is now to use a Queue.Queue to somehow pass events to the Tk mainloop, similarly to the following example.
from Tkinter import *
import Queue
import time
# this queue will be filled by other threads
queue = Queue.Queue()
queue.put("Helloooo!")
queue.put("hi there, everyone!")
class Application(Frame):
def update(self):
while True:
try:
text = queue.get(False)
self.hi_there["text"] = text
time.sleep(3)
except Queue.Empty:
self.quit()
def create_widgets(self):
self.hi_there = Label(self)
self.hi_there["text"] = "Hello"
self.hi_there.pack({"side": "left"})
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
self.create_widgets()
root = Tk()
app = Application(master=root)
app.update()
app.mainloop()
Of course, I must not call update myself (this will execute everything before the UI is even shown) but need Tk to handle that during its mainloop.
Is there any foolproof way to accomplish that with Tk which will not break under certain circumstances? Or should I just resort to Qt for that?
As a general rule of thumb a GUI application should never, ever call sleep, should never have an infinite loop (except for the event loop), and you should never call 'update'. The only exception to never calling update is that is is ok only when you truly understand why you should not.
Create a method that does two things: check the queue, and then use 'after' to call itself after some small period of time. Then, call this method once at the beginning of your program just before starting the event loop, after all other initialization has taken place.
For a working example of such a function, see How to create a timer using tkinter?

Categories