Initializing tkinter gui leads to main thread not in main loop - python

My question is regarding initializing a GUI built with tkinter. I'm starting another thread within the GUI that's running a script which takes 10 minutes to finish. The reason I'm doing this in another thread is to be able to keep the GUI responsive during these 10 minutes.
Right now I'm trying to do the following (simplified)
import tkinter as tk
from threading import Thread
class GUI:
def __init__(self):
self.master = tk.Tk()
self.master.geometry("1400x700")
//
...
//
self.master.mainloop()
def run_long_script(self): # Called by button in GUI
self.t1 = Thread(target = long_script)
self.start()
def long_script(self):
try:
...
except InterruptedError as error:
GUI()
This works okay, but when I try to close the GUI with long_script running, I get the error message main thread not in main loop. How should I design the code to be able to close the program correctly?

Tkinter like many other GUIs can use widgets only in main thread
(or rather in thread which runs mainlooop).
Other thread has to update global value (which is shared between threads) or use queue to send value to main thread. And main thread has to use after(milliseconds, function_name) to run periodically function which will get value from global variable or from queue and update progress bar.
Minimal working code.
import threading
import time
import tkinter as tk
import tkinter.ttk as ttk
# --- functions ---
def long_script():
global progress_value
for i in range(20):
print('loop:', i)
# update global variable
progress_value += 5
time.sleep(.5)
def run_long_script():
global progress_value
global t
if t is None: # run only one thread
# set start value
progress_value = 0
# start updating progressbar
update_progressbar()
# start thread
t = threading.Thread(target=long_script)
t.start()
else:
print('Already running')
def update_progressbar():
global t
# update progressbar
pb['value'] = progress_value
if progress_value < 100:
# run it again after 100ms
root.after(100, update_progressbar)
else:
# set None so it can run thread again
t = None
# --- main ---
# default value at start
progress_value = 0
t = None
# - gui -
root = tk.Tk()
pb = ttk.Progressbar(root, mode="determinate")
pb.pack()
b = tk.Button(root, text="start", command=run_long_script)
b.pack()
root.mainloop()

Related

How to have a progress bar display while a function is being processed (tkinter) [duplicate]

I would like to implement a progress bar in Tkinter which fulfills the following requirements:
The progress bar is the only element within the main window
It can be started by a start command without the need of pressing any button
It is able to wait until a task with unknown duration is finished
The indicator of the progress bar keeps moving as long as the task is not finished
It can be closed by a stop command without the need of pressing any stop bar
So far, I have the following code:
import Tkinter
import ttk
import time
def task(root):
root.mainloop()
root = Tkinter.Tk()
ft = ttk.Frame()
ft.pack(expand=True, fill=Tkinter.BOTH, side=Tkinter.TOP)
pb_hD = ttk.Progressbar(ft, orient='horizontal', mode='indeterminate')
pb_hD.pack(expand=True, fill=Tkinter.BOTH, side=Tkinter.TOP)
pb_hD.start(50)
root.after(0,task(root))
time.sleep(5) # to be replaced by process of unknown duration
root.destroy()
Here, the problem is that the progress bar does not stop after the 5s are over.
Could anybody help me finding the mistake?
Once the mainloop is active, the script wont move to the next line until the root is destroyed.
There could be other ways to do this, but I would prefer doing it using threads.
Something like this,
import Tkinter
import ttk
import time
import threading
#Define your Progress Bar function,
def task(root):
ft = ttk.Frame()
ft.pack(expand=True, fill=Tkinter.BOTH, side=Tkinter.TOP)
pb_hD = ttk.Progressbar(ft, orient='horizontal', mode='indeterminate')
pb_hD.pack(expand=True, fill=Tkinter.BOTH, side=Tkinter.TOP)
pb_hD.start(50)
root.mainloop()
# Define the process of unknown duration with root as one of the input And once done, add root.quit() at the end.
def process_of_unknown_duration(root):
time.sleep(5)
print 'Done'
root.destroy()
# Now define our Main Functions, which will first define root, then call for call for "task(root)" --- that's your progressbar, and then call for thread1 simultaneously which will execute your process_of_unknown_duration and at the end destroy/quit the root.
def Main():
root = Tkinter.Tk()
t1=threading.Thread(target=process_of_unknown_duration, args=(root,))
t1.start()
task(root) # This will block while the mainloop runs
t1.join()
#Now just run the functions by calling our Main() function,
if __name__ == '__main__':
Main()
Let me know if that helps.

Python Multithreading - not running simultaneously

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

Python match progress bar to long process

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.

Change tkinter Label in other class?

