How does a thread close when using Tkinter - python

I know this question has been asked numerous times before, but they all use people's code and classes/various functions, etc, so its been a bit difficult to try and follow/understand what's going on. I wanted a more simpler setup to understand what's happening here and how it all works.
When you thread a function, once the function is complete, the thread is closed:
import threading
def fun():
x=0
while x<1000:
x+=1
threading.Thread(target=fun).start()
So I decided to take this idea one step further with Tkinter.
import tkinter as tk
from tkinter import *
from tkinter import messagebox as mb
import threading
def fun():
x=0
while x<10000900:
x+=1
if x == 50:
print(x)
def main():
while True:
fun()
if mb.askquestion('Replay', 'Would you like to play another round?') != 'yes':
root.destroy()
break
root = tk.Tk()
root.geometry('600x600')
threading.Thread(target=main).start()
root.mainloop()
The idea being, that when the main function was broken (based on the user response), the thread would also close automatically. Additionally, so would the Tkinter window. However, when the above script is run, the Tkinter window does close, but the terminal still indicates something, which I assume is the thread. What I don't understand is why in the first case where I use threading, the program ends properly, whereas the 2nd one doesn't.

When you execute root.destroy() you kill the main thread (mainloop) as well as the extra thread running your main funtion. That way the break statement never gets executed - the thread that would execute that statement is abruptly ended.
If you replace root.destroy() with root.after(10, root.destroy) the program acts as expected. This is because you're delaying the call to root.destroy() by some time (10 ms). This delay allows the break statement to be executed since the thread is still alive for 10 ms.

Related

Tkinter , root update

Why i am getting this error with root.update() when i destroy the window:
_tkinter.TclError: invalid command name ".!label"
or
_tkinter.TclError: invalid command name ".!frame.!text"
here is an example code:
from tkinter import *
import random
r=Tk()
a=Label(r)
b=[1,2,3,4,5,6,7]
while(True):
a.configure(text=f'{random.choice(b)}')
a.pack()
r.update()
r.mainloop()
any way to fix this error?
The error is because you are running an infinite loop
After closing the window also loop is try to run the program and try to access the Tk() window, but it has been destroyed.
the solution is break the loop also when you close the window
try:
a.configure(text=f'{random.choice(b)}')
r.update()
except:
break
In above code program will try to access the window, if success it will run the program
Otherwise failed it will execute 'except' condition and break the loop
your full code will be
from tkinter import *
import random
r=Tk()
a=Label(r)
a.pack()
b=[1,2,3,4,5,6,7]
while(True):
try:
a.configure(text=f'{random.choice(b)}')
r.update()
except:
break
r.mainloop()
That, or similar, happens any time you close a tkinter window and there is an uncompleted action waiting to process. Frankly, it can be ignored...
..but, if you really want to get rid of it, you need to manage your ongoing processes and wait for them to finish when closing. You will need to overwrite the destroy() method in a customized version of the Tk class to have that, and you will probably be better off using after() if you want active processes going on with the program. The way you are doing it right now, it never hits the mainloop so some functionality will not work.
Here is a simple fix:
from tkinter import *
import random
class Globals:
stop = False
class MyTk(Tk):
def trueDestroy(self):
Tk.destroy(self)
def destroy(self):
Globals.stop = (self)
def myUpdate():
a.configure(text=f'{random.choice(b)}')
if Globals.stop:
r.trueDestroy()
else:
r.after(1, myUpdate)
r=MyTk()
a=Label(r)
a.pack()
b=[1,2,3,4,5,6,7]
r.after(1, myUpdate)
r.mainloop()
Really, you should make a queue for each process you have running instead of just one 'stop' function, and then only destroy it once they all are stopped, but hopefully you can extrapolate form this.

Strange behaviour of tkinter when using multiple threads

When a new thread destroys a tkinter Tk object whose mainloop is in the main thread, the new thread seems to get stuck and never continues or completes. Why does this happen? The code below shows an example of what I mean:
from tkinter import *
from threading import *
import time
win = Tk()
def exit_after_time():
time.sleep(2)
win.destroy()
thread = Thread(target=exit_after_time)
thread.start()
win.mainloop()
print("mainloop quit")
thread.join()
print("thread joined")
When this is run, the mainloop is quit after 2 seconds, but the thread is never joined. At the moment I'm just working around this by making the thread a daemon and not waiting for it to finish, but I'd like to know what's actually going on.

label.configure works sometimes why?

