Unable to exit tkinter app when using "wait_variable()" - python

I have a python code that includes tkinter window and other running tasks.
I've been trying to bind "WM_DELETE_WINDOW" event to a function that exits my python code when I close the window but can't achieve that.
This is what I try:
def on_exit():
root.destroy()
sys.exit()
root.protocol('WM_DELETE_WINDOW', on_exit)
The window is destroyed successfully but the python code doesn't exit. Any possible reason for sys.exit() not to work?
What am I doing wrong? any alternative approach should I try?
Doing some testing I figured out what can be the problem.
Here's a small code that summarizes my code which is much bigger.
import tkinter as tk
import sys
root = tk.Tk()
submitted = tk.IntVar()
def on_exit():
root.destroy()
sys.exit()
root.protocol('WM_DELETE_WINDOW', on_exit)
def submit():
submitted.set(1)
print("submitted")
button= tk.Button(root, text="Submit",command=submit)
button.pack()
button.wait_variable(submitted)
root.mainloop()
I believe now that wait_variable is the source of the problem.
And the code actually exits when I added submitted.set(1) to on_exit() ( or if I clicked the button first before closing the window ) but if I tried closing the window without pressing the button, the code won't exit.
So does this mean that wait_variable not only makes tkinter app wait, but also prevents python code exiting?!
I tried os._exit(1) and it worked, but I think it's not clean.

As your updated question points out the problem is wait_variable(). Going off the documentation for this method wait_variable() enters a local event loop that wont interrupt the mainloop however it appears that until that local event loop is terminated (the variable is updated in some way) it will prevent the python instance from terminating as there is still an active loop. So in order to prevent this you have also correctly pointed out you need to update this variable right before you terminate the tk instance.
This might seam odd but it is the behavior I would expect. It is my understanding that an active loop needs to be terminated before a python instance can exit.
As Bryan has pointed out in the comments the wait_variable() method is "a function which calls the vwait command inside the embedded tcl interpreter. This tcl interpreter knows nothing about python exceptions which is likely why it doesn't recognize the python exception raised by sys.exit()"
Link to relevant documentation:
wait_variable()
Relevant text from link:
wait_variable(name)
Waits for the given Tkinter variable to
change. This method enters a local event loop, so other parts of the
application will still be responsive. The local event loop is
terminated when the variable is updated (setting it to it’s current
value also counts).
You can also set the variable to whatever it is currently set as to terminate this event loop.
This line should work for you:
submitted.set(submitted.get())
That said you do not actually need sys.exit(). You can simply use root.destroy().
You new function should look like this:
def on_exit():
submitted.set(submitted.get())
root.destroy()
The python instance will automatically close if there is no more code after the mainloop.

Related

Pause function execution while awaiting input in entry (tkinter)