Can somebody help me please, I'm making an exercise about class and running task on other thread then tkinter. I want to change the label in another class. Can't get my script to work.
I tried different things but I'm having some troubles with understanding the inheriting from classes and the threads, so this is just an example to learn more about it.
from tkinter import *
import tkinter as tk
from tkinter import ttk
import threading
#Gloabl for stopping the run task
running = True
#class 1 with window
class App():
def __init__(self):
#making the window
self.root = tk.Tk()
self.root.geometry("400x400+300+300")
self.root.protocol("WM_DELETE_WINDOW", self.callback)
self.widgets()
self.root.mainloop()
# stop task and close window
def callback(self):
global running
running = False
self.root.destroy()
# all the widgets of the window
def widgets(self):
global labelvar
#startbutton
self.start_button = tk.Button(self.root, text="Start", command=lambda:App2())
self.start_button.pack()
#stopbutton
self.stop_button = tk.Button(self.root, text="Stop", command=lambda:self.stop())
self.stop_button.pack()
#Defining variable for text for label
labelvar = "Press start to start running"
self.label = tk.Label(self.root, text=labelvar)
self.label.pack()
#stop the task
def stop(self):
global running
running = False
#class 2 with task in other thread
class App2(threading.Thread):
def __init__(self):
global running
#check if task can be run
running = True
threading.Thread.__init__(self)
self.start()
def run(self):
#starting random work
for i in range(10000):
print(i)
labelvar = "running"
App.label.pack()
#checking if task can still be running else stop task
if running == False:
break
labelvar = "stopped"
App.label.pack()
#initiate main app
app = App()
As I said in a comment, tkinter doesn't support multithreading itself, but you can do it as long as only one thread, usually the main one, uses (or "talks") it.
If you want to affect what the GUI displays, the other thread(s) must communicate somehow with the GUI thread. This is often done through a queue.Queue, but in this relatively simple case it can be done through a global variable provided that concurrent access to it is controlled by some means—sharing memory space (i.e. global variables) is one of the advantages of multithreading vs multitasking, but it has to be done and done correctly.
An easy way to share a resource like this is by using a threading.Lock dedicated for that purpose. (See the Wikipedia article Lock (computer science) for more details.)
All references to this shared resource (the running flag) should only be done after "acquiring" the Lock and "releasing" it afterwards. Fortunately it's trivial to do this using a Python with statement (as shown below).
Another crucial aspect of the multithreading problem is how any information exchanged between the two threads is processed. In this case I choose to make the tkinter thread poll the running flag, watch for changes, and update any affected widgets accordingly. This can be done by using the universal widget method after() which tells tkinter to schedule a future call (inside the 'mainloop') to a user-supplied function or method and to pass it certain arguments. To get this to happen repeatedly, the called function can reschedule itself to run again by calling after() before it finishes.
Below is a modified version of your code that does these thing. Note that App2 never calls tkinter or touches any of its widgets, which is why it works.
import threading
from time import sleep
from tkinter import *
import tkinter as tk
from tkinter import ttk
DELAY = 100 # millisecs between status label updates
# global flag and a Lock to control concurrent access to it
run_flag_lock = threading.Lock()
running = False
# class 1 with window
class App():
def __init__(self):
global running
self.root = tk.Tk()
self.root.geometry("400x400+300+300")
self.root.protocol("WM_DELETE_WINDOW", self.quit)
self.create_widgets()
with run_flag_lock:
running = False
self.root.after(DELAY, self.update_status, None) # start status widget updating
self.root.mainloop()
# create all window widgets
def create_widgets(self):
self.start_button = tk.Button(self.root, text="Start", command=self.start)
self.start_button.pack()
self.stop_button = tk.Button(self.root, text="Stop", command=self.stop)
self.stop_button.pack()
self.status_label = tk.Label(self.root, text='')
self.status_label.pack()
def update_status(self, run_state):
""" Update status label text and state of buttons to match running flag. """
# no need to declare run_flag_lock global since it's not being assigned a value
with run_flag_lock:
if running != run_state: # status change?
if running:
status_text = 'Press Stop button to stop task'
run_state = True
else:
status_text = 'Press Start button to start task'
run_state = False
self.status_label.config(text=status_text)
# also update status of buttons
if run_state:
self.start_button.config(state=DISABLED)
self.stop_button.config(state=ACTIVE)
else:
self.start_button.config(state=ACTIVE)
self.stop_button.config(state=DISABLED)
# run again after a delay to repeat status check
self.root.after(DELAY, self.update_status, run_state)
# start the task
def start(self):
global running
with run_flag_lock:
if not running:
app2 = App2() # create task thread
app2.start()
running = True
# stop the task
def stop(self):
global running
with run_flag_lock:
if running:
running = False
# teminate GUI and stop task if it's running
def quit(self):
global running
with run_flag_lock:
if running:
running = False
self.root.destroy()
# class 2 with task in another thread
class App2(threading.Thread):
def __init__(self):
super(App2, self).__init__() # base class initialization
self.daemon = True # allow main thread to terminate even if this one is running
def run(self):
global running
# random work
for i in range(10000):
print(i)
# Normally you shouldn't use sleep() in a tkinter app, but since this is in
# a separate thread, it's OK to do so.
sleep(.25) # slow printing down a little
# stop running if running flag is set to false
with run_flag_lock:
if not running:
break # stop early
with run_flag_lock:
running = False # task finished
# create (and start) main GUI app
app = App()

Create new Tkinter window with new thread

I'm attempting to create a Tkinter GUI that runs the mainloop in its own thread, enabling me to run things in other threads (which potentially involves updating elements in the Tkinter GUI). I have the construct below.
import tkinter as tk
from threading import Thread
class DisplayWindow(object):
def __init__(self):
self.running = False
def start(self):
self.running = True
self.thread = Thread(target = self.run)
self.thread.start()
def callback(self):
self.running = False
self.root.destroy()
def run(self):
self.root = tk.Tk()
self.root.protocol('WM_DELETE_WINDOW', self.callback)
self.root.geometry('600x600')
tk.Label(self.root, text='Hello World').pack()
self.root.mainloop()
I can do something like the following with this
win = DisplayWindow()
win.start()
My understanding of how this works is that when win.start() is called the first time, a new thread is created, set to run the run method, and the thread is started. This thread executes the creation of a Tkinter GUI that runs the mainloop until the window is closed (at which time the thread should become inactive). If I close the window and call win.start() again, I expect that the second call should just repeat the process, creating a new thread that calls the run method. However, calling start a second time simply crashes python.
What am I doing wrong?

Categories

Resources