Tkinter: Launching process via multiprocessing creates not-wanted new window - python

I'm planning to write a small GUI around a numerical simulation which is why I'm now playing around with Tkinter. Simulations should be launched from GUI in seperate processes. To play around a bit I defined a function random_process that generates pairs of randn numbers (this should later be a real simulation process). As that function is meant to be launched in a seperate process, two mp.Event objects and one mp.Pipe object are passed as parameters.
The main application can use one event to request accumulated data from the process, another event is used as a "Poison Pill" to kill the "simulation" process. A pipe is then used to pass the data.
In the main application, I use Tkinter's after-function to regularly check if new data has arrived and then plot it. Starting and stopping the "simulation process" is done by buttons in the main app, the same goes for requesting data from it.
At least that was the idea, in practice the program doesn't play nice. When I click on the "go!" button that is meant to launch the simulation process, a second Tkinter window appears, identical to the main one. I don't have the slightest clue why that happens. The communication with the process doesn't work neither, no data seems to be send. When googling for a solution, I found a working example of a Tkinter program launching processes and talking to them, but I didn't find out what makes it not work in my case. Has anybody got a clue?
BTW, the OS is Windows-7.
Cheers,
Jan
import matplotlib
matplotlib.use('TkAgg')
import time
import multiprocessing as mp
import Tkinter as Tk
import numpy.random as npr
import matplotlib.figure
import matplotlib.backends.backend_tkagg as tkagg
def random_process(delay, data_request, data_in, poison):
while not poison.is_set():
time.sleep(delay)
print("Generating pair of random numbers...")
x,y = npr.randn(), npr.randn()
try:
random_process.l.append((x,y))
except:
random_process.l = [(x,y)]
if data_request.is_set():
data_request.clear()
try:
ll = len(random_process.l)
if ll > 0:
print("Sending %d pairs to main program.." % ll)
data_in.send(random_process.l)
random_process.l = []
except:
print("data requested, but none there.")
# when poison event is set, clear it:
poison.clear()
class GuiInterfaceApp:
def __init__(self, parent):
self.myParent = parent
self.previewplot_container = Tk.Frame(self.myParent)
self.f = matplotlib.figure.Figure()
self.ax = self.f.add_subplot(111)
self.preview_canvas = tkagg.FigureCanvasTkAgg(self.f, master=self.previewplot_container)
self.preview_canvas.show()
self.button_container = Tk.Frame(self.myParent)
self.hellobutton = Tk.Button(self.button_container, text="hello!")
self.hellobutton.config(command = self.printhello)
self.startbutton = Tk.Button(self.button_container, text="go!")
self.startbutton.config(command=self.run_simulation)
self.plotbutton = Tk.Button(self.button_container, text="show!")
self.plotbutton.config(command=self.request_data)
self.stopbutton = Tk.Button(self.button_container, text="stop.")
self.stopbutton.config(command=self.stop_simulation)
self.quitbutton = Tk.Button(self.button_container, text="get me outta here!")
self.quitbutton.config(command=self.quit_program)
self.previewplot_container.pack(side = Tk.TOP)
self.preview_canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1)
self.button_container.pack(side = Tk.BOTTOM)
self.hellobutton.pack(side = Tk.LEFT)
self.startbutton.pack(side = Tk.LEFT)
self.plotbutton.pack(side = Tk.LEFT)
self.stopbutton.pack(side = Tk.LEFT)
self.quitbutton.pack(side = Tk.LEFT)
self.simulation_running = False
self.datarequest = mp.Event()
self.DataIn, self.DataOut = mp.Pipe()
self.PoisonEvent = mp.Event()
self.p = mp.Process(target = random_process, args=(1.0, self.datarequest, self.DataIn, self.PoisonEvent))
self.l = [] # list of received pairs to plot
self.mytask_time = 100 # delay in ms between calls to self.mytask
def printhello(self):
print("hello!")
def run_simulation(self):
print("startbutton pressed.")
if not self.simulation_running:
print("starting simulation...")
self.p.start()
self.simulation_running = True # attention: no error checking
def stop_simulation(self):
print("stop button pressed.")
if self.simulation_running:
print("Sending poison pill to simulation process..")
self.PoisonEvent.set()
self.simulation_running = False
# todo: wait a short amount of time and check if simu stopped.
def request_data(self):
print("plotbutton pressed.")
if self.simulation_running:
print("requesting data from simulation process")
self.datarequest.set()
def update_plot(self):
print("update_plot called.")
if len(self.l) > 0:
print("there is data to plot.")
while len(self.l) > 0:
x,y = self.l.pop()
print("plotting point (%.2f, %.2f)" % (x,y))
self.ax.plot([x], [y], '.', color='blue')
print("drawing the hole thing..")
self.ax.draw()
else:
print("nothing to draw")
def quit_program(self):
print("quitbutton pressed.")
if self.simulation_running:
print("sending poison pill to simulation process..")
self.PoisonEvent.set()
print("quitting mainloop..")
self.myParent.quit()
print("destroying root window..")
self.myParent.destroy()
def receive_data(self):
if self.DataOut.poll():
print("receiving data..")
data = self.DataOut.recv()
self.l.append(data)
self.update_plot()
def my_tasks(self):
self.receive_data()
self.myParent.after(self.mytask_time, self.my_tasks)
return
root = Tk.Tk()
myGuiInterfaceApp = GuiInterfaceApp(root)
root.after(100, myGuiInterfaceApp.my_tasks)
root.mainloop()

