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.
Related
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.
Suppose I have Python code like this
# <Pure Python statement A>
root = tk.Tk()
mainframe = tk.Frame(root)
# <Pure Python statement B>
# <other tkinter code>
root.mainloop()
Which statements are then ending up on tkinter's mainloop? Is it just the 3 tkinter statements?
EDIT
There must be more things going on, because some code between the tkinter code is affected: When I run the following code (taken from another question)
import tkinter as tk
import tkinter.filedialog
filename = ""
def op():
global filename
filename =tk.filedialog.askopenfilename()
root = tk.Tk()
mainframe = tk.Frame(root)
mainframe.grid(column=0, row=0)
tk.Button(mainframe, text="Open file", command=op).grid(column=0, row=1)
root.mainloop()
print(filename)
after closing the program the selected filename is displayed. But when running
import tkinter as tk
import tkinter.filedialog
filename = "this_is_a_test"
def op():
global filename
filename =tk.filedialog.askopenfilename()
root = tk.Tk()
mainframe = tk.Frame(root)
mainframe.grid(column=0, row=0)
tk.Button(mainframe, text="Open file", command=op).grid(column=0, row=1)
print(filename)
root.mainloop()
after closing the program, nothing is displayed. So somehow the pure Python statements before mainloop seem to get absorbed.
What kind of code does tkinter's mainloop collect?
It doesn't collect anything. It simply processes events, and calls functions bound to those events. It also calls functions added to the queue via after.
Which statements are then ending up on tkinter's mainloop? Is it just the 3 tkinter statements?
Nothing "ends up on tkinter's mainloop". That's a nonsensical statement, nothing can end up on it. It is just a function that processes events, and doesn't return until the window is destroyed. All code before the call to mainloop executes according to the normal rules of python.
Calling mainloop is effectively the same as if you put this in its place (but it is much more efficient):
while True:
self.update()
Much like with the above, any code after mainloop() will not execute until the loop exits, which happens when the window has been destroyed.
The reason your print seems to work after the call to mainloop but not before is simply that before mainloop, filename is the empty string. The print run normally, it's just that there's nothing to print. That print statement happens a few milliseconds after the program starts, way before the user has a chance to do anything. When called after, it seems to work because that code doesn't run until the window has been destroyed. At that point it presumably has a value, so you see something printed.
The simple answer is: There's no kind of code that mainloop collects.
It 'collect' s all configuration that is related to the Tcl interpreter it is a method of. As in if your GUI is a configuration of root = tk.Tk(), and the mainloop is a method of root then all configurations under it will be accounted for such as children widgets and their configurations.
Your print statement doesn't get absorbed. It simply prints what would've been printed if the button was never used. Try the 2nd code with simply closing the GUI without using the button. mainloop doesn't absorb anything. It simply waits for events for the GUI configured.
I am using python3 on a mac and run scripts with the IDLE which comes automatically with the python3 installation.
I am trying to make an alert to the user and found the command
tkinter.messagebox.showinfo("title","some text")
So I i tried a minimal script to check if I can get along with that command
import tkinter
tkinter.messagebox.showinfo("test" , "blabla")
The window is displayed correctly but it doesn't respond when I click on the "OK" button.
Addtionally there is a second empty window which appears when I start the script.
What is the explanation for this or at least how can I fix that?
tkinter isn't designed to work this way. Every tkinter requires a root window. If you don't explicitly create one (and you didn't), one will be created for you. That's what the blank window is.
Also, a tkinter GUI can't function properly unless it has a running event loop. This is necessary because some functions, such as responding to buttons and redrawing the window, only happens in response to events. If the event loop isn't running, events can't be processed.
Bottom line: the dialogs aren't designed to be used outside of the context of a proper tkinter app.
Wrapper for standalone use
The following code can be used to display one of the dialogs in standalone mode. It works by creating and hiding a root window, displaying the dialog, and then destroying the root window.
import tkinter as tk
from tkinter import messagebox
def show_dialog(func, *args, **kwargs):
# create root window, then hide it
root = tk.Tk()
root.withdraw()
# create a mutable variable for storing the result
result = []
# local function to call the dialog after the
# event loop starts
def show_dialog():
# show the dialog; this will block until the
# dialog is dismissed by the user
result.append(func(*args, **kwargs))
# destroy the root window when the dialog is dismissed
# note: this will cause the event loop (mainloop) to end
root.destroy()
# run the function after the event loop is initialized
root.after_idle(show_dialog)
# start the event loop, then kill the tcl interpreter
# once the root window has been destroyed
root.mainloop()
root.quit()
# pop the result and return
return result.pop()
To use it, pass the dialog you want as the first option, followed by dialog-specific options.
For example:
result = show_dialog(messagebox.askokcancel, "title", "Are you sure?")
if result:
print("you answered OK")
else:
print("you cancelled")
From my last question, how to add the selected files from dialog window to a dictionary?, I opened an another IDLE window, which has nothing (no menu and command)
import Tkinter,tkFileDialog
root = Tkinter.Tk()
How could I close this window?
This was originally a comment, but it seems to be what you need, so:
I'm not 100% sure, but from what I can gather from your post, I think what you're looking for is root.destroy()
As #inspectorG4dget says, you can use root.destroy(), but that is for destroying a widget and all it's children. If you mean by "closing the window" that you actually want to shut down your program, you should use root.quit().
So for that I created you an example, where I binded the ESC button to the quitter function:
import Tkinter as tk
def quit(obj):
obj.quit()
root = tk.Tk()
root.bind('<Escape>', lambda e: quit(root))
root.mainloop()
So after you program is running, if you hit the ESC it will quit.
Anyway, for further info RTFM: Tk Interface Book
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.