Python Tkinter: How to use update() thread safe instead of using mainloop()? - python

For context why I want to use update() and update_idletask() instead of just using mainloop():
I need to display a cv2.imshow fullscreen FullHD 50 FPS stream, grabbed in a different thread from a Basler Dart USB camera, with a (Tkinter) GUI window topmost .
The conversion from the cv2 Mat to a tk PhotoImage and updating the canvas or label displaying the image is taking over 30ms under best conditions, while grabbing the frame and performing some bitwise operations to overlay an transparent image only takes a few ms. Displaying the stream this way is to slow.
For the sake of not needing to learn another gui framework right now and reusing most of the existing code I found a solution to displaying a cv2.imshow() fullscreen window and simultaneously a tk.Tk window topmost, but I am unsure if this is a good idea and how to implement it the right way, because of the warning in cpython _tkinter.c about the thread lock situation.
I read some suggested solutions for displaying a tk and cv2 window at the same time by using threading, but those didn't work for me, maybe because the image grabbing within my CV2Window is already in a thread.
Just calling update() within the cv2 loop works easy, but I don't know if this is a good idea:
Would it be safe not caring about the tcl lock and just using update(), if I implement the communication between the two windows with a threadsafe queue and nothing within the tkinter events blocks too long?
My simplified code right now is:
# Standard library imports
from cv2 import waitKey
from sys import exit
# Local application imports
from CV2Window import CV2Window
from tkWindows import MenuWindow, MenuFrame
# child class of tk.Tk
# -topmost and overrideredirect True, geometry "+0+0"
class MenuApp(MenuWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# frame withdraws and deiconifies buttons when the menu toggle button is clicked,
# to create a dropdown-menu. Row 0 is used by the toggle button
self.menu_frame = MenuFrame(self)
self.menu_frame.add_button("Example Button", command=None, icon="file-plus", row=1)
self.menu_frame.add_button("Close", command=self.close, icon="power", row=2)
self.menu_frame.grid()
def close(self):
self.destroy()
def main():
cv2window = CV2Window()
gui = MenuApp()
while True:
## Update cv2 window
cv2window.update()
## Update GUI manually
gui.update()
## Check if GUI is still open, otherwise close cv2 window
try:
gui.winfo_exists()
except:
print("GUI closed, closing cv2 window..")
cv2window.close()
break
But the CV2Window contains the camera grabbing with MyCameraHandler, which is a class version of the Basler pypylon example "grabusinggrabloopthread.py" and is aquiring the frames in a different thread.
Simplified code for CV2Window:
# Standard library imports
from cv2 import namedWindow, setWindowProperty, imshow, WINDOW_FULLSCREEN, WND_PROP_FULLSCREEN, waitKey, destroyAllWindows
from pypylon.genicam import GenericException
# Local application imports
from CameraClass import MyCameraHandler
class CV2Window():
def __init__(self):
try:
self.cam_handler = MyCameraHandler()
self.cam_handler.start_grabbing()
except GenericException as e:
print("Exception in CV2Window: ", e)
try:
self.cam_handler.stop()
except:
pass
exit(1)
self.img = self.cam_handler.get_image()
namedWindow("cam", WND_PROP_FULLSCREEN)
setWindowProperty("cam", WND_PROP_FULLSCREEN, WINDOW_FULLSCREEN)
imshow("cam", self.img)
def update(self):
self.img = self.cam_handler.get_image()
waitKey(0)
imshow("cam", self.img)
def close(self):
destroyAllWindows()
self.cam_handler.stop()
By reading the cpython _tkinter.c code this could be a problem:
The threading situation is complicated. Tcl is not thread-safe, except
when configured with --enable-threads.
So we need to use a lock around all uses of Tcl. Previously, the
Python interpreter lock was used for this. However, this causes
problems when other Python threads need to run while Tcl is blocked
waiting for events.
If mainloop() is used, Tkinter will get and release the right locks at the right time, but then it is not possible to display and update my CV2Window. From my understanding, update() is only a single tcl call, without any lock management.
As of right now, it is working, but there is zero communication between the two windows.
I need to be able to invoke methods in CV2Window from the Tkinter GUI, and probably in the future also sharing small data/information from the CV2Window to Tkinter.
The next thing I'll try is communicating with a queue, since I don't need to share the image, only some information or actions to perform, and the queue.Queue is threadsafe... this should work, I think?
As long as the events performed because of the update call are taking less than ~15ms, I should be fine and get my needed frametime of <20ms together with the frame grab and imshow, right?
Am I missing something? I am quite new to Python and Tkinter, and wrapping my head around the tcl stuff invoked by Tkinter isn't that easy for me, so any help would be greatly appreciated.

If you are on Unix, the winfo_id() method of any Tkinter widget will return the XID of the underlying drawing surface. You can use that handle to get some other code to draw on that surface. You are recommended to use a Frame with the background set to either None or the empty string (not quite sure which works!) so that Tkinter won't draw on it at the same time; no other widget class supports that. (I know I'd use an empty string in Tk for that effect, which gets converted into a NULL in the underlying structure and that triggers the frame painting code to do nothing on a redraw/repaint request.) I don't know how you'd get the XID into the other framework; it would be responsible for opening its own connection and setting up listeners, but that would work very well in another thread (or even another process; I've done that in the past). That solution does not work on other platforms, as I believe they use IDs that do not have meaning outside of the toolkit implementation. (Technically, there are a few options in Tk that Tkinter does not expose that would help, possibly, but I'm not sure. The -container/-use protocol is not well documented.)
More generally, a custom image type (pretty much requires writing C or C++ code) would let you do something close, though the whole image system is not really designed for animated streaming media. (It certainly isn't designed for hardware acceleration!) It's not something for a quick fix, and I've never tried doing that myself.

Related

How do I Update Tkinter constantly without freezing everything else?

I need to send updates to tkinter from another thread, which should be handled immediately. Sadly Tkinter freezes whenever I try multithreading.
My Attempt:
I've read through several Tkinter-threaded pages but didn't find anything that works well, since most of them try creating a new thread on button.click(), which doesn't help here.
I tried not calling .mainloop() and instead calling the update functions myself, whenever an update comes in:
#GUI
def update(self, string):
self._interactor.config(text=string)
#interactor is a button
self._tk.update_idletasks()
self._tk.update()
This works fine until I use a loop with sleep() in the master to constantly update the text. GUI and master are frozen during sleep().
So I tried to use a threaded timer as discussed here.
#MASTER
gui=gui_remote(self)
def changetext():
text=self.gettextsomewhere()
self._gui.update(text)
loop=RepeatedTimer(5, changetext)
But this just leads to the following error, thrown by Tkinter:
RuntimeError: main thread is not in main loop
Having a hard time on how to solve this. Is it possible to call a GUI class on main thread and still have proper access to its functions?
How I got to this point:
For my project, I need a button, which represents several Buttons.
Every y (eg. 1.5) seconds, the displayed text should be updated to a new one from the outside.
Also, I want to keep GUI, Controller and Data separated, (using blueprint methods) so that later adjustments on each of them will be easier.
I already got it to work, using TK's .after() function, but I had to use GUI and controlling functions closely together.
My Plan
Have a GUI class, which is updateable from another object via simple public functions. The other object (the master) should be able to create a GUI object and call the GUI's update functions with new data every y seconds.
When the GUI button is clicked, simply call a certain method at master every time:
#GUI example
from tkinter import Tk, Button, Frame, Label
class gui_sample:
def __init__(self, master):
"""This is the very simple GUI"""
self._master=master
self._tk=Tk()
self._interactor= Button(self._tk, text="Apfelsaft", command=self._click)
self._interactor.pack()
self._tk.mainloop()
def update(self, string):
"""Handle interactor update"""
self._interactor.config(text=string)
def _click(self):
self._master.click()
#MASTER
from gui_module import *
class Controller:
def __init__(self):
self._gui=gui_sample(self)
self._run()
def _run(self):
#call this every 5 seconds
new_text=self.gettextfromsomewhere()
self._gui.update(new_text)
def click():
#do something
pass
#this code is just a blueprint it probably does nothing
My Problem:
I don't want the master to use TK functions since I might switch to another UI module later and keep the master's functionality. The master will constantly loop through what's being displayed next and needs to be accessible at the same time. Using loops with sleep() isn't a good idea since they will block both, the master and the GUI. Calling .mainloop() is also problematic since it will block all other programs. The gui should always respond to updates and not ask for them.

How to easily avoid Tkinter freezing?

I developed a simple Python application doing some stuff, then I decided to add a simple GUI using Tkinter.
The problem is that, while the main function is doing its stuff, the window freezes.
I know it's a common problem and I've already read that I should use multithreads (very complicated, because the function updates the GUI too) or divide my code in different function, each one working for a little time.
Anyway I don't want to change my code for such a stupid application.
My question is: is it possible there's no easy way to update my Tkinter window every second? I just want to apply the KISS rule!
I'll give you a pseudo code example below that I tried but didn't work:
class Gui:
[...]#costructor and other stuff
def refresh(self):
self.root.update()
self.root.after(1000,self.refresh)
def start(self):
self.refresh()
doingALotOfStuff()
#outside
GUI = Gui(Tk())
GUI.mainloop()
It simply will execute refresh only once, and I cannot understand why.
Thanks a lot for your help.
Tkinter is in a mainloop. Which basically means it's constantly refreshing the window, waiting for buttons to be clicked, words to be typed, running callbacks, etc. When you run some code on the same thread that mainloop is on, then nothing else is going to perform on the mainloop until that section of code is done. A very simple workaround is spawning a long running process onto a separate thread. This will still be able to communicate with Tkinter and update it's GUI (for the most part).
Here's a simple example that doesn't drastically modify your psuedo code:
import threading
class Gui:
[...]#costructor and other stuff
def refresh(self):
self.root.update()
self.root.after(1000,self.refresh)
def start(self):
self.refresh()
threading.Thread(target=doingALotOfStuff).start()
#outside
GUI = Gui(Tk())
GUI.mainloop()
This answer goes into some detail on mainloop and how it blocks your code.
Here's another approach that goes over starting the GUI on it's own thread and then running different code after.
'Not responding' problem can be avoided using Multithreading in python using the thread module.
If you've defined any function, say combine() due to which the Tkinter window is freezing, then make another function to start combine() in background as shown below:
import threading
def combine():
...
def start_combine_in_bg():
threading.Thread(target=combine).start()
Now instead of calling combine(), you have to call start_combine_in_bg()
I did this and now my window is not freezing so I shared it here. Remember you can run only one thread at a time.
Have a look for reference: stackoverflow - avoid freezing of tkinter window using one line multithreading code
Here's how I managed to work around this issue (maybe it's quite dirty I don't know):
Whenever I update my tkinter window through the doingALotOfStuff() function, I call myTk.update().
It avoids freezing for me.

urllib.urlretrieve makes GUI window not respond

I'm making a game using Panda3D, and I'm currently making a downloader to download the latest update. If you don't know what Panda3D is, just imagine the GUI I'm talking about as a Tkinter window :P. The functions I use are:
def doDownload():
urllib.urlretrieve("http://hiddenfile/hi.txt", "hi.txt", reporthook=report)
def report(count, blockSize, totalSize):
percent = int(count*blockSize*100/totalSize)
gui.downloadBar['value'] = percent
However, this makes the GUI window not respond, yet the console window is fine. If it doesn't respond, users will think it's stuck and end its process, and their gamedata will be corrupted. I've tried running on seperate threads, like this:
def doDownload():
threading.Thread(target=__doDownload).start()
def __doDownload():
urllib.urlretrieve("http://hiddenfile/hi.txt", "hi.txt", reporthook=report)
def report(count, blockSize, totalSize):
percent = int(count*blockSize*100/totalSize)
gui.downloadBar['value'] = percent
But that doesn't even download it. Is there any way to download a file, without a window (ex. Tkinter window) going unresponsive?
You're on the right track with using a separate thread for I/O. It's probably not working because you need to update the UI from the main thread. Try changing report() to save that information in a structure that can be checked later by the UI thread.

QPixmap not safe outside GUI thread

My program has the main GUI thread to handle the user interface.
Another thread is started to handle the hard work (loops, calculations, etc) without freezing the main GUI.
From my "calculations thread" I am using another module draw_plots which only draws and saves many kinds of plots.
import draw_plots as plots
class calculatorThread(QtCore.QThread):
signal1 = QtCore.pyqtSignal(int, int)
signal2 = QtCore.pyqtSignal(int,int)
signal3 = QtCore.pyqtSignal(int)
def __init__(self,input_file_txt, parameter_file_txt):
QtCore.QThread.__init__(self)
#and etc etc
At some point of this thread I am calling:
plots.stacked_barplot(*arguments)
Everything works fine, however, I get a message on the screen many many times:
QPixmap: It is not safe to use pixmaps outside the GUI thread
Would like to know what am I doing wrong and how to avoid this message.
Well, you issue the plot command from the calculator thread, which in turn uses a QPixmap to draw the plot - all from inside your calculator thread.
Ideally, you shouldn't draw from the calculator thread, but e.g. emit a signal that you're ready for plotting - and do the plot in the main thread. Maybe along the following lines:
class calculatorThread(QtCore.QThread):
plot_emit = QtCore.pyqtSignal()
def run(self):
self.plot_args = your_calculation()
self.plot_ready.emit()
Outside, connect the plot_ready signal to your plot command:
calculator.plot_emit.connect(lambda x:
plots.stacked_barplot(*calculator.plot_args))
You should use a QImage if the computation is done in a separate thread. You can also safely access QImage.bits() and do your image processing directly working on pixel data (a lot faster).
As the warning is stating clearly, QPixmap objects should be used only from the GUI thread. You can convert the computed QImage into a QPixmap if you really need a QPixmap (but you can for example draw QImages directly on a QPainter).

Threaded Tkinter script crashes when creating the second Toplevel widget

I have a Python script which uses Tkinter for the GUI. My little script should create a Toplevel widget every X seconds. When I run my code, the first Toplevel widget is created successfully, but when it tries to create a second one the program crashes.
What I am doing is using the after method to call the function startCounting every 5 seconds alongside root's mainloop. Every time this function is called, I append a Toplevel widget object into a list and start a new thread which hopefully will be running the new mainloop.
I would be very grateful if someone could figure this problem out. By the way, this is just a little script that I am currently using to solve my problem, which is preventing me from going on with my real school project.
The code:
import threading,thread
from Tkinter import *
def startCounting():
global root
global topLevelList
global classInstance
topLevelList.append (Toplevel())
topLevelList[len(topLevelList)-1].title("Child")
classInstance.append(mainLoopThread(topLevelList[len(topLevelList)-1]))
root.after(5000,startCounting)
class mainLoopThread(threading.Thread):
def __init__(self,toplevelW):
self.toplevelW = toplevelW
threading.Thread.__init__(self)
self.start()
def run(self):
self.toplevelW.mainloop()
global classInstance
classInstance = []
global topLevelList
topLevelList = []
global root
root = Tk()
root.title("Main")
startCounting()
root.mainloop()
Tkinter is designed to run from the main thread, only. See the docs:
Just run all UI code in the main
thread, and let the writers write to a
Queue object; e.g.
...and a substantial example follows, showing secondary threads writing requests to a queue, and the main loop being exclusively responsible for all direct interactions with Tk.
Many objects and subsystems don't like receiving requests from multiple various threads, and in the case of GUI toolkit it's not rare to need specfically to use the main thread only.
The right Python architecture for this issue is always to devote a thread (the main one, if one must) to serving the finicky object or subsystem; every other thread requiring interaction with said subsystem or object must them obtain it by queueing requests to the dedicated thread (and possibly waiting on a "return queue" for results, if results are required as a consequence of some request). This is also a very sound Python architecture for general-purpose threading (and I expound on it at length in "Python in a Nutshell", but that's another subject;-).
Tkinter has issues dealing with input from multiple threads, I use mtTkinter instead, you won't need to change any code and everything will work fine. Just import mtTkinter instead of Tkinter.
You can get it here:
http://tkinter.unpythonic.net/wiki/mtTkinter
Is there a reason you want (or think you need) one event loop per toplevel window? A single event loop is able to handle dozens (if not hundreds or thousands) of toplevel windows. And, as has been pointed out in another answer, you can't run this event loop in a separate thread.
So, to fix your code you need to only use a single event loop, and have that run in the main thread.

Categories