Python method strangely on pause until tkinter root is closed - python

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.

Related

Tkinter event based background label updating

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.

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()

Create a python tkinter window with no X (close) button

I'm writing a 'wizard' type Python Tkinter GUI that collects information from the user and then performs several actions based on the user's entries: file copying, DB updates, etc. The processing normally takes 30-60 seconds and during that time, I want to:
Provide the user with text updates on the activity and progress
Prevent the user from closing the app until it's finished what it's doing
I started on the route of having the text updates appear in a child window that's configured to be trainsient and using wait_window to pause the main loop until the activities are done. This worked fine for other custom dialog boxes I created which have OK/cancel buttons that call the window's destroy method. The basic approach is:
def myCustomDialog(parent,*args):
winCDLG = _cdlgWin(parent,*args)
winCDLG.showWin()
winCDLG.dlgWin.focus_set()
winCDLG.dlgWin.grab_set()
winCDLG.dlgWin.transient(parent)
winCDLG.dlgWin.wait_window(winCDLG.dlgWin)
return winCDLG.userResponse
class _cdlgWin():
def __init__(self,parent,*args):
self.parent = parent
self.dlgWin = tk.Toplevel()
self.userResponse = ''
def showWin(self):
#Tkinter widgets and geometry defined here
def _btnOKClick(self):
#self.userResponse assigned from user entry/entries on dialog
self.dlgWin.destroy()
def _btnCancelClick(self):
self.dlgWin.destroy()
However this approach isn't working for the new monitor-and-update dialog I want to create.
First, because there's no user-initiated action to trigger the copy/update activities and then the destroy, I have to put them either in showWin, or in another method. I've tried both ways but I'm stuck between a race condition (the code completes the copy/update stuff but then tries to destroy the window before it's there), and never executing the copy/update stuff in the first place because it hits the wait_window before I can activate the other method.
If I could figure out a way past that, then the secondary problem (preventing the user from closing the child window before the work's done) is covered by the answers below.
So... is there any kind of bandaid I could apply to make this approach work the way I want? Or do I need to just scrap this because it can't work? (And if it's the latter, is there any way I can accomplish the original goal?)
self.dlgWin.overrideredirect(1) will remove all of the buttons (make a borderless window). Is that what you're looking for?
As far as I know, window control buttons are implemented by the window manager, so I think it is not possible to just remove one of them with Tkinter (I am not 100% sure though). The common solution for this problem is to set a callback to the protocol WM_DELETE_WINDOW and use it to control the behaviour of the window:
class _cdlgWin():
def __init__(self,parent,*args):
self.parent = parent
self.dlgWin = tk.Toplevel()
self.dlgWin.protocol('WM_DELETE_WINDOW', self.close)
self.userResponse = ''
def close(self):
tkMessageBox.showwarning('Warning!',
'The pending action has not finished yet')
# ...

Python - Check for keypresses without using 100% cpu