Let's say I've got a function that requests input. How can I pause the function that calls this function while I am waiting for user input in an entry widget. I tried it with a while loop and time.sleep(sec). Furthermore I executed the function within another thread (so the main thread should not be interrupted), but the problem that always occurs is that the whole program freezes (typing in entry impossible)!
Because I do not have that much experience with Python I am truly stuck.
PS: I am coding on a mac.
The code I used:
import time
_input = []
def get_input()
try:
return _input[0]
except IndexError:
return None
def req():
while get_input() == None:
time.sleep(1)
return get_input()
The function req() is always called within a function which is called via 'getattr()' in a function which parses the input in the entry widget. The variable '_input' automatically gets the user input from the entry. The input I then successfully got from the '_input' variable is then discarded.
Maybe the problem is that the function is running and that is why another function cannot be executed... but shouldn't that be irrelevant if I was using a distinct thread? Why didn't that work...?
Here's a function that creates a simple Tkinter GUI that allows the user to input data into an Entry widget. When the user hits Enter the function gets the current value from the Entry, closes the GUI and returns the value to the calling code. The calling code will block until tkinter_input returns. If the user closes the GUI window with the close button the contents of the Entry are ignored, and None is returned.
import tkinter as tk
def tkinter_input(prompt=""):
root = tk.Tk()
tk.Label(root, text=prompt).pack()
entry = tk.Entry(root)
entry.pack()
result = None
def callback(event):
nonlocal result
result = entry.get()
root.destroy()
entry.bind("<Return>", callback)
root.mainloop()
return result
result = tkinter_input("Enter data")
print(result)
The way to wait for user input is to open up a dialog. A modal dialog will force the user to dismiss the dialog, a non-modal will allow the user to continue to use the main application.
In your case, you can create a dialog using a Toplevel and fill it with any widgets that you want, then use the wait_window function to wait for that window to be destroyed. To make it modal you can create a "grab" on the toplevel. To keep this simple, I have not done that in the following example.
Here is a basic example. The key is the call to wait_window which will not return until the dialog is destroyed.
import tkinter as tk
class CustomDialog(object):
def __init__(self, parent, prompt="", default=""):
self.popup = tk.Toplevel(parent)
self.popup.title(prompt)
self.popup.transient(parent)
self.var = tk.StringVar(value=default)
label = tk.Label(self.popup, text=prompt)
entry = tk.Entry(self.popup, textvariable=self.var)
buttons = tk.Frame(self.popup)
buttons.pack(side="bottom", fill="x")
label.pack(side="top", fill="x", padx=20, pady=10)
entry.pack(side="top", fill="x", padx=20, pady=10)
ok = tk.Button(buttons, text="Ok", command=self.popup.destroy)
ok.pack(side="top")
self.entry = entry
def show(self):
self.entry.focus_force()
root.wait_window(self.popup)
return self.var.get()
To use it, call the show method:
dialog = CustomDialog(root, prompt="Enter your name:")
result = dialog.show()
With the above, result will have the string that you entered.
For more information about creating dialogs, see Dialog Windows on the effbot site,
GUI programming is quite different from normal python scripts.
When you see the GUI pop up, it is already running in the mainloop. That means that your code is only invoked from the mainloop as a callback attached to some event or as a timeout function. Your code actually interrupts the flow of events in the mainloop.
So to keep the GUI responsive, callbacks and timeouts have to finish quickly (say in 0.1 second max). This is why you should not run long loops in a callback; the GUI will freeze.
So the canonical way to do a long calculation in a GUI program is to split it up into small pieces. Instead of e.g. looping over a long list of items in a for loop, you create a global variable that holds the current position in the list. You then create a timeout function (scheduled for running by the after method) that takes e.g. the next 10 items from the list, processes them, updates the current position and reschedules itself using after.
The proper way to get input for a function is to get the necessary input before starting the function. Alternatively, you could use a messagebox in the function to get the input. But in general it is considered good design to keep the "guts" of your program separate from the GUI. (Consider that you might want to switch from Tkinter to the GTK+ or QT toolkits in the future.)
Now onto threads. You might think that using threads can make long-running tasks easier. But that is not necessarily the case. For one thing, the standard Python implementation (we shall call it CPython) has a Global Interpreter Lock that ensures that only one thread at a time can be running Python bytecode. So every time your long-running calculation thread is running, the other thread containing the mainloop is halted. In Python 3 the thread scheduling is improved w.r.t. Python 2 as to try and not starve threads. But there is no guarantee that the GUI thread gets enough runtime when the other thread is doing a ton of work.
Another restriction is that the Tkinter GUI toolkit is not thread safe. So the second thread should not use Tkinter calls. It will have to communicate with the GUI thread by e.g. setting variables or using semaphores. Furthermore, data structures that are used by both threads might have to be protected by Locks, especially if both threads try to modify them.
In short, using threads is not as simple as it seems. Also multithreaded programs are notoriously difficult to debug.

Simple animation with Tkinter Python

I've searched for a simple animation code with Tkinter but I've found very different examples and I can't understand the correct way to write an animation.
Here my working code to display a simple moving circle:
import tkinter as tk
import time
root=tk.Tk()
canvas=tk.Canvas(root,width=400,height=400)
canvas.pack()
circle=canvas.create_oval(50,50,80,80,outline="white",fill="blue")
def redraw():
canvas.after(100,redraw)
canvas.move(circle,5,5)
canvas.update()
canvas.after(100,redraw)
root.mainloop()
In this code I can't correctly understand: how the after method works, where correctly put the update and the move method (before after method ?), is there another way to write an animation code? may you post me another example and comment the code please?
Thanks :)
Calling update
You should not call canvas.update(). As a general rule of thumb you should never call update. For a short essay on why, see this essay written by one of the original developers of the underlying tcl interpreter.
If you take out the call to canvas.update(), you have the proper way to do animation in a tkinter program.
Calling after to start the animation
You don't need to call after immediately before calling root.mainloop(). This works just as well:
...
redraw()
root.mainloop()
The choice to use or not use after in this specific case is dependent on if you want the animation to start immediately (possibly even before the widget is visible) or if you want it to happen after a short delay (possibly after the widget is made visible)
How after works
mainloop is nothing more than an infinite loop that checks the event queue for events. When it finds an event, it pops it off of the list and processes it. after is nothing more than making a request that says "in 100 ms, please add a new event to the queue". When the time limit expires, an event is added to the queue that says, in effect, "run this command". The next time the loop checks for events, it sees this event, pulls it off of the queue, and runs the command.
When you call after from within a method that itself was called by after, you're saying in effect "wait 100ms and do it again", creating an infinite loop. If you put the call to after before moving the object, you're saying "every 100ms run this function". If you put it after you're saying "run this function 100 ms after the last time it was run". The difference is very subtle and usually not perceptible unless your function takes a long time to run.
my code is:
from tkinter import *
import time
tk = Tk()
płótno = Canvas(tk, width=500, height=500)
płótno.pack()
płótno.create_polygon(10,10,10,70,70,10,fill="blue",outline="black")
for x in range(0,51):
płótno.move(1,5,0)
płótno.update()
rest(0.05)
płótno means canvas

