How to Stop Tkinter Program When Using a Class - python

I'm using python to create a class object that controls a Tkinter window. My code looks like this:
class MyGui:
def __init__(self):
self.root = tk.Tk()
self.root.title("My Gui")
return
def makeButton(self, text, on_click_function, should_pack = True):
""" Makes a button widget """
button = tk.Button(self.root, text=text, command=on_click_function)
if should_pack:
button.pack()
return button
def start(self):
self.root.mainloop()
def stop(self):
self.root.destroy()
I want to be able to start an instance of the class, then stop it like so:
a = MyGui()
a.makeButton('STOP', a.stop)
a.start()
... // OTHER CODE HERE EVENTUALLY // ...
Everything works fine and the window is successfully created on start() and it vanishes when the button is clicked and stop() executes. The issue is that the mainloop continues to run. I have to manually kill the program using Ctrl+C and it shows it being killed at self.tk.mainloop(n).
If I just do all my tkinter code in a normal file rather than a class, everything works the same but the mainloop also stops when I call destroy on root, and the program ends. If I put it in a class, however, mainloop doesn't stop in spite of the window being destroyed.
Why? How can I get it to stop mainloop too? (I can stop the whole program by calling sys.exit, but I want to keep executing non-Gui stuff after I call stop via the button-press).

I figured out I can solve the problem if I put self.root.quit() and self.root.destroy() together in the stop() function, but not if I use them separately (using just destroy closes the window but leaves the mainloop running; using just quit cancels the mainloop but leaves the window open and in a weird state that causes crashing when you try to close it manually).
So the class function actually needs to look like this:
def stop(self):
self.root.quit()
self.root.destroy()

Related

How can I close a Tkinter window without killing my process?

I have a visualization tool written in Tkinter. Typically, I execute it standalone and then manually close the window when I'm finished. However, I'd like to be call it from another Python program, execute the main loop once (and save the canvas as an SVG), and then close the window, allowing the program to continue.
If I could not even bother opening a window, but just re-use my code to draw an SVG, that would work too.
My tk application looks like this:
class MainApplication(tk.Frame):
def __init__(self, output_path, parent=None):
tk.Frame.__init__(self, parent)
self.parent = parent
# draw visualization, save as svg
#...
#
From another part of my code, I call the
kill = True
root = tk.Tk()
root.title("Placement Visualizer")
MainApplication(output_path, root ).pack(side="top", fill="both", expand=True)
if kill:
root.destroy()
root.mainloop()
I get this error: Tcl_AsyncDelete: async handler deleted by the wrong thread Aborted (core dumped)
I've tried using root.quit() or removing root.mainloop(), but I don't get the desired result.
Thank you
In "the other part of my code":
if kill:
root.quit()
else:
root.mainloop()
To not open a window at all, but to draw everything and save the SVG, just call root.quit(). To open the window normally (and have to close it, killing the process), call root.mainloop().

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.

Reject events during function execution in TKinter

I have a TKinter application (Python) that reacts to button presses and keyboard input. However, it is possible that a function that is called after one of these events may take some time to execute. I would like for the GUI to reject all user input while the function is executing. [EDIT: Just want to clarify: all user input refers to any and all buttons/key bindings that exist in the app - I am not just trying to reject the event from a particular button.]
Here is a very simple example to show that TKinter currently seems to add events to a queue that gets executed after each event is handled. Each time the button is pressed, the text in the button has a zero appended to it. However, if the button is pressed when the text reads 00, the function takes a while to execute (I put in a self.after(3000)), and if the button is pressed while that function is executing, then each of those presses will register. So if I press the button 5 times in under the 3 seconds that I have it "stall", then each of those clicks registers, and I end up seeing 0000000 on the button.
import tkinter as tk
# To demonstrate that keystrokes/button clicks do register while a function is executing (and then subsequently fire)
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
# Build the app
self.text = tk.StringVar()
self.pack()
self.create_widgets()
def create_widgets(self):
self.btn = tk.Button(self, textvariable=self.text, command=self.DoSomething, padx=30, pady=30)
self.btn.grid(row=0, column=0)
self.text.set('0')
def DoSomething(self):
# self.RejectEventsWhileThisFuncExecutes()
old_msg = self.text.get()
if old_msg == '00':
self.after(3000)
self.text.set(old_msg + '0')
# self.BeginAcceptingEventsAgain()
root = tk.Tk()
app = Application(master=root)
app.mainloop()
Basically, what I'd like is maybe something I can call in DoSomething() at the beginning and end of the function, say self.RejectEventsWhileThisFuncExecutes() and self.BeginAcceptingEventsAgain(), that will make sure that no clicks are registered while the function is executing.
A common strategy is to create an invisible widget (eg: a 1x1 frame in the corner of the window), and do a grab (ie: call grab_set) on that widget. That causes all events to be funneled to that widget. As long as there are no bindings on that window, the net effect is that the events get ignored. You just need to flush the event queue (call the update method) before removing the grab.
You can remove the command from the button at the start of DoSomething:
self.btn.config(command=lambda: None)
And then reset it at the end. Just make sure to update to process all queued events before rebinding:
self.update()
self.btn.config(command=self.DoSomething)

tkinter: event binding remains after application terminates?

I'm running tkinter in Python 3.4. A button event seems to remain bound to a command even after the application terminates. Code snippet:
# application class
class DataSel:
def __init__(self,parent):
self.parent = parent
<...>
self.button_sel = tk.Button(self.parent,text='Select')
self.button_sel.grid(row=1,sticky='nesw')
self.button_sel.bind('<Button-1>',self.sel_click)
self.button_quit = tk.Button(self.parent,text='Quit')
self.button_quit.grid(row=2,sticky='nesw')
self.button_quit.bind('<Button-1>',self.quit_click)
def sel_click(self,event):
self.filename = askopenfilename(parent=self.parent)
<...>
def quit_click(self,event):
self.parent.destroy()
# main part of application
root = tk.Tk()
root.lift()
sel = DataSel(root)
root.lift()
root.mainloop()
When I restart the interpreter from scratch and run this application, there is no error message. However, the button_sel button remains pressed (in low relief) after the sel_click method is finished. Then, if I quit the application and rerun it, I get the following message in the shell:
invalid command name ".94227256"
while executing
"$w cget -state"
(procedure "tk::ButtonDown" line 12)
invoked from within
"tk::ButtonDown .94227256"
(command bound to event)
where the number .94227256 changes each time I rerun.
Apart from this message, and the fact that the button remains in low relief, all other functionality is OK. But it seems like the button event somehow stays bound to a stale command!
What is happening is that your binding happens before the button widget is able to process the same event. You are doing this during the processing of the events and you aren't telling Tkinter to stop processing the events further. Therefore, when Tkinter gets around to having the widget process the click event, the window no longer exists and tkinter throws an error.
The root of the problem is that you are putting bindings on a button. You shouldn't do that. If you want to call a function from a button you need to use the command attribute of the button.
If you really think you need to do this via a binding (rather than via the command attribute), you need your function to return '"break"` to tell tkinter to stop any further processing of the event.

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

Categories