Part of my code is as follows:
def get_songs():
label6.configure(text='Wait')
os.system('/home/norman/my-startups/grabsongs')
label6.configure(text='Done')
The label is not updated at the first .configure() but is at the second one.
Except if I cause a deliberate error immediately after the first one at which point it is updated and then the program terminates.
The system call takes about 2 minutes to complete so it isn't as if there isn't time to display the first one.
I am using Python 2.7.6
Does anyone know why please?
I'm going to guess you're using Tkinter. If so, as #albert just suggested, you'll want to call label.update_idletasks() or label.update() to tell Tkinter to refresh the display.
As a very crude example to reproduce your problem, let's make a program that will:
Wait 1 second
Do something (sleep for 2 seconds) and update the text to "wait"
Display "done" afterwards
For example:
import Tkinter as tk
import time
root = tk.Tk()
label = tk.Label(root, text='Not waiting yet')
label.pack()
def do_stuff():
label.configure(text='Wait')
time.sleep(2)
label.configure(text='Done')
label.after(1000, do_stuff)
tk.mainloop()
Notice that "Wait" will never be displayed.
To fix that, let's call update_idletasks() after initially setting the text:
import Tkinter as tk
import time
root = tk.Tk()
label = tk.Label(root, text='Not waiting yet')
label.pack()
def do_stuff():
label.configure(text='Wait')
label.update_idletasks()
time.sleep(2)
label.configure(text='Done')
label.after(1000, do_stuff)
tk.mainloop()
As far as why this happens, it actually is because Tkinter doesn't have time to update the label.
Calling configure doesn't automatically force a refresh of the display, it just queues one the next time things are idle. Because you immediately call something that will halt execution of the mainloop (calling an executable and forcing python to halt until it finishes), Tkinter never gets a chance to process the changes to the label.
Notice that while the gui displays "Wait" (while your process/sleep is running) it won't respond to resizing, etc. Python has halted execution until the other process finishes running.
To get around this, consider using subprocess.Popen (or something similar) instead of os.system. You'll then need to perodically poll the returned pipe to see if the subprocess has finished.
As an example (I'm also moving this into a class to keep the scoping from getting excessively confusing):
import Tkinter as tk
import subprocess
class Application(object):
def __init__(self, parent):
self.parent = parent
self.label = tk.Label(parent, text='Not waiting yet')
self.label.pack()
self.parent.after(1000, self.do_stuff)
def do_stuff(self):
self.label.configure(text='Wait')
self._pipe = subprocess.Popen(['/bin/sleep', '2'])
self.poll()
def poll(self):
if self._pipe.poll() is None:
self.label.after(100, self.poll)
else:
self.label.configure(text='Done')
root = tk.Tk()
app = Application(root)
tk.mainloop()
The key difference here is that we can resize/move/interact with the window while we're waiting for the external process to finish. Also note that we never needed to call update_idletasks/update, as Tkinter now does have idle time to update the display.

Can I run a while loop in the background at the same time as other things? [duplicate]

I am trying to run a While Loop in order to constantly do something. At the moment, all it does is crash my program.
Here is my code:
import tkinter
def a():
root = tkinter.Tk()
canvas = tkinter.Canvas(root, width=800, height=600)
while True:
print("test")
a()
It will loop the print statement, however the actual canvas refuses to open.
Are there any viable infinite loops that can work alongside Tkinter?
Extra Information
When I remove the While True statement, the canvas reappears again.
Tkinter hangs unless it can execute its own infinite loop, root.mainloop. Normally, you can't run your own infinite loop parallel to Tkinter's. There are some alternative strategies, however:
Use after
after is a Tkinter method which causes the target function to be run after a certain amount of time. You can cause a function to be called repeatedly by making itself invoke after on itself.
import tkinter
#this gets called every 10 ms
def periodically_called():
print("test")
root.after(10, periodically_called)
root = tkinter.Tk()
root.after(10, periodically_called)
root.mainloop()
There is also root.after_idle, which executes the target function as soon as the system has no more events to process. This may be preferable if you need to loop faster than once per millisecond.
Use threading
The threading module allows you to run two pieces of Python code in parallel. With this method, you can make any two infinite loops run at the same time.
import tkinter
import threading
def test_loop():
while True:
print("test")
thread = threading.Thread(target=test_loop)
#make test_loop terminate when the user exits the window
thread.daemon = True
thread.start()
root = tkinter.Tk()
root.mainloop()
But take caution: invoking Tkinter methods from any thread other than the main one may cause a crash or lead to unusual behavior.

Python Tkinter Program is crashing with forget()

In my Program I want to use forget() on a button. Now if I try that, the program crashes. I know that it has something to do with threading but I couldn't find a soultion yet. Thanks in advance. Here is my examplecode:
import Tkinter as tk
import thread
window = tk.Tk()
def ok():
pass
def voice():
button1.forget()
print("If you see this, it works!")
thread.start_new_thread(voice,())
button1=tk.Button(command=ok, text="PRESS")
button1.pack()
window.mainloop()
You can't access tkinter objects from any thread but the thread that creates the object. In other words, you can't call button1.forget() from a thread and expect it to work reliably.
The generally accepted solution is to have your thread(s) write information to a thread-safe queue, and have your GUI thread poll that queue perioducally, pull an item off, and do whatever that item is requesting.
So I solved this problem simply by using the module mtTkinter , which you can find here :
http://tkinter.unpythonic.net/wiki/mtTkinter To use it you only have to write import mtTkinter as Tkinter at the beginning. After that you can use your Tkinter normally. This module changes nothing in Tkinter, it only makes it thread-friendly.
Tkinter is notorious for the fact that its lack of thread safety means that code you have written can sometimes work, and sometimes cause the entire program to hang with no errors produced, which is a pain.
Luckily, Tkinter does have its own measure for dealing with the problem, so to start a a thread with voice in it, just call voice. However, at theend of voice make sure you have made use of the window.after method to call it again later down the line. For example:
import Tkinter as tk
import thread
window = tk.Tk()
def ok():
pass
def voice():
button1.forget()
print("If you see this, it works!")
window.after(10, voice())
voice()
button1=tk.Button(command=ok, text="PRESS")
button1.pack()
window.mainloop(

Categories

Resources