Tkinter: Check if root has been destroyed?

I am writing an application using Tkinter along with threading.
The problem I got is, after closing the main app, some thread is still running, and I need a way to check whether the root windows has been destroyed to avoid the TclError: can't invoke "wm" command.
All methods I know: wminfo_exists() and state() all return error once the root is destroyed.
I will add my workaround for this, in case anyone came across the same issue. I was following the suggestion from here. I intercept the windows' closing event to set my flag that marks the root is already dead, and check for that flag when I need.
exitFlag = False
def thread_method():
global root, exitFlag
if not exitFlag:
// execute the code relate to root
def on_quit():
global exitFlag
exitFlag = True
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_quit)
If you are using something like this:
import Tkinter
root = Tkinter.Tk()
root.bind('<space>', lambda e: root.quit()) # quitting by pressing spacebar
root.mainloop()
and not: root.destroy() then the quit method will kill the Tcl interpreter not just breaks out from the mainloop and deletes all widgets. So once you called root.quit() you can be sure, that your root is completely dead!
All the other methods you suggested (like: wminfo_exists()) are only available when at least one valid Tk exists.
NOTE:
If you are using more than one mainloop, you should use the destroy method to make sure, that your main mainloop won't be killed -- but I don't think this is your case.

Tkinter Keyboard Binds

I'm working on an interface using Tkinter and the canvas widget, and so far have found answers to issues I have had from others questions and the answers posted, but I am stumped on this one.
I have several keyboard binds in the class where my GUI elements are created, and they all work fine when the program is started. The binds looks something like this:
self.canvas.get_tk_widget().bind("<Control-o>",self.flash_open)
and are within the __init__ function of the class. As of yesterday, I initialized this class
to start the program, then waited for the user to select open from a menu, which then opened (among other things) a tkmessagebox
self.specfilename =askopenfilename(filetypes=[("spec", "")],initialdir= self.pathname)
With this filename I am able to retrieve my required variable names from a certain filetype (inconsequential to the problem). Today I modified the __init__ function to call the open function when the program starts. Since nothing else can be done until this file is opened, it would make sense to open it first thing. Once the file is selected and the Tkmessagebox is closed, the root window is active, but none of the keyboard binds work. My functions still work using the menu/buttons assigned to them, just not the binds. I have tried binding the shortcuts to the root, with the same result, and am now thinking it may be an issue with the order I am calling them
def __init__(self):
...
self.openfile() #calls the tkmessagebox
self.root.mainloop() #starts gui
I had actually run into this issue before, where a toplevel() instance was closed/destroyed and disabled the binds of the parent window. There isn't any error message to speak of, the binds just don't do anything. I should also mention I have tried to focus on the root window again using
self.openfile()
self.root.mainloop()
self.root.focus_set()
I got around it before by using the wm_withdraw() and wm_deiconify() functions to simply hide the child window, then close it after the program is complete. This fix is a little more difficult to apply in this case however. If anyone can shed some light on the cause of the problem I'd appreciate it.
Edit:
I've written up a runable code segment to show exactly what my issue is.
import os
from tkFileDialog import askopenfilename
from Tkinter import *
class Start:
def __init__(self):
self.root = Tk()
self.root.title('Binding Troubles')
menubar = Menu(self.root)
#add items and their commands to the menubar
filemenu = Menu(menubar, tearoff=0)
filemenu.add_command(label="Do work", command=self.do_work)
filemenu.add_command(label="Open File",command=self.openfile)
menubar.add_cascade(label="File", menu=filemenu)
#bind control-o to perform the do work function
self.root.bind("<Control-o>",self.flash_do_work)
self.root.bind("<Control-O>",self.flash_do_work)
#add the menubar to the GUI
self.root.config(menu=menubar)
#initially open a tkdialog to open a file
self.openfile()#comment out this line to make the bind work
self.root.focus()#also tried self.root.focus_set()
self.root.mainloop()
def flash_do_work(self,event):
#indirect tie to the do_work() function, I'm don't know a
#proper way to make functions handle calls from both events and non-events
self.do_work()
def openfile(self):
#gets current path
self.pathname = os.getcwd()
#Requests filename using a tkdialog
self.filename =askopenfilename(initialdir= self.pathname)
print self.filename
def do_work(self):
#placeholder for actual function; shows whether the bind is working or not
print "work"
Start()
The bind will work if self.openfile() is removed from __init__, and used only from the menu
Another Edit: I've updated the example again, giving a menu option to run the openfile() function. I noticed that if openfile() is called in __init__, the bind will not work. But if next the openfile function is called again, this time manually from the menu, the bind will start working again. Not exactly sure what to take from this. Also, my apologies for the post getting so long.
Change
self.openfile()
to
self.root.after(1, self.openfile)
This moves the call to askopenfilename into the main event loop. Having it outside the main event loop is somehow clobbering your event bindings.
I had this kind of problem a couple of times and it took quite a while until I found a solution I was comfortable with. As #Steven Rumbalski suggests I tried with delaying the application, which works but seems shaky.
Then I found the functions for waiting until something is complete, in this case wait_visibility(widget). This will delay execution until the widget is visible, which seems to be the thing to be waiting for. Try this:
self.root.wait_visibility(self.root) # Wait for root to be displayed
self.openfile()
Now; I'm not sure why this is so, and it seems that there may be differences depending on platform: Tkinter window event . This has nevertheless worked for me on Windows10 and Python 3.10.5.