Try hiding your main logic behind a test for whether the code is being run or imported.
if __name__ == "__main__":
root = Tk.Tk()
...

Related

Python Tkinter Threading - How to end / kill / stop thread when user closes GUI

If I have a non for loop threaded task being run alongside tkinter such as time.sleep(seconds) how can I safely end the task before it has concluded, or, before the time.sleep() has ended.
If you run the program and click the button and then close the GUI window then finished will still print in the interpreter window a few seconds later.
Please note that in this case time.sleep() is just a placeholder for a long running non for loop function.
# python 3 imports
import tkinter as tk
import threading
import queue
import time
def center_app(toplevel):
toplevel.update_idletasks()
w = toplevel.winfo_screenwidth() - 20
h = toplevel.winfo_screenheight() - 100
size = tuple(int(Q) for Q in toplevel.geometry().split("+")[0].split("x"))
toplevel.geometry("%dx%d+%d+%d" % (size + (w / 2 - size[0] / 2, h / 2 - size[1] / 2)))
class app(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.protocol("WM_DELETE_WINDOW", self.USER_HAS_CLOSED_WINDOW)
self.b1 = tk.Button(self, text = "*** Start Thread ***", font = ("Arial",25,"bold"), command = self.func_1)
self.b1.pack(side = "top", fill = "both", expand = True)
center_app(self)
self.queue = queue.Queue()
def USER_HAS_CLOSED_WINDOW(self, callback = None):
print ("attempting to end thread")
# end the running thread
self.destroy()
def func_1(self):
self.b1.config(text = " Thread Running ")
self.b1.update_idletasks()
t = ThreadedFunction(self.queue).start()
self.after(50, self.check_queue)
def check_queue(self):
try:
x = self.queue.get(0) # check if threaded function is done
self.b1.config(text = "*** Start Thread ***")
self.b1.update_idletasks()
except:
self.after(50, self.check_queue)
class ThreadedFunction(threading.Thread):
def __init__(self, queue):
threading.Thread.__init__(self, daemon = True)
self.queue = queue
def run(self):
time.sleep(10) # how to end this if user closes GUI
print ("finished")
self.queue.put("result")
a = app()
a.mainloop()
I have made a function USER_HAS_CLOSED_WINDOW to catch the user closing the window, where I was thinking I would put something to end the running thread, but I'm not sure how.
I guess the other option is to put an appropriate timeout on the thread and use .join() somehow. But I'm not sure how to do that either
It appears that you should use Event object and use wait() method and not time.sleep when you use thread that you want to interrupt.
Probably this can help you:
https://stackoverflow.com/a/5205180
https://stackoverflow.com/a/38828735

Display count variable from background task in main task (Tkinter GUI)

I am trying to display a count variable from a background task in the main task which is my tkinter GUI. Why? I want to display that the long time taking background task is performing and later use this count variable to visualize it with a progress bar.
My problem is, that even when using a Queue, I am not able to display the count variable. Maybe I've got problems in understanding python and its behaviour with objects and/or threads.
import threading
import time
import Queue
import Tkinter as Tk
import Tkconstants as TkConst
from ScrolledText import ScrolledText
from tkFont import Font
import loop_simulation as loop
def on_after_elapsed():
while True:
try:
v = dataQ.get(timeout=0.1)
except:
break
scrText.insert(TkConst.END, str(v)) # "value=%d\n" % v
scrText.see(TkConst.END)
scrText.update()
top.after(100, on_after_elapsed)
def thread_proc1():
x = -1
dataQ.put(x)
x = loop.loop_simulation().start_counting()
# th_proc = threading.Thread(target=x.start_counting())
# th_proc.start()
for i in range(5):
for j in range(20):
dataQ.put(x.get_i())
time.sleep(0.1)
# x += 1
time.sleep(0.5)
dataQ.put(x.get_i())
top = Tk.Tk()
dataQ = Queue.Queue(maxsize=0)
f = Font(family='Courier New', size=12)
scrText = ScrolledText(master=top, height=20, width=120, font=f)
scrText.pack(fill=TkConst.BOTH, side=TkConst.LEFT, padx=15, pady=15, expand=True)
th = threading.Thread(target=thread_proc1)
th.start()
top.after(100, on_after_elapsed)
top.mainloop()
th.join()
In thread_proc1() I want to get the value of the counter of background task. This is the background task:
import time
class loop_simulation:
def __init__(self):
self.j = 0
# self.start_counting()
def start_counting(self):
for i in range(0, 1000000):
self.j = i
time.sleep(0.5)
def get_i(self):
return str(self.j)
The reason the count variable isn't being displayed is due to the
x = loop.loop_simulation().start_counting()
statement in thread_proc1(). This creates a loop_simulation instance and calls its start_counting() method. However, other than already inserting a -1 into the dataQ, thread_proc1() doesn't do anything else until start_counting() returns, which won't be for a long time (500K seconds).
Meanwhile, the rest of your script is running and displaying only that initial -1 that was put in.
Also note that if start_counting() ever did return, its value of None is going to be assigned to x which later code attempts to use with: x.get_i().
Below is reworking of your code that fixes these issues and also follows the PEP 8 - Style Guide for Python Code more closely. To avoid the main problem of calling start_counting(), I changed your loop_simulation class into a subclass of threading.Thread and renamed it LoopSimulation, and create an instance of it in thread_proc1, so there are now two background threads in addition to the main one handling the tkinter-based GUI.
import loop_simulation as loop
from ScrolledText import ScrolledText
import threading
import Tkinter as Tk
import Tkconstants as TkConst
from tkFont import Font
import time
import Queue
def on_after_elapsed():
# removes all data currently in the queue and displays it in the text box
while True:
try:
v = dataQ.get_nowait()
scrText.insert(TkConst.END, str(v)+'\n')
scrText.see(TkConst.END)
except Queue.Empty:
top.after(100, on_after_elapsed)
break
def thread_proc1():
dataQ.put(-1)
ls = loop.LoopSimulation() # slow background task Thread
ls.start_counting()
while ls.is_alive(): # background task still running?
for i in range(5):
for j in range(20):
dataQ.put(ls.get_i())
time.sleep(0.1)
time.sleep(0.5)
dataQ.put('background task finished')
top = Tk.Tk()
dataQ = Queue.Queue(maxsize=0)
font = Font(family='Courier New', size=12)
scrText = ScrolledText(top, height=20, width=120, font=font)
scrText.pack(fill=TkConst.BOTH, side=TkConst.LEFT, padx=15, pady=15,
expand=TkConst.YES)
th = threading.Thread(target=thread_proc1)
th.daemon = True # OK for main to exit even if thread is still running
th.start()
top.after(100, on_after_elapsed)
top.mainloop()
loop_simulation.py module:
import threading
import time
class LoopSimulation(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.daemon = True # OK for main to exit even if instance still running
self.lock = threading.Lock()
self.j = 0
start_counting = threading.Thread.start # an alias for starting thread
def run(self):
for i in range(1000000):
with self.lock:
self.j = i
time.sleep(0.5)
def get_i(self):
with self.lock:
return self.j

Tkinter text entry with pyHook hangs GUI window

I have a Tkinter GUI application that I need to enter text in. I cannot assume that the application will have focus, so I implemented pyHook, keylogger-style.
When the GUI window does not have focus, text entry works just fine and the StringVar updates correctly. When the GUI window does have focus and I try to enter text, the whole thing crashes.
i.e., if I click on the console window or anything else after launching the program, text entry works. If I try entering text immediately (the GUI starts with focus), or I refocus the window at any point and enter text, it crashes.
What's going on?
Below is a minimal complete verifiable example to demonstrate what I mean:
from Tkinter import *
import threading
import time
try:
import pythoncom, pyHook
except ImportError:
print 'The pythoncom or pyHook modules are not installed.'
# main gui box
class TestingGUI:
def __init__(self, root):
self.root = root
self.root.title('TestingGUI')
self.search = StringVar()
self.searchbox = Label(root, textvariable=self.search)
self.searchbox.grid()
def ButtonPress(self, scancode, ascii):
self.search.set(ascii)
root = Tk()
TestingGUI = TestingGUI(root)
def keypressed(event):
key = chr(event.Ascii)
threading.Thread(target=TestingGUI.ButtonPress, args=(event.ScanCode,key)).start()
return True
def startlogger():
obj = pyHook.HookManager()
obj.KeyDown = keypressed
obj.HookKeyboard()
pythoncom.PumpMessages()
# need this to run at the same time
logger = threading.Thread(target=startlogger)
# quits on main program exit
logger.daemon = True
logger.start()
# main gui loop
root.mainloop()
I modified the source code given in the question (and the other one) so that the pyHook
related callback function sends keyboard event related data to a
queue. The way the GUI object is notified about the event may look
needlessly complicated. Trying to call root.event_generate in
keypressed seemed to hang. Also the set method of
threading.Event seemed to cause trouble when called in
keypressed.
The context where keypressed is called, is probably behind the
trouble.
from Tkinter import *
import threading
import pythoncom, pyHook
from multiprocessing import Pipe
import Queue
import functools
class TestingGUI:
def __init__(self, root, queue, quitfun):
self.root = root
self.root.title('TestingGUI')
self.queue = queue
self.quitfun = quitfun
self.button = Button(root, text="Withdraw", command=self.hide)
self.button.grid()
self.search = StringVar()
self.searchbox = Label(root, textvariable=self.search)
self.searchbox.grid()
self.root.bind('<<pyHookKeyDown>>', self.on_pyhook)
self.root.protocol("WM_DELETE_WINDOW", self.on_quit)
self.hiding = False
def hide(self):
if not self.hiding:
print 'hiding'
self.root.withdraw()
# instead of time.sleep + self.root.deiconify()
self.root.after(2000, self.unhide)
self.hiding = True
def unhide(self):
self.root.deiconify()
self.hiding = False
def on_quit(self):
self.quitfun()
self.root.destroy()
def on_pyhook(self, event):
if not queue.empty():
scancode, ascii = queue.get()
print scancode, ascii
if scancode == 82:
self.hide()
self.search.set(ascii)
root = Tk()
pread, pwrite = Pipe(duplex=False)
queue = Queue.Queue()
def quitfun():
pwrite.send('quit')
TestingGUI = TestingGUI(root, queue, quitfun)
def hook_loop(root, pipe):
while 1:
msg = pipe.recv()
if type(msg) is str and msg == 'quit':
print 'exiting hook_loop'
break
root.event_generate('<<pyHookKeyDown>>', when='tail')
# functools.partial puts arguments in this order
def keypressed(pipe, queue, event):
queue.put((event.ScanCode, chr(event.Ascii)))
pipe.send(1)
return True
t = threading.Thread(target=hook_loop, args=(root, pread))
t.start()
hm = pyHook.HookManager()
hm.HookKeyboard()
hm.KeyDown = functools.partial(keypressed, pwrite, queue)
try:
root.mainloop()
except KeyboardInterrupt:
quit_event.set()

How to interrupt a thread/process by making a correct "Stop" button in Tkinter (Python)?

I want some basics on the problem of making some sort of "Stop" button that in my case terminates the series of beeps:
from tkinter import *
import winsound
from random import randint
class App(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
self.widgets()
def widgets(self):
self.beep = Button(self, text = "Beep", command = play_beep)
self.beep.pack()
self.stop = Button(self, text = "Stop", command = stop_beep)
self.stop.pack()
go_on = True
def play_beep():
count = 10
while go_on == True and count != 0:
winsound.Beep(randint(100, 2500), 200)
count -= 1
def stop_beep():
go_on = False
root = Tk()
app = App(root)
root.mainloop()
When I press the "Beep" button it gets stuck as well as all the GUI until the beeps end. Could anyone tell me how to fix it?
I don't use TKinter, but I believe your button press is not creating a separate thread or process. The reason why your button gets stuck is because your play_beep loop is blocking your GUI execution loop. So we use threading. The thread executes at the same time as your GUI, so you can basically do two things at once (listen for GUI events and play beep noises).
import threading
class App(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.is_playing = False
self.pack()
self.widgets()
def widgets(self):
self.beep = Button(self, text = "Beep", command = self.play_beep)
self.beep.pack()
self.stop = Button(self, text = "Stop", command = self.stop_beep)
self.stop.pack()
def play_beep(self):
self.is_running = True
self.beep_th = threading.Thread(target=self.run)
self.beep_th.start()
def run(self):
count = 10
while self.is_running == True and count != 0:
winsound.Beep(randint(100, 2500), 200)
count -= 1
def stop_beep(self):
try:
self.is_running = False
self.beep_th.join(0)
self.beep_th = None
except (AttributeError, RuntimeError): # beep thread could be None
pass
def closeEvent(self, event): # This is a pyside method look for a TKinter equivalent.
"""When you close the App clean up the thread and close the thread properly."""
self.stop_beep()
super().closeEvent(event)
First off, your question has nothing to do about threads or processes. Tkinter is single-threaded.
If you want to run some function periodically in a tkinter program, you must give the event loop a chance to process events. The typical solution is to do it like this:
def play_beep(count=10):
if go_on and count != 0:
winsound.Beep(randint(100, 2500), 200)
root.after(1000, play_beep, count=1)
This will cause the beep to play every second (1000ms) for ten iterations. In between each call, the event loop will have a chance to process other events.
Now, if the code you are running takes a long time, you're going to have to run that code in a separate thread or process. I know nothing about winsound.Beep so I don't know if that's necessary or not.
Second, to be able to interrupt it, you need to make go_on global, otherwise you're simply setting a local variable that never gets used.
def stop_beek():
global go_on
go_on = False

Python script freezes for no apparent reason (afaik)

i'm not particularly new at python but i just thought there might be a reason for this program not working properly. i have written a similar one from which this is derived, and that still works fine. it is a program to graph the average time of a set of ping responses, to see if there is any pattern in the time over the day. the source is as follows
from Tkinter import *
import matplotlib
import time
import os, sys, threading, Queue
matplotlib.use('TkAgg')
from numpy import arange, array, sin, pi
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import Tkinter
import sys
class App(object):
def __init__(self):
self.q = Queue.Queue()
self.q2 = Queue.Queue()
self.avvals=[]
self.addlist=['bbc.co.uk', 'google.co.uk', 'nhgs.co.uk', 'bing.co.uk', 'msn.com']
self.addlistlen = len(self.addlist)
self.root = Tkinter.Tk()
self.root.wm_title("Connection Speed")
self.frame = Tkinter.Frame(self.root)
self.frame.pack(side='top', expand=1, fill='both',)
self.frame2 = Tkinter.Frame(self.frame)
self.frame2.pack(side='bottom', expand=1, fill='both',)
self.f = Figure(figsize=(5,4), dpi=100)
self.a = self.f.add_subplot(111)
self.gframe = Tkinter.Frame(self.frame)
self.gframe.pack(side='top', expand=1, fill='both',)
self.canvas = FigureCanvasTkAgg(self.f, master=self.gframe)
self.canvas.show()
self.canvas.get_tk_widget().pack(side='top', fill='both', expand=1)
self.canvas._tkcanvas.pack(side='top', fill='both', expand=1)
self.lt = threading.Thread(target=self.loop())
def getping(self, add):
pingaling = os.popen("ping -n 2 "+str(add).strip())
sys.stdout.flush()
line = pingaling.read()
if line:
try:
line = line.split('Maximum = ')[1]
time = line.split('ms, Average')[0]
self.q.put(int(time))
except:
self.q.put(None)
def gpthread(self, *a):
t = threading.Thread(target=self.getping, args=a)
t.isDaemon = True
t.start()
def loop(self):
while 1:
for x in self.addlist:
self.gpthread(x)
while self.q.qsize<self.addlistlen:
pass
tl = []
for u in range(self.addlistlen):
temp = self.q.get()
if temp != None:
tl.append(temp)
if len(tl)>0:
self.update(sum(tl)/len(tl))
else:
self.update(None)
def update(self, val):
self.a.clear()
self.avvals.append(val)
self.a.plot(self.avvals, linewidth=0.5, color = 'b')
self.canvas.draw()
a = App()
try:
a.root.mainloop()
except:
a.root.destroy()
i probably dont need the bottom try..except but i put it in to check if it would make a difference.
i haven't had a chance to try it on another computer, but my other scripts are working fine so....
i simply can't comprehend why it freezes, stops responding, and if i exit it by any method i get a error saying
Fatal python error: PyEval NULL tstate
or somthing very similar.
now it doesn't even expand! it just goes straight to not responding!
Change
self.lt = threading.Thread(target=self.loop())
to
self.lt = threading.Thread(target=self.loop)
target=self.loop() calls the loop method before passing the result to threading.Thread.
Passing target=self.loop passes the method object to threading.Thread without calling it. This lets threading.Thread call the method in a new thread.
Here is some code which uses threads to ping some ips, and displays the average ping times in an animated matplotlib bar chart, embedded in a Tkinter window:
import Tkinter
import threading
import subprocess
import Queue
import shlex
import re
import matplotlib.pyplot as plt
import matplotlib.backends.backend_tkagg as tkagg
import atexit
import numpy as np
pingers=[]
def cleanup():
print('terminating ping subprocesses...')
for pinger in pingers:
pinger.proc.terminate()
atexit.register(cleanup)
class Pinger(threading.Thread):
def __init__(self,app,queue):
threading.Thread.__init__(self)
self.app=app
self.queue=queue
def run(self):
# One ping subprocess is started by each Pinger thread.
# The ping subprocess runs indefinitely, terminated by the cleanup function
# which is called by atexit right before the main program terminates.
ip = self.queue.get()
cmd="ping %s" % ip
self.proc = subprocess.Popen(shlex.split(cmd),
stdout=subprocess.PIPE)
for line in iter(self.proc.stdout.readline,''):
match=re.search('time=(.*)\s+ms',line)
if match:
avg=float(match.group(1))
self.app.update(ip,avg)
class App(object):
def __init__(self,master,ips):
self.ips=ips
self.fig = plt.Figure(figsize=(5,4), dpi=100)
self.fig.subplots_adjust(bottom=0.25)
self.ax=self.fig.add_subplot(1,1,1)
self.canvas = tkagg.FigureCanvasTkAgg(self.fig, master=master)
self.canvas.get_tk_widget().pack(side='top', fill='both', expand=1)
self.canvas.show()
N=len(self.ips)
self.idx=dict(zip(self.ips,range(N)))
# I set an initial ping time of 200 just to make the initial bar chart
times=[200]*N
self.rects=self.ax.bar(range(N), times)
self.ax.set_xticks(np.arange(N)+0.8*0.5)
self.ax.set_xticklabels(self.ips, rotation=25)
def update(self,ip,avg):
# This is called by Pinger threads, each time a new ping value is obtained
print(ip,avg)
self.rects[self.idx[ip]].set_height(avg)
self.canvas.draw()
def main():
root = Tkinter.Tk()
root.wm_title("Connection Speed")
ips=['bbc.co.uk', 'google.co.uk', 'nhgs.co.uk', 'bing.co.uk', 'msn.com']
app = App(root,ips)
queue = Queue.Queue()
for ip in ips:
queue.put(ip)
# This starts one Pinger for each ip.
pinger=Pinger(app,queue)
pingers.append(pinger)
pinger.daemon=True
pinger.start()
Tkinter.mainloop()
if __name__=='__main__':
main()

Categories