I'm writing a GUI program in Python using Tkinter and I need a way to check if a keypress is happening without using all my cpu. Currently I'm using the threading module to start a thread that will check for the keypress without freezing the interface (Tkinter). I use win32api.GetKeyState() in a while loop inside my thread so that it constantly checks the status of the key because it needs to be able to tell if the key is being pressed even when the window doesnt have focus. The problem is the program uses 100% cpu the moment I start the thread. If I put a time.sleep() in the loop it cuts back the cpu usage dramatically BUT there is a delay between the actual keypress and the time that it knows that you are pressing a key.
Is there a way to capture a keypress the very moment it gets pressed even when the window is out of focus WITHOUT using so much cpu?
from Tkinter import *
import win32api
class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
coords = StringVar()
Label(master=self, textvariable=coords).pack()
def GetCoords():
coords.set(str(win32api.GetCursorPos()))
root.bind_all("<Scroll_Lock>", self.GetCoords)
root = Tk()
app = Application(master=root)
#root.wm_iconbitmap(default='INSERT ICON HERE')
#root.wm_title("TITLE OF PROGRAM")
#app.master.maxsize(640, 480)
app.master.minsize(640, 480)
app.master.resizable(0, 0)
app.mainloop()
app.quit()
That script give me the following result:
AttributeError: Application instance has no attribute 'GetCoords'
You want to catch key events, instead of polling for the current keyboard state.
See Events and Bindings in the TkInter docs, which has a simple example that does exactly what you want (plus, it's cross-platform instead of Win32-only).
And this is generally an issue with all GUI programming (and network servers, for that matter), and the answer is always the same. You don't directly use non-blocking "check for current X status" calls usefully, with or without threads. In the main thread, you ask the event loop "call my function when X status changes", or you create a background thread and make a blocking "wait forever until X happens" call.
The Wikipedia page on Event loop actually has a pretty good description of this.
Looking at your edited version, you've got a completely new problem now:
class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
coords = StringVar()
Label(master=self, textvariable=coords).pack()
def GetCoords():
coords.set(str(win32api.GetCursorPos()))
root.bind_all("<Scroll_Lock>", self.GetCoords)
GetCoords is a local function defined inside Application.__init__. But you're trying to use it as if it were a method of Application. You can't do self.GetCoords unless GetCoords is a method of self. That's exactly what the error message AttributeError: Application instance has no attribute 'GetCoords' means.
But you can just pass the local function GetCoords, just by taking out the self. prefix. I'm not sure this will do what you think (because I'm not sure whether you can close over a StringVar like that or not), but… try it and see.
Alternatively, you can make GetCoords a method just by moving it out of the def __init__ and giving it a self parameter. Then you can access self.GetCoords, and get a bound method, which you can pass exactly as you're trying to. However, in that case, it won't be able to access coords anymore, since that's a local variable inside __init__. To fix that, change that local variable into a member variable, by using self.coords everywhere in __init__ and GetCoords (and anywhere else) instead of coords.

tk destroy() and grid_forget() don't always work

i'm hoping anyone can help me out here. i'm having an issue with a tkinter gui i built. the issue only happens in windows. My GUI creates a results frame with some labels in it, when it's time to calculate something else, the user clicks on the "newPort" button and that button is supposed to remove the results frame and set to False some instance attributes internal to the calculation. The issue i'm having, which is apparent only in windows is that sometimes the results frame, and its descendant labels don't disappear every time. Sometimes they do, sometimes they don't. The instance variable is correctly set to False but the widgets are still visible on the main GUI. The GUI also contains a couple checkboxes and radiobuttons but they don't impact the creation of the results frame nor its expected destruction. I have not been able to pin point a pattern of actions the user takes before clicking on the newPort button which causes the frame and labels to not get destroyed. This happens when i freeze my app with py2exe, as well as running the app from the python interpreter within the eclipse IDE. I have not tried running the app from the python interpreter directly (i.e. without the IDE) and this problem does not happen on my Mac when i run the app using the eclipse python interpreter. Thanks very much all! My code looks like this:
import Tkinter as TK
class widget(object):
def __init__(self,parent=None):
self.parent = TK.Frame(parent)
self.parent.grid()
self.frame = TK.Frame(self.parent)
self.frame.grid()
newLedger = TK.Button(self.parent,command=self.newPort).grid()
self.calcButton = TK.Button(self.frame,command=self.showResults)
self.calcButton.grid()
self.calcVariable = True
def newPort(self):
self.calcVariable = False
try:
self.second.grid_forget()
self.first.grid_forget()
self.resultsFrame.grid_forget()
self.second.destroy()
self.first.destroy()
self.resultsFrame.destroy()
except:
raise
self.frame.update_idletasks()
def showResults(self):
self.resultsFrame = TK.Frame(self.frame)
self.resultsFrame.grid()
self.first = TK.Label(self.resultsFrame,text='first')
self.first.grid()
self.second = TK.Label(self.resultsFrame,text='second')
self.second.grid()
if __name__ == '__main__':
root = TK.Tk()
obj = widget(root)
root.mainloop()
You don't need to destroy or call grid_forget on the labels, and you don't need to call grid_forget on the resultsFrame; when you destroy the resultsFrame it will cause all off its children to be destroyed, and when these widgets are destroyed they will no longer be managed by grid.
The only way I can get widgets to not be destroyed is if I click on the "calc" button twice in a row without clicking on the "new" button in-between. I'm doing this by running your program from the command line.

Categories