Update a Tkinter text widget as it's written rather than after the class is finished

I'm in a bind, since this is being written on a classified machine I am unable to copy+paste here. Being somewhat a novice, my approach is probably unorthodox.
I have a GUI written in Tkinter with several buttons. Each button is linked to a class that, in effect, runs a short script. When the button is clicked, I inititalize a class log_window which is simply a Tkinter text widget. I then create a global variable linking log to the log_window I just created, and as the script runs I pipe sys.stdout/stderr to log (I created a write method specifically for this). Everything is kosher, except that the log_window text widget doesn't update with my piped stdout until after the class calling it is finished. However, if I simply print within the class, it will print in the order it is called.
Example
import Tkinter
from Tkinter import *
import time
class log_window:
def __init__(self,master):
self.textframe = Tkinter.Frame(master)
self.text = Text(self.textframe)
self.text.pack()
self.textframe.pack()
def write(self,text):
self.text.insert(END,text)
class some_func1: # This effectively waits 5 seconds then prints both lines at once
def __init__(self,master):
log.write("some text")
time.sleep(5)
log.write("some text")
class some_func2: # This prints the first object, waits 5 seconds, then prints the second
def __init__(self,master):
print "some text"
time.sleep(5)
print "some text"
if __name__ == '__main__':
global log
root = Tk()
log = log_window(root)
root.after(100,some_func1, root)
root.after(100,some_func2, root)
root.mainloop()
Sorry if my example is a little bit muffed, but I think it makes the point. The piping I do is through Popen and some system calls, but they aren't part of the issue, so I only highlighted what, I presume, is the LCD of the issue.
I don't know the details of Tkinter's concurrency, but fiddling around reveals that if you put
master.update_idletasks()
after each call to log.write, it updates on cue. You could give log a .flush() method to do that (like file handles have), or you could just make log.write call it after writing.
When you call sleep it causes your whole GUI to freeze. You must remember that your GUI runs an event loop, which is an infinite loop that wraps all your code. The event loop is responsible for causing widgets to redraw when they are changed. When a binding is fired it calls your code from within that loop, so as long as your code is running, the event loop can't loop.
You have a couple of choices. One is to call update_idletasks after adding text to the widget. This lets the event loop service "on idle" events -- things that are schedule to run when the program isn't doing anything else. Redrawing the screen is one such event, and there are others as well.
The other option is to run your functions in a thread or separate process. Because Tkinter isn't thread safe, these other threads or processes can't directly communicate with the GUI. What they must do is push a message onto a queue, and then your main (GUI) thread must poll the queue and pull messages off. It would be easy to build this code into your log class, and polling the queue can be done using the event loop -- just write a method that pulls messages off the queue and inserts them into the widget, the calls itself using after a few hundred milliseconds later.
You have to update your widget content by adding self.text.update() after self.text.insert(END,text)

Categories

Resources