Python Tkinter changing UI from the main thread VS new thread - python

In my learning python process, I came across the typical freezing problem when main thread is blocked by a long running function. The solution I found was basically starting a new thread. I ended up having 2 ways that seemed to be working correctly, but I would love to know if my understanding is correct.
The first way I believe the pb.destory() is an example of updating UI from a non-main thread
# --- functions ---
def longFunction():
for i in range(10):
print(i)
time.sleep(0.25)
# I believe this is updating the UI from the non-main thread (remove the progress bar)
pb.destroy()
def createNewThread():
# I am expecting the prgoressbar animation to not get blocked/freeze.
pb.start(10)
pb.pack()
thread = threading.Thread(target=longFunction)
thread.daemon = True
thread.start()
root = tk.Tk()
pb = Progressbar(root,mode='indeterminate')
tk.Button(root,text='START', command=createNewThread).pack()
root.mainloop()
The second way, I basically tried to use after() with a flag that I checks on periodically. I believe in this one the UI update happens in the main thread
# --- functions ---
def longFunction():
for i in range(10):
print(i)
time.sleep(0.25)
global isDone
isDone = True
# Check on isDone flag every 100 ms
def checkFlag():
global isDone
if isDone:
# I believe this is updating the UI from the main thread (remove the progress bar)
pb.destroy()
else:
root.after(100,checkFlag)
def createNewThread():
# I am expecting the prgoressbar animation to not get blocked/freeze.
pb.start(10)
pb.pack()
thread = threading.Thread(target=longFunction)
thread.daemon = True
thread.start()
root.after(100,checkFlag)
# flag
isDone = False
root = tk.Tk()
pb = Progressbar(root,mode='indeterminate')
tk.Button(root,text='START', command=createNewThread).pack()
root.mainloop()
My questions are:
1- Are my assumptions correct where the first way is updating the UI from non-main thread and the 2nd way is updating the UI from the main thread?
2- Is there an issue in updating the UI from a non-main thread ( I know in WPF/C# it causes too many problems)
3- Is there a better solution?

Related

PyQt5. Controlling GUI from separate thread [duplicate]

This question already has an answer here:
In PyQt, what is the best way to share data between the main window and a thread
(1 answer)
Closed 5 months ago.
I want to produce simple GUI, a label and a push button. When I press the push button it should invoke a separate thread (the GUI unfreezes ). In the thread I have simple loop
for i in range(4):
self.label.setText(str(i))
time.sleep(1)
the loop must change the label text each second. So I expect to see the GUI how the label changes 0, 1, 2, 3 each second. This is my code:
self.pushButton.clicked.connect(self.clicked_1)
self.label.setText("Lubo1")
def clicked_1(self):
for i in range(4):
self.label.setText(str(i))
app.processEvents()
print("Sleep!")
time.sleep(1)
print("Weak up!")
x = threading.Thread(target=clicked_1, args=(1,))
x.start()
The code works exactly as I expect when I comment x = threading.Thread(target=clicked_1, args=(1,)) and uncomment app.processEvents().
However when app.processEvents() is commented and x = threading.Thread(target=clicked_1, args=(1,)) is uncommented the code gives an error:
AttributeError: 'int' object has no attribute 'label'
I don't want to use app.processEvents() since I know this is not the right way. The correct way is to use threading, but I don't know how to overcome the AttributeError: 'int' object has no attribute 'label' error. Please help.
Best Regards, thank you in advance.
Lubomir Valkov
You should never do such a process, Qt does not support gui operations of any kind outside the main thread. Sooner or later, if you call your GUI out of your main thread your app will crash because it calls methods which are not thread-safe. You should always use signals and slots to communicate between threads and main gui.
# your thread class
class ThreadClass(QtCore.QThread):
any_signal = QtCore.pyqtSignal(int)
def __init__(self, time_to_wait, parent=None):
super(ThreadClass, self).__init__(parent)
self.time_wait = time_to_wait
def run(self):
for val in range(self.time_wait):
time.sleep(1)
self.any_signal.emit(val+1)
your GUI
self.pushButton.clicked.connect(self.clicked_1)
self.label.setText("Lubo1")
def clicked_1(self):
self.thread = ThreadClass(time_to_wait=4, parent=None)
self.thread.any_signal.connect(self.update_label)
self.thread.start()
def update_label(self, value):
self.label.setText(str(i))
As you can see you your button start a thread that emit a signal. Your gui captures this signal and do what ever it need to do in the main GUI, but your thread is never changing anything of the main thread

How can I update Tkinter label from a spawned process (multiprocessing.Process)?

Summary: In Python when I update a Tkinter label text from a spawned process, the label on the GUI is not updated, although the spawned process is executed. How can I make it update from the spawned process?
Im am working with Python 2.7.2 in Lubuntu 20.04
EDIT: I also tried with Python 3.8, had to install python3-tk extra, change a little syntax (parentheses after print command and replacing Tkinter with tkinter), but problem still there looking identical.
END EDIT
Here is my sample code working standalone to try out:
from Tkinter import *
from multiprocessing import Process
# simple Label change invoked from "simple" button
def bnAction_sync():
print "bnAction_sync"
changeLabel()
print "bnAction_sync done"
# asynchronous label change invoked from Async button
def bnAction_async():
print "bnAction_async"
p = Process(target=changeLabel)
p.start()
print "bnAction_Async done"
def changeLabel():
print "change label"
lbl['text'] = "Text changed"
### Apr 19 2021:
### uncommenting the following line really updates label but makes program crash ###
# root.update_idletasks
print "change label done"
root = Tk()
btnSync = Button(root, text="simple", command=bnAction_sync)
btnSync.pack()
btnAsync = Button(root, text="async", command=bnAction_async)
btnAsync.pack()
lbl = Label(root, text="Initial text")
lbl.pack()
root.mainloop()
If I press the "simple" button, text is updated in label. All good.
But:
If I press "async" button,
as you can verify by the prints I have provided, the asynchronous process starts,
the label text updation line is executed.
But: Here is my problem: The label on the GUI is not displaying the updated text.
The reason I want to do it this way:
Because I am starting a long running spawned process, after which I want to update the label. All other processes however should run in parallel. So I created a function f, containing the long running function and the label update function sequentially. I want to call f asynchronously.
So in principle:
def longprocess():
code...
def updatelabel_after_longprocess():
code...
def f():
longprocess()
updatelabel_after_longprocess()
p = Process(target=f)
p.start()
do other things immediately
Somewhere I read, that refresh is suspended, while a script is still running.
I tried some p.join inserts with no luck.
Please help, thanks!
It's unlikely that you'll be able to make updating your label from another process work. It might be possible, but it will be very complicated. What I'd suggest instead is that you make a thread that launches the expensive code in another process, and then waits to update the GUI until after the process is done:
from multiprocessing import Process
from threading import Thread
def longprocess():
# do whatever you need here, it can take a long time
def updatelabel_after_longprocess():
# do whatever GUI stuff you need here, this will be run in the main process
def thread_helper():
process = Process(target=longprocess)
process.start()
process.join() # this waits for the process to end
updatelabel_after_longprocess()
if __name__ == "__main__":
t = Thread(target=thread_helper)
t.start()
# do whatever else you have to do in parallel here
t.join()

Running complicated code inside of pySimpleGUI?

I'm trying to run some code inside of a GUI, where I run a function after I get a few text inputs. however the function I am trying to run is actually really complicated, so when it runs, it makes the entire gui freeze up for 10-15 seconds before continuing.
How can I make it so that when I hit the run button, it doesn't freeze up the entire GUI waiting for the function to complete?
I do understand that there is a way to make functions threaded, however, I don't know how to implement that?
An example of how I can wrap a function to make it a threaded one would be great.
The code below gives an example of the problem that I am dealing with.
import PySimpleGUI as sg
import time
def simple_gui():
layout = [ [sg.T('try clicking "do something" and move the window')],
[sg.Button('do something'), sg.Button('Exit')] ]
w = sg.Window('test', layout)
while True:
events, values = w.read()
if events == 'do something':
# If you hit the button "do something":
# run a function that takes 30 seconds to complete.
time.sleep(30)
if events == sg.WIN_CLOSED or events == 'Exit':
break
w.close()
simple_gui()
There are a number of examples of how you can use threads to perform "long operations" using PySimpleGUI. You'll find the demo programs located at http://Demos.PySimpleGUI.org . There are at least 6 sample programs labeled as being multi-threaded examples.
Some are also available on Trinket to run online. This one shows how to run a thread and then wait for it to complete:
https://pysimplegui.trinket.io/demo-programs#/multi-threaded/multi-threaded-long-task-simple
Here's the code that you'll find there.
#!/usr/bin/python3
import threading
import time
import PySimpleGUI as sg
"""
DESIGN PATTERN - Multithreaded Long Tasks GUI using shared global variables
Presents one method for running long-running operations in a PySimpleGUI environment.
The PySimpleGUI code, and thus the underlying GUI framework, runs as the primary, main thread
The "long work" is contained in the thread that is being started. Communicating is done (carefully) using global variables
There are 2 ways "progress" is being reported to the user.
You can simulate the 2 different scenarios that happen with worker threads.
1. If a the amount of time is known ahead of time or the work can be broken down into countable units, then a progress bar is used.
2. If a task is one long chunk of time that cannot be broken down into smaller units, then an animated GIF is shown that spins as
long as the task is running.
"""
total = 100 # number of units that are used with the progress bar
message = '' # used by thread to send back a message to the main thread
progress = 0 # current progress up to a maximum of "total"
def long_operation_thread(seconds):
"""
A worker thread that communicates with the GUI through a global message variable
This thread can block for as long as it wants and the GUI will not be affected
:param seconds: (int) How long to sleep, the ultimate blocking call
"""
global message, progress
print('Thread started - will sleep for {} seconds'.format(seconds))
for i in range(int(seconds * 10)):
time.sleep(.1) # sleep for a while
progress += total / (seconds * 10)
message = f'*** The thread says.... "I am finished" ***'
def the_gui():
"""
Starts and executes the GUI
Reads data from a global variable and displays
Returns when the user exits / closes the window
"""
global message, progress
sg.theme('Light Brown 3')
layout = [[sg.Text('Long task to perform example')],
[sg.Output(size=(80, 12))],
[sg.Text('Number of seconds your task will take'),
sg.Input(key='-SECONDS-', size=(5, 1)),
sg.Button('Do Long Task', bind_return_key=True),
sg.CBox('ONE chunk, cannot break apart', key='-ONE CHUNK-')],
[sg.Text('Work progress'), sg.ProgressBar(total, size=(20, 20), orientation='h', key='-PROG-')],
[sg.Button('Click Me'), sg.Button('Exit')], ]
window = sg.Window('Multithreaded Demonstration Window', layout)
thread = None
# --------------------- EVENT LOOP ---------------------
while True:
event, values = window.read(timeout=100)
if event in (None, 'Exit'):
break
elif event.startswith('Do') and not thread:
print('Thread Starting! Long work....sending value of {} seconds'.format(float(values['-SECONDS-'])))
thread = threading.Thread(target=long_operation_thread, args=(float(values['-SECONDS-']),), daemon=True)
thread.start()
elif event == 'Click Me':
print('Your GUI is alive and well')
if thread: # If thread is running
if values['-ONE CHUNK-']: # If one big operation, show an animated GIF
sg.popup_animated(sg.DEFAULT_BASE64_LOADING_GIF, background_color='white', transparent_color='white', time_between_frames=100)
else: # Not one big operation, so update a progress bar instead
window['-PROG-'].update_bar(progress, total)
thread.join(timeout=0)
if not thread.is_alive(): # the thread finished
print(f'message = {message}')
sg.popup_animated(None) # stop animination in case one is running
thread, message, progress = None, '', 0 # reset variables for next run
window['-PROG-'].update_bar(0,0) # clear the progress bar
window.close()
if __name__ == '__main__':
the_gui()
print('Exiting Program')

Why is this tkinter program freezing?

I'm having an issue with my GUI freezing, and I don't know why. The run method is not releasing the lock.
Demo program
import time
import threading
import Tkinter as tk
import ttk
LOCK = threading.Lock()
class Video(threading.Thread):
def __init__(self):
super(Video, self).__init__()
self.daemon = True
self.frame = tk.DoubleVar(root, value=0)
self.frames = 1000
def run(self):
while True:
with LOCK:
position = self.frame.get()
if position < self.frames:
position += 1
else:
position = 0
self.frame.set(position)
time.sleep(0.01)
root = tk.Tk()
video = Video()
root.minsize(500, 50)
def cb_scale(_):
with LOCK:
print('HELLO')
scale = ttk.Scale(
root, from_=video.frame.get(), to=video.frames, variable=video.frame,
command=cb_scale)
scale.grid(row=0, column=0, sticky=tk.EW)
root.columnconfigure(0, weight=1)
if __name__ == '__main__':
video.start()
root.mainloop()
Problem
Spam-clicking the progress bar freezes the program.
Attempts at debugging
I used mttkinter by adding import mttkinter to the import statements and the problem persists. The issue is the lock not being released.
I inserted print statements to find out where exactly the program freezes.
Program with print statements:
from __future__ import print_function
import time
import threading
import Tkinter as tk
import ttk
def whichthread(say=''):
t = threading.current_thread()
print('{}{}'.format(say, t))
LOCK = threading.Lock()
class Video(threading.Thread):
def __init__(self):
super(Video, self).__init__()
self.daemon = True
self.frame = tk.DoubleVar(root, value=0)
self.frames = 1000
def run(self):
while True:
whichthread('run tries to acquire lock in thread: ')
with LOCK:
whichthread('run acquired lock in thread: ')
position = self.frame.get()
if position < self.frames:
position += 1
else:
position = 0
self.frame.set(position)
whichthread('run released lock in thread: ')
time.sleep(0.01)
root = tk.Tk()
video = Video()
root.minsize(500, 50)
def cb_scale(_):
whichthread('cb_scale tries to acquire lock in thread: ')
with LOCK:
whichthread('cb_scale acquired lock in thread: ')
print('HELLO')
whichthread('cb_scale released lock in thread: ')
scale = ttk.Scale(
root, from_=video.frame.get(), to=video.frames, variable=video.frame,
command=cb_scale)
scale.grid(row=0, column=0, sticky=tk.EW)
root.columnconfigure(0, weight=1)
if __name__ == '__main__':
video.start()
root.mainloop()
This produces the following output right before the program freezes:
...
run tries to acquire lock in thread: <Video(Thread-1, started daemon 140308329449216)>
run acquired lock in thread: <Video(Thread-1, started daemon 140308329449216)>
cb_scale tries to acquire lock in thread: <_MainThread(MainThread, started 140308415592256)>
This shows that for some reason, the run method does not release the lock.
I tried to comment out lines in order to narrow the problem down.
Removing any of the two with LOCK statements fixes the issue. Unfortunately, in my real program the run and cb_scale function do something meaningful that requires locking.
Commenting out both the calls to get and set in run fixes the issue.
... and this is where I am stuck! :)
EDIT
Thanks to Mike - SMT I was able to track the problem down further.
Using
class DummyDoubleVar(object):
def get(self):
return 500
def set(self, _):
pass
and
self.frame = DummyDoubleVar()
in Video.__init__ prevents the program from freezing.
(Remember that the original program reliably freezes even with mttkinter. I am stumped what's going on here!)
In this post, I will show the solution to the problem and what led me to discover it. It involves going over CPython _tkinter.c code, so if that's not something you are up for, you can just skip to the TL;DR section below. Now, let's dive down the rabbit hole.
Lead-Up
The problem occurs only when moving the sliding bar manually. The MainThread and the Video-thread are then in dead-lock with each other over the LOCK, which I will call the user-lock. Now, the run method never releases the user-lock after is has acquired it, which implies that is hanging because it is waiting for another lock or some operation to complete which cannot. Now, looking at the log output of your verbose example, it becomes clear that the program does not hang consistently: It takes a few tries.
By adding more prints to the run method, you may discover that the problem is not consistently caused by either get or set. When the problem is caused, get may have already finished, or it may not have. This implies that the problem is not caused by get or set specifically, rather by some more generic mechanism.
Variable.set and Variable.get
For this section, I considered only Python 2.7 code, even though the problem is also present in Python 3.6. From the Variable-class in the Tkinter.py file of CPython 2.7:
def set(self, value):
"""Set the variable to VALUE."""
return self._tk.globalsetvar(self._name, value)
def get(self):
"""Return value of variable."""
return self._tk.globalgetvar(self._name)
The self._tk attribute is the Tk-object defined in the C-code of Tkinter, and for the code of globalgetvar we must jump back to _tkinter.c:
static PyObject *
Tkapp_GlobalGetVar(PyObject *self, PyObject *args)
{
return var_invoke(GetVar, self, args, TCL_LEAVE_ERR_MSG | TCL_GLOBAL_ONLY);
}
Jumping to var_invoke:
static PyObject*
var_invoke(EventFunc func, PyObject *selfptr, PyObject *args, int flags)
{
#ifdef WITH_THREAD
// Between these brackets, Tkinter marshalls the call to the mainloop
#endif
return func(selfptr, args, flags);
}
Just to make sure: I compiled Python with thread support and the problem persists. The call is marshalled to the main thread, which I checked with a simple printf in that location. Now, is this done correctly? The function var_invoke will wait until the MainThread has resumed and has executed the requested call. What is the MainThread doing at this point? Well, it is executing its queue of events, in the sequence it got them. What sequence did it get them in? That depends on the timing. This is what causes the problem: In some cases, Tkinter will execute the call to the callback right before a get or set, but while the lock is held.
Regardless of whether mtTkinter is imported (as long as Python is compiled WITH_THREAD support), the call of get and set is marshalled off to the mainloop, but that mainloop may just be trying at that moment to call the callback, which also needs the lock... This is what causes the deadlock and your problem. So basically mtTkinter and plain Tkinter offer the same behaviour, though for mtTkinter this behaviour is caused in the Python code and for plain Tkinter it happes in C-code.
TL;DR; In short
The problem is caused by only the user-lock. Neither GIL nor the Tcl-interpreter lock is involved. The problem is caused by the get and set methods marshalling their actual invocation off to the MainThread and then waiting for completion of the call by this MainThread, while the MainThread tries to do the events in order and execute the callback first.
Is this intended behaviour? Maybe, I'm not sure. I sure can see that with all the ENTER_TCL and LEAVE_TCL macro's in the _tkinter.c file, a better solution might be possible than the current one. For now though, there is no real work-around for this problem (bug? feature?) that I can see, apart from using Tk.after(0, Variable.set), so that the Video-thread does not hold the lock while the MainThread might need it. My suggestion would be removing the DoubleVar.get and set invocations from the code where the lock is held. After all, if your program does something meaningful, it might not need to hold the lock while it sets the DoubleVar. Or, if that is not an option, you will have to find some other means of synchronizing the value, like a subclass of the DoubleVar. Which suits your needs best highly depends on your actual application.
I do not know 100% why your program locks up when clicking on the slider however I suspect that it is due to the tk.DoubleVar() as this is part of the main thread.
Consider using after() instead.
See below example and let me know if you have any questions.
import tkinter as tk
import tkinter.ttk as ttk
class Video(tk.Tk):
def __init__(self):
super().__init__()
self.minsize(500, 50)
self.daemon = True
self.frames = 1000
self.columnconfigure(0, weight=1)
self.vid_var = tk.DoubleVar(self, value=0)
scale = ttk.Scale(self, from_=self.vid_var.get(), to=self.frames, variable=self.vid_var, command=self.cb_scale)
scale.grid(row=0, column=0, sticky='ew')
self.run()
def cb_scale(self, var):
print('HELLO', var)
def run(self):
position = self.vid_var.get()
if position < self.frames:
position += 1
self.after(10, self.run)
else:
position = 0
self.vid_var.set(position)
if __name__ == '__main__':
Video().mainloop()
I had the same problem but i found the solution.
Just create your own sleep function.
like this -
from time import time
def sleep(timeforsleep):
global root
starttime = time()
while True:
if (starttime <= time()-timeforsleep):
break
else:
root.update()
and then use sleep(2)
and then remember to change import time to from time import time
Easy Peasy

Multiprocessing in Python tkinter

How to run multiple processes in python without multithreading? For example consider the following problem:-
We have to make a Gui,which has a start button which starts a function(say, prints all integers) and there is a stop button, such that clicking it stops the function.
How to do this in Tkinter?
Then you need to bind the Button widget with the function which starts the working thread. For example:
import time
import threading
import Tkinter as tk
class App():
def __init__(self, root):
self.button = tk.Button(root)
self.button.pack()
self._resetbutton()
def _resetbutton(self):
self.running = False
self.button.config(text="Start", command=self.startthread)
def startthread(self):
self.running = True
newthread = threading.Thread(target=self.printints)
newthread.start()
self.button.config(text="Stop", command=self._resetbutton)
def printints(self):
x = 0
while self.running:
print(x)
x += 1
time.sleep(1) # Simulate harder task
With the self.running approach, you can end the thread gracefully only by changing its value. Note that the use of multiple threads serves to avoid blocking the GUI while printints is being executed.
I have read this previous question and I suppose why you explicitly asked here for a solution without multithreading. In Tkinter this solution can be used in a scenario where the other threads have to communicate with the GUI part. For example: filling a progressbar while some images are being rendered.
However, as it has been pointed in the comments, this approach is too complex for just printing numbers.
Here you can find a lot of information and more examples about Tkinter.
Edit:
Since your new question has been closed, I'll change the code here to clarify the last point.
Did you try to used multiprocessing module? Seems to be the one you're looking for.

Categories

Resources