I have a tkinter program, and it gets some data from API. When the internet connection is slow, it can take a really long time. The tkinter window becomes unresponsive. To stop it I have to force close the program. How do I avoid this?
I have a function which is called that retrieves data from the API. I want to be able to stop that function while still keeping the program and tkinter window running.
After uploading the image, I upload that to the API(which obviously takes a bit of time), and after getting the response, pastes the relevant info retrieved into a txt file and opens it.
Use a thread for the call and a recursive event check until the call is done.
from threading import Thread
from time import sleep
import tkinter as tk
class App(tk.Tk):
def api_call(self, time):
sleep(time)
print('API call done')
self.api_result = 10
def on_button_click(self):
self.button.config(state=tk.DISABLED)
self.api_thread = Thread(target=self.api_call, args=(3,))
self.api_thread.start()
self.button_update()
def __init__(self):
tk.Tk.__init__(self)
self.api_thread = None
self.api_result = None
self.button = tk.Button(self, text='Send', command=self.on_button_click)
self.button.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
def button_update(self):
if self.api_result is not None:
if self.api_thread.is_alive():
self.api_thread.join(timeout=2)
self.button.config(state=tk.NORMAL)
self.api_thread = None
self.api_result = None
else:
self.button.after(500, self.button_update)
if __name__ == '__main__':
aplication = App()
aplication.mainloop()
Related
Why does in the code below button1 hang until the time.sleep(10) has completed.
I can only assume tKinter is waiting for the click event to finish before updating it's paint function.
I want on button1 click the state to change to DISABLED as in the code straight away, not when mainformbutton1press() has finished.
I have put time.sleep(10) to mimic rest of code functions - but the actual programme will be many minutes instead.
EDIT! - sleep is just there to show how tkinter hangs. My real programme has lots more code and no sleep function - and it takes a long time to process data with the hung GUI as mentioned. No more sleep suggestions please :)
import tkinter as tk
from tkinter import ttk
from tkinter.constants import DISABLED, NORMAL
import time
# ==================================================
class App:
def __init__(self, tk, my_w):
self.button1 = tk.Button(my_w, text="START", width=34, command = self.mainformbutton1press)
self.button1.grid(columnspan=3, row=6, column=1,padx=10,pady=20, ipadx=20, ipady=20)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def mainformbutton1press(self):
self.button1.config(text="PLEASE WAIT...")
self.button1['state'] = DISABLED
# DO REST OF PROCESSING
# TO MIMIC THIS:
time.sleep(10)
print("doing...")
# ==================================================
if __name__ == "__main__":
my_w = tk.Tk()
my_w.geometry("430x380")
my_w.resizable(False, False)
app = App(tk, my_w)
my_w.mainloop() # Keep the window open
Tk.mainloop is a sort of while loop. time.sleep() stops the loop for a particular period of time. That makes the window unresponsive. You might use .after function:
class App:
def __init__(self, tk, my_w):
self.my_w=my_w
....
def continue_next(self):
print("Doing")
....
def mainformbutton1press(self):
self.button1.config(text="PLEASE WAIT...")
self.button1['state'] = DISABLED
# DO REST OF PROCESSING
# TO MIMIC THIS:
self.my_w.after(10000,self.continue_next)
The only change you need to make to your code is to insert an update to your button.
The 10 second delay might need to be shortened (10 seconds is a long time to wait)
self.button1.config(text="PLEASE WAIT...")
self.button1['state'] = DISABLED
# INSERT UPDATE HERE
self.button1.update()
# DO REST OF PROCESSING
# TO MIMIC THIS:
time.sleep(1)
print("doing...")
I am posting this as informational for folks looking for a way to communicate thread progress back to a tkinter Frame or window. I have seen several approaches detailed in SO and other sites, but none really seemed adequate to me. So here is an approach that displays progress as both a message box update and advancing a Scale Widget. It uses the tkinter variable classes StringVar and DoubleVar rather than trying to use callbacks or continuously poll a queue in the main thread.
Comments are, of course, welcome, but it appears this approach works well.
`
import tkinter as tk
from tkinter import ttk, END, NW, GROOVE
import threading
import queue
import time
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.queue = queue.Queue()
self.msgCt=0
self.listbox = tk.Listbox(self, width=20, height=5)
self.scaleVal=tk.DoubleVar()
self.progressbar = ttk.Scale(self, orient='horizontal',
length=300,
from_=0.0, to=100.0,
variable=self.scaleVal)
self.scaleVal.set(0.0)
self.button = tk.Button(self, text="Start", command=self.spawnthread)
self.msgBtn = tk.Button(self,text="Set Msg", command=self.sendMessage)
self.msgTxt=tk.StringVar(self,"Messages Here...")
self.msgBox=tk.Message(self,textvariable=self.msgTxt,width=200,
anchor=NW,relief=GROOVE)
self.listbox.grid(row=0,column=0,columnspan=2)
self.msgBox.grid(row=1,column=0,columnspan=2)
self.progressbar.grid(row=2,column=0,columnspan=2)
self.button.grid(row=3,column=0)
self.msgBtn.grid(row=3,column=1)
def spawnthread(self):
self.button.config(state="disabled")
self.listbox.delete(0, END)
self.thread = ThreadedClient(self.queue,self.msgTxt,self.scaleVal)
self.thread.start()
self.periodiccall()
def sendMessage(self,msg=None):
if not msg==None:
self.msgTxt.set(msg)
else:
self.msgTxt.set("Message {}".format(self.msgCt))
self.msgCt+=1
def periodiccall(self):
self.checkqueue()
if self.thread.is_alive():
self.after(100, self.periodiccall)
else:
self.button.config(state="active")
def checkqueue(self):
while self.queue.qsize():
try:
msg = self.queue.get(0)
self.listbox.insert('end', msg)
# self.progressbar.step(25)
except queue.Empty:
pass
class ThreadedClient(threading.Thread):
def __init__(self, qu, mtxt,dvar):
threading.Thread.__init__(self)
self.queue = qu
self.msgTxt=mtxt
self.scaleVal=dvar
def run(self):
self.scaleVal.set(0.0)
for x in range(1, 10):
time.sleep(2)
msg = "Function %s finished..." % x
self.msgTxt.set(msg)
self.scaleVal.set(x*10)
self.queue.put(msg)
if __name__ == "__main__":
app = App()
app.mainloop()
`
In response to the several comments, here is a new version of the code:
import tkinter as tk
from tkinter import ttk, END, NW, GROOVE
import threading
import queue
import time
from pickle import FALSE
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.msgCt=0
self.thrdCt=0;
self.scaleVal=tk.DoubleVar()
self.doneSignal=tk.BooleanVar()
self.doneSignal.set(False)
self.doneSignal.trace("w",self.on_doneSignal_set)
self.progressbar = ttk.Progressbar(self, orient='horizontal',
length=300,
maximum=100.0,
variable=self.scaleVal)
self.scaleVal.set(0.0)
self.startBtn = tk.Button(self, text="Start", command=self.spawnthread)
self.stopBtn=tk.Button(self,text="Stop", command=self.stopthread)
self.stopBtn.config(state="disabled")
self.msgBtn = tk.Button(self,text="Set Msg", command=self.sendMessage)
self.msgTxt=tk.StringVar(self,"Messages Here...")
self.msgBox=tk.Message(self,textvariable=self.msgTxt,width=200,
anchor=NW,relief=GROOVE)
self.msgBox.grid(row=0,column=0,columnspan=3)
self.progressbar.grid(row=1,column=0,columnspan=3)
self.startBtn.grid(row=2,column=0)
self.stopBtn.grid(row=2,column=1)
self.msgBtn.grid(row=2,column=2)
def on_doneSignal_set(self,*kwargs):
self.sendMessage("Thread is DONE")
self.startBtn.config(state="active")
self.stopBtn.config(state="disabled")
def stopthread(self):
if self.thread.is_alive():
self.thread.stopNow()
def spawnthread(self):
self.thrdCt=0
self.startBtn.config(state="disabled")
self.stopBtn.config(state="active")
self.thread = ThreadedClient(self.msgTxt,self.scaleVal,self.doneSignal)
self.thread.start()
# self.periodiccall()
def sendMessage(self,msg=None):
if not msg==None:
self.msgTxt.set(msg)
else:
self.msgTxt.set("Message {}".format(self.msgCt))
self.msgCt+=1
class ThreadedClient(threading.Thread):
def __init__(self, mtxt,dvar,dsig):
threading.Thread.__init__(self)
self.msgTxt=mtxt
self.scaleVal=dvar
self._stopNow=False
self._doneSignal=dsig
self._lock=threading.Lock()
def run(self):
self._stopNow=False
self.scaleVal.set(0.0)
for x in range(1, 10):
if not self.checkStopNow():
time.sleep(2)
msg = "Function %s finished..." % x
self.msgTxt.set(msg)
self.scaleVal.set(x*10)
else:
break
self._doneSignal.set(True)
def stopNow(self):
with self._lock:
self._stopNow=True
def checkStopNow(self):
rtrn=False
with self._lock:
rtrn=self._stopNow
return rtrn
if __name__ == "__main__":
app = App()
app.mainloop()
First, the motivation for this exercise in the first place: I have a large python application that uses Scipy.optimize to find a solution to a modeling problem. This can often take a long time, so I wanted a way to have it run, but post messages to the user periodically to let them know things are happening, allow user to abort in the middle, and finally to post a message to the main thread that the modeling is now done. My original code was partly based on a threading example that assumed the producer/consumer model of threading, whereby a producer creates data (put into a Queue) and the consumer consumes it. This is the WRONG model for my problem, so this new code has no Queue. It just simulates a long modeling process using the run() method, in which there are calls to sleep() which could obviously be replaced by steps in the SciPy.minimize function calls.
This new example uses DoubleVar to allow the thread to update a progressBar (thanks to stovfl for that suggestion), a StringVar to update a message box in the main thread from the modeling thread, and finally a BooleanVar to signal the main thread that things are done. This version has no polling in the main thread. To me, that does not seem a very elegant solution!
How do I know the changes to the DoubleVar, StringVar and BooleanVar get through to the main thread? Only that this program works!! Note that the message box can be updated from either the modeling thread or using a button in the main GUI thread.
Again, comments welcome -- Give me reasons this should NOT work, then tell me why it does given those reasons! Does this violate some basic design of Python, or would there be a situation where this would not work for some reason??
im trying to learn python. But i have problems with the threading. First i failed in the "Proces" class because i putted the loop on the wrong place and my program newer returned from the other class.
But now i think all is correct and it still does not work. I need to have a GUI where i want to be able to write my conditions via text entries and i need another class "Proces" that will do stuff, checking status ower internet and so on constantly or in a specified interval...
The Problem is that my tkinter GUI is freezing after pressing something
here is my GUI.py file:
import tkinter as tk
from Proces import Proces
root = tk.Tk()
frame = tk.Frame(root)
frame.pack()
button = tk.Button(frame, text="QUIT", fg="red",command=quit).pack(side=tk.LEFT)
pr = Proces()
print("\nGUI: proces init...")
pr.start()
print("\nGUI: Start ended")
root.mainloop()
here is the Proces.py file:
import time, threading
class Proces(threading.Thread):
def loop(self):
while True:
time.sleep(2)
print("\nProces: looping")
def __init__(self):
threading.Thread.__init__(self)
print("\nProces: Starting proces")
time.sleep(2)
def run(self):
self.deamon = True
print("\nProces: Starting loop")
self.loop()
*This is the output: *
Proces: Starting proces
GUI: proces init...
Proces: Starting loop
GUI: Start ended
Proces: looping
Proces: looping
Proces: looping
Proces: looping
*But the GUI of the tkinter does not react.*
How should i do this kind of task?
Thank you for your help, advice and answer
I think you don't have problem about starting thread etc. However, you should be able to control your thread, meaning your thread should return based on some condition. It seems you have a button to quit. I assume you want to finish the process by clicking the button.
To do that, when you click to button, your main thread should pass a variable to Process in order to break your while loop, which is your thread basically.
Here is a code that you can work on.
import tkinter as tk
import time
import threading
class MainWindow:
def __init__(self, master):
self.master = master
self.quit_button = tk.Button(self.master, text="QUIT", command=lambda:self.quit(), width=20)
self.quit_button.pack(side=tk.LEFT)
self.process = None
print("\nGUI: process init...")
self.start_task()
def quit(self):
self.process.stop_process = True
print("\nGUI: Start ended")
def start_task(self):
self.process = Process()
self.process.start()
class Process(threading.Thread):
def loop(self):
while True:
if not self.stop_process:
time.sleep(2)
print("\nProcess: looping")
else:
print("\nProcess: looping ended")
return
def __init__(self):
threading.Thread.__init__(self)
self.stop_process = False
print("\nProcess: Starting proces")
time.sleep(2)
def run(self):
self.deamon = True
print("\nProcess: Starting loop")
self.loop()
if __name__ == '__main__':
root = tk.Tk()
app = MainWindow(master=root)
root.mainloop()
So you start yout tkinter, which is your main thread. Then iniating another class within your main thread, which is inherits the thread. So you have two seperate thread that working. When you clicked to "quit" button, you pass variable stop_process, which breaks the loop and return, meaning ending your thread. Your main thread is still alive for running your tkinter window.
I hope it will help
I have a Python script that has some processes that take some time such as creating a .shp file and opening an .mxd. I've read Python Progress Bar but I would like to avoid using a external package. So far I have;
from tkinter import *
from tkinter.ttk import *
import os, arcpy
tk=Tk()
progress=Progressbar(tk,orient=HORIZONTAL,length=100,mode='determinate')
def bar():
import time
os.startfile("Assignment.mxd")
progress['value']=20
tk.update_idletasks()
time.sleep(8)
progress['value']=50
tk.update_idletasks()
time.sleep(8)
progress['value']=80
tk.update_idletasks()
time.sleep(8)
progress['value']=100
progress.pack()
Button(tk,text='Open',command=bar).pack()
mainloop()
It seems to be working but I don't think the process to open the .mxd is paying attention to what stage the progress bar is at. I would like to get it so that the process finishing coincides with the progress bar reaching 100%. Is there away to get the process to 'wait' for the progress bar? Could anyone offer some pointers on how I might get this to work?
Thanks
The problem is that the sleep function and possibly any other blocking function you call in your bar function blocks the GUI. A solution could be pushing the actions to a worker thread. A very quick solution I can come up with is this:
from random import randint
from threading import Thread
from tkinter import Tk, DoubleVar, HORIZONTAL
from tkinter.ttk import Frame, Button, Progressbar
from time import sleep
class Worker(Thread):
def __init__(self, reference):
super().__init__()
self.reference = reference
def run(self):
print("running...")
# replace this dummy loop with actual processing tasks
while self.reference.get_progress() < 100:
duration = randint(1, 3)
sleep(duration)
self.reference.add_progress(10 * duration)
print("finished.")
class Example(Frame):
def __init__(self, root):
super().__init__(master=root)
self.progress = DoubleVar(value=0.0)
self.worker = Worker(reference=self)
self.startbutton = Button(master=self, text="Start", command=self.start)
self.startbutton.pack()
self.progressbar = Progressbar(master=self, orient=HORIZONTAL, length=100, mode='determinate', variable=self.progress)
self.progressbar.pack()
self.infobutton = Button(master=self, text="Info", command=self.info)
self.infobutton.pack()
self.pack()
def get_progress(self):
return self.progress.get()
def set_progress(self, value):
self.progress.set(value)
def add_progress(self, value):
self.progress.set(self.progress.get() + value)
def start(self):
# print("sleeping...")
# sleep(10) # uncomment this to block the GUI
print("starting...")
self.worker.start()
def info(self):
print(self.worker.is_alive())
def main():
root = Tk()
app = Example(root)
app.mainloop()
if __name__ == '__main__':
main()
Note that the info button works before, while and after the thread runs. One problem with this quick solution is that the thread can only be run once. To run it multiple times, some kind of resetting needs to be implemented.
I created a simple tkinter app, where have used two threads. Their task is to write numbers to widgets such as label and text. One thread is triggered by button (click event) and second is executed as a background thread.
import Tkinter as tk
from ttk import *
from Tkconstants import *
import threading, thread, time
def tl1(text,counter):
while True:
text.insert(END,counter)
counter += 1
time.sleep(2)
def tl2(label,counter):
while True:
label['text'] = counter
counter += 1
time.sleep(1)
class mainWindow():
def __init__(self, master):
self.master = master
self._initLayout()
def _initLayout(self):
#button
self.button = tk.Button(self.master, text="thread1_start", command = self._task1)
self.button.pack()
#label
self.label = tk.Label(self.master)
self.label.pack()
#text
self.text = tk.Text(self.master, width=30)
self.text.pack()
def _task1(self):
t1 = thread.start_new_thread(tl1,(self.text,1))
def _task2(self):
t2 = thread.start_new_thread(tl2,(self.label,1000))
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.mainWindow = mainWindow(self)
self.mainWindow._task2() #background_thread
app = App()
app.mainloop()
In this manner everything works fine, but if we change the background thread to display results on text widget, the whole app freezes.
Why background thread works fine communicating with label but causes problems with text widget? Is there any way to run it properly?
Tkinter isn't thread safe. You can only access widgets from the thread that created them. Your threads will need to put data on a thread-safe queue, and your GUI thread will need to poll the queue.
In your particular case you don't need threads at all. You can use the tkinter after method to run code periodically.