I am currently working on a project that takes a camera stream and after doing image processing, adds some overlays to measure some features of the image.
I am using tkinter for window management, threads to liberate the main thread (showing the camera stream) from image processing (sometimes quite lengthy operations and also non-live-showing critical dependency) and recently I moved to modules as the main script was +1000 lines.
Here I needed to make a decision about how I manage the project because I am using the window to show some updated values from the image processing, so I am quite literally "transporting" the window between modules (sending it as an argument in the buttons) so I can update values of the window in the aforementioned modules. Here is a MWE of what I am doing:
main.py:
import modules.win_manager as win_manager
if __name__ == "__main__":
win = win_manager.create_window()
win = setup_window(win)
win.root.mainloop()
win_manager.py in modules folder:
from functools import partial
from modules.cam import *
import tkinter as tk
def create_window():
root = tk.Tk()
return root
def img_proc(img,win):
img, val = process(img) #arbitrary image processing in a different file, imag_proc.py
win.objects.strv.set("Finished processing new value {}".format(val))
def cmd(win):
img = win.cam.get_camera()
img_proc(img,win)
def setup_window(win):
win.root = create_window()
win.objects= type('objects', (), {})()
win.objects.strv = strvar=tk.StringVar()
strvar.trace("w",lambda name, index, mode,strvar=strvar:win.cb(strvar))
root = tk.Tk()
win.objects.label=tk.Label(root,text="label 1")
win.objects.entry=tk.Entry(root,textvariable=strv)
win.button=tk.Button(root,text="action",command=partial(cmd,win))
win.cam = cam(win)
Finally the camera module cam.py in modules folder:
import threading
import cv2 as cv
class camera(threading.Thread):
def __init__(self,win,ini):
self.threads = type('threads', (object,), {})()
threading.Thread.__init__(self,daemon=True)#stops thread on exit
self.queue = queue.Queue()
self.queue.flag = False # Init to a "False" i.e. task is not completed
self.threads.flag = False
self.capture = init_camera() #arbitary function to get the camera with CV
def get_camera():
self.frame = self.capture.read()
return self.frame
My code is several more lines (i.e. more tkinter buttons which update several info fields) and the modules have many more functions, but the gist of what the functions do scale pretty much the same as in this code: Buttons take a window object using partial which has all the variables, objects and flags that I may need. In this case, my question is if it is worth all this internal transporting of my window object, and I should have instead made it a global object.
The code is working perfectly at the moment, the window gets updated with all the running threads, and so far there are no major bugs during runtime. My main question is if this
object passing through modules is something that is pythonic and somewhat good practice, or if I should opt for more global variables (especially the window object which has smaller objects that are frequently updated).
EDIT: Added martineau's correction of terminology.
Related
My python program allows users to make a maze by drag and drop interference. It even searches for the correct path in the maze. However 720 variables need to be defined and binded for the grid where the maze will be built, for this purpose I used a loop which takes some time to work, henceforth I made a loading screen. When the program loads fully, the loading screen closes and it's thread dies but when I close the main window, I get the error(not when the loading screen closes but when I close the main window) Tcl_AsyncDelete: async handler deleted by the wrong thread. Here's a simplified version of my code to show what I'm trying to do:
from tkinter import *
from threading import Thread
loaded = False
def load():
load = Tk()
Label(load, text = "Loading...").pack()
def check():
if loaded:
load.destroy()
else:
load.after(1, check)
load.mainloop()
t = Thread(target = load)
t.start()
root = Tk()
#defining the 720 variables
for x in range(0, 720):
print(x)
exec("var" + str(x) + " = " + str(x))
loaded = True
root.mainloop()
I did read a few other questions on stack overflow, and I found out this happens when you delete the window from another thread(not the main thread), however I am deleting the loading screen from the thread in which it is running itself, not main thread. Any help?
This error specifically is caused by there being multiple instances of Tk in one app. There should always be only one instance of Tk in a tkinter app. If you want to create secondary windows, use Toplevel for them instead of Tk.
But you'll notice that if you do replace load = Tk with load = Toplevel, load doesn't show at all. When using threads in tkinter applications, it's a good idea to keep all of the tkinter stuff in one thread. If you split any tkinter widgets or operations into separate threads, it can cause problems. You should put the for loop inside load(), and put everything that's currently inside load() where the for loop was. This prevents any tkinter function calls or widget creation from happening in two different threads.
Third, I strongly recommend that you don't define variables in a loop like that. It makes it troublesome when trying to access the variables after creating them, and it's slow. For so many numbers, you can create a list and just append the numbers to the list. In my example, the list is called numbers.
Fourth, you will need to use global in your load function, so that the loading variable can be used properly in the thread. You also have two variables named load: the function and the window. You should rename one of them so that one doesn't overshadow the other. In my example, I rename load the window to load_window.
To make the Toplevel show on top, you can set load_window.wm_attributes("-topmost", True).
Here is what the code should look like:
from tkinter import *
# import time # Uncomment this to show the loading window for a little longer
from threading import Thread
loaded = False
numbers = []
def load():
global loaded, numbers
# Add the numbers to the list
numbers = [x for x in range(0, 720)]
# time.sleep(1) # Uncomment this to show the loading window for a little bit longer
loaded = True
root = Tk()
load_window = Toplevel(root)
Label(load_window, text = "Loading...").pack()
t = Thread(target = load)
t.start()
def check():
global loaded, load_window
if loaded:
load_window.destroy()
else:
load_window.after(1, check)
check()
load_window.wm_attributes("-topmost", True)
load_window.mainloop()
root.mainloop()
Note that when you run the code, you get a _tkinter.TclError:
Traceback (most recent call last):
File "/home/user/test.py", line 29, in <module>
load_window.wm_attributes("-topmost", True)
File "/usr/lib/python3.8/tkinter/__init__.py", line 1976, in wm_attributes
return self.tk.call(args)
_tkinter.TclError: bad window path name ".!toplevel"
This is because load() runs so fast that it destroys loaded_window before -topmost is set. As you probably know, calling functions on a destroyed widget raises errors. So this whole loading window would only really be needed with higher numbers. Even when I use range(0, 900000) instead of range(0, 720), the code runs, but it's still so fast that you only see load_window for a split second.
If you still want to see the loading window, you can uncomment the import time and time.sleep(... lines, and it will cause load_window to wait a second before closing.
The goal is to achieve different "screens" in TkInter and change between them. The easiest to imagine this is to think of a mobile app, where one clicks on the icon, for example "Add new", and new screen opens. The application has total 7 screens and it should be able to change screens according to user actions.
Setup is on Raspberry Pi with LCD+touchscreen attached. I am using tkinter in Python3. Canvas is used to show elements on the screen.
Since I am coming from embedded hardware world and have very little experience in Python, and generally high-level languages, I approached this with switch-case logic. In Python this is if-elif-elif...
I have tried various things:
Making global canvas object. Having a variable programState which determines which screen is currently shown. This obviously didn't work because it would just run once and get stuck at the mainloop below.
from tkinter import *
import time
root = Tk()
programState = 0
canvas = Canvas(width=320, height=480, bg='black')
canvas.pack(expand=YES, fill=BOTH)
if(programState == 0):
backgroundImage = PhotoImage(file="image.gif")
canvas.create_image(0,0, image=backgroundImage, anchor=NW);
time.sleep(2)
canvas.delete(ALL) #delete all objects from canvas
programState = 1
elif(programState == 1):
....
....
....
root.mainloop()
Using root.after function but this failed and wouldn't show anything on the screen, it would only create canvas. I probably didn't use it at the right place.
Trying making another thread for changing screens, just to test threading option. It gets stuck at first image and never moves to second one.
from tkinter import *
from threading import Thread
from time import sleep
def threadFun():
while True:
backgroundImage = PhotoImage(file="image1.gif")
backgroundImage2 = PhotoImage(file="image2.gif")
canvas.create_image(0,0,image=backgroundImage, anchor=NW)
sleep(2)
canvas.delete(ALL)
canvas.create_image(0,0,image=backgroundImage2, anchor=NW)
root = Tk()
canvas = Canvas(width=320, height=480, bg='black')
canvas.pack(expand=YES, fill=BOTH)
# daemon=True kills the thread when you close the GUI, otherwise it would continue to run and raise an error.
Thread(target=threadFun, daemon=True).start()
root.mainloop()
I expect this app could change screens using a special thread which would call a function which redraws elements on the canvas, but this has been failing so far. As much as I understand now, threads might be the best option. They are closest to my way of thinking with infinite loop (while True) and closest to my logic.
What are options here? How deleting whole screen and redrawing it (what I call making a new "screen") can be achieved?
Tkinter, like most GUI toolkits, is event driven. You simply need to create a function that deletes the old screen and creates the new, and then does this in response to an event (button click, timer, whatever).
Using your first canvas example
In your first example you want to automatically switch pages after two seconds. That can be done by using after to schedule a function to run after the timeout. Then it's just a matter of moving your redraw logic into a function.
For example:
def set_programState(new_state):
global programState
programState = new_state
refresh()
def refresh():
canvas.delete("all")
if(programState == 0):
backgroundImage = PhotoImage(file="image.gif")
canvas.create_image(0,0, image=backgroundImage, anchor=NW);
canvas.after(2000, set_programState, 1)
elif(programState == 1):
...
Using python objects
Arguably a better solution is to make each page be a class based off of a widget. Doing so makes it easy to add or remove everything at once by adding or removing that one widget (because destroying a widget also destroys all of its children)
Then it's just a matter of deleting the old object and instantiating the new. You can create a mapping of state number to class name if you like the state-driven concept, and use that mapping to determine which class to instantiate.
For example:
class ThisPage(tk.Frame):
def __init__(self):
<code to create everything for this page>
class ThatPage(tk.Frame):
def __init__(self):
<code to create everything for this page>
page_map = {0: ThisPage, 1: ThatPage}
current_page = None
...
def refresh():
global current_page
if current_page:
current_page.destroy()
new_page_class = page_map[programstate]
current_page = new_page_class()
current_page.pack(fill="both", expand=True)
The above code is somewhat ham-fisted, but hopefully it illustrates the basic technique.
Just like with the first example, you can call update() from any sort of event: a button click, a timer, or any other sort of event supported by tkinter. For example, to bind the escape key to always take you to the initial state you could do something like this:
def reset_state(event):
global programState
programState = 0
refresh()
root.bind("<Escape>", reset_state)
hello everyone i have a code for read video from webcam and show it into the tkinter window with timer threading.
when user click show button, app make a thread and run it every x second to show frame by frame.
here's the problem : every several frame app shows first frame that captured from video source.
that's weird i know but i cant find out why!!!
here's my code:
import tkinter as tk
from tkinter import ttk
from tkinter import *
import threading
import cv2
import PIL.Image, PIL.ImageTk
from PIL import Image
from PIL import ImageTk
import time
class App(threading.Thread):
def __init__(self, root, window_title, video_source=0):
self.root = root
self.root.title(window_title)
self.video_source = video_source
self.show_switch=False
self.showButton = Button(self.root, text="PlayStream",command=self.showStram,width=15, padx="2", pady="3",compound=LEFT)
self.showButton.pack()
# Create a canvas that can fit the above video source size
self.canvas = tk.Canvas(root, width = 530, height = 397, cursor="crosshair")
self.canvas.pack()
self.root.mainloop()
def updateShow(self):
# Get a frame from the video source
cap=cv2.VideoCapture(0)
while True:
if(cap.isOpened()):
#read the frame from cap
ret, frame = cap.read()
if ret:
#show frame in main window
self.photo = PIL.ImageTk.PhotoImage(image = PIL.Image.fromarray(frame))
self.canvas.create_image(0, 0, image = self.photo, anchor = tk.NW)
else:
break
raise ValueError("Unable to open video source", video_source)
if self.show_switch==False:
cap.release()
return False
time.sleep(0.0416666666666667)
#release the cap
cap.release()
def showStram(self):
if self.show_switch:
self.showButton["text"]="StartStream"
# self.showButton.configure(image=self.playIcon)
self.show_switch=False
else:
self.showButton["text"]="StopStream"
self.show_switch=True
# self.showButton.configure(image=self.stopIcon)
self.showTimer=threading.Thread(target=self.updateShow,args=())
#self.showTimer.daemon=True
self.showTimer.start()
App(tk.Tk(), "Main")
This is a complicated question, because your example code is extensive and combines multiple things that can go wrong. I cannot test your code in its current form.
First off, you are accessing your Tk instance from a thread that is not the MainThread. This can cause all sorts of issues. There are also bugs present in the implementation of supposed thread-safety in Tkinter, and the solution has not yet been merged. Checkout mtTkinter if you really need to use multiple threads with Tkinter, and even then, it is better not to, especially not if you are building a new application and have the option to use Queues or some other system instead.
Second, you create a (subclass) instance of threading.Thread, but you never call threading.Thread.__init__ in App.__init__ (which is an absolute requirement if you want to use it as a Thread!). Then you create a new Thread in def showStream(self), while you actually already had a thread. Now, this does not break your code, but you do not need to subclass threading.Thread if you do not intend to use your class as a Thread. As you create a Thread in your class, there is no need to make a Thread out of your class.
Then, moving on through your code, you do start the Thread, so updateShow gets run. However, in your while loop, there is a problem:
while True:
if (cap.isOpened()):
ret, frame = cap.read()
if ret:
...
else:
break
# Error will never be raised because of break
# break exits the loop immediately, so the next line is never evaluated
raise ValueError()
cap.release()
# No notification of the loop having ended
There may be two things going wrong here. The first is that maybe your loop just ends because of the break in the else-clause. Break immediately exits the loop code. Anything following it will never be executed. If it does exit the loop because it failed to get the next frame, you cannot know for sure as you do not check whether the thread is still alive (threading.Thread.is_alive) or have any print statements to indicate that the loop has ended.
Secondly, your program might actually be hard-crashing on you accessing Tkinter from a second thread. Doing this causes undefined behaviour, including weird errors and Python-interpreter lockups because the Tk interpreter and Python interpreter are fighting for flow control in a deadlock (simply put).
Last but not least, there is a problem with the way you create your images:
self.canvas.create_image(0, 0, image = self.photo, anchor = tk.NW)
In this line, you create a new image on the Canvas. However, if an image already exists on the Canvas in the location where you are creating a new image, it will appear under the image already shown. If you do not delete your old image (which is in any case advisable to prevent a huge memory leak), it will remain visible on the Canvas.
def __init__(...):
...
self.im = None
def updateShow(self):
...
while True:
if (cap.isOpened()):
...
if ret:
if self.im is not None:
self.canvas.delete(self.im)
...
self.im: str = self.canvas.create_image(0, 0, image=self.photo, anchor=tk.NW)
Summarizing: With your current posted code, there are multiple things that can be going wrong. Without additional information, it is impossible to know. However, if you fix accessing Tkinter from a different Thread, adjust your while loop to not break but raise the error, you adjust your Canvas image creation code and your video source actually does work properly, it should work out.
We have a functioning program that uses Tkinter as its GUI. Everything works fine however different branches of the code are now using different hardware which realistically need different buttons. Hence we'd like to have the main GUI import modules representing the buttons depending on what hardware is being used.
I've cut out some of the code below, I'm interested in removing the makemenu() function to a separate module, hence when it is called in the Application __init__ (self.makemenu(master)) I would like to make that a reference to a separate module. I've tried doing this and am having trouble. Is this even possible?
I'm a little confused on the parent structure, what needs to be passed to my button module, etc.? I know this is a poorly constructed question but if anyone is able to advise if this is possible and put my on the right track that would be great. For example if someone could show how to modify this code to have the buttons defined in a separate module I could figure out how to do the same in my module.
# Import necessary libraries
import sys
import os
import Tkinter as tk
class Application(tk.Frame):
##################################################################
## Final functions are designed to initialize the GUI and
## connect various mouse movements to useful functions.
##################################################################
def definevars(self):
'''Original definition of all of the key variables that
we need to keep track of while running the GUI
'''
self.disable = True
self.savimgstatus = 'off'
self.mode = 'Standby'
self.status = 'Not Ready'
def makemenu(self,master):
''' Function to create the main menu bar across
the top of the GUI.
'''
self.menubar = tk.Menu(master)
## Motor Submenu
motormenu = tk.Menu(self.menubar,tearoff=1)
motormenu.add_command(label='ALT',state='disabled')
motormenu.add_command(label='error check',
command=lambda: self.geterror('alt'))
motormenu.add_separator()
motormenu.add_command(label='AZ',state='disabled')
motormenu.add_command(label='error check',
command=lambda: self.geterror('az'))
self.menubar.add_cascade(label='Tracker Motors',menu=motormenu)
## Set the big menu as the main menu bar.
master.config(menu=self.menubar)
def __init__(self,tcpconn,DOME,TRACKERSTAGE, master=None):
'''Main function to initialize the GUI. Will scale
the size of the GUI to fit any size screen... to a
point. It will not allow it to be smaller than
600x800.
'''
self.buf = 1024
## Check resolution of screen. Make GUI 2/3rds of size
## unless that means under 600x800.
fh = round(master.winfo_screenheight()*2./3.)
fw = round(master.winfo_screenwidth()*2./3.)
if fh < 600: fh = 600
if fw < 800: fw = 800
print 'GUI resolution set to {0} x {1}'.format(fw,fh)
self.fw = fw
self.fh = fh
self.imwidth = int(0.45*self.fw)
self.imheight = int(0.45*self.fh)
self.imcentx = self.imwidth/2
self.imcenty = self.imheight/2this
## Initialize Frame
tk.Frame.__init__(self, master, height=fh,width=fw)
self.grid()
self.grid_propagate(0)
## Initialize Various variables.
self.definevars()
## Create buttons, etc.
self.createWidgets()
self.makemenu(master)
self.disableall()
## Main Loop function
self.checkoutput()
###################################################################
# Initialize GUI window.
root = tk.Tk()
root.title('Hardware') # window title
app = Application(master=root)
app.mainloop() # go into the main program loop
sys.exit()
If you want to move makemenu to a separate module, that should be pretty simple. However, you'll need to change a few things.
Since makemenu no longer has a reference to self (or has a different reference, if you implement it as a separate class), you need to replace calls like command=lambda: self.geterror('alt')) to be command=lambda: master.geterror('alt')).
The other thing I recommend is to remove the call to add the menu to the root. I believe that modules shouldn't have side effects like this -- the function should make a menu and return it, and let the caller decide how to use it, ie:
self.menubar=makemenu(master)
master.configure(menu=self.menubar)
Roughly speaking, this is a variation of the MVC (model/view/controller) architectural pattern where the Application instance is your controller (and also part of the view unless you make modules of all your UI code). The menu is part of the view, and forwards UI functions to the controller for execution.
Your application then looks something like this:
from makemenu import makemenu
class Application(...):
def __init__(...):
...
self.menubar = makemenu(master)
master.config(menu=self.menubar)
...
I have written a short module that can be passed an image and simply creates a Tkinter window and displays it. The problem that I am having is that even when I instantiate and call the method that displays the image in a separate thread, the main program will not continue until the Tkinter window is closed.
Here is my module:
import Image, ImageTk
import Tkinter
class Viewer(Tkinter.Tk):
def __init__(self,parent):
Tkinter.Tk.__init__(self,parent)
self.parent = parent
self.initialize()
def initialize(self):
self.grid()
def show(self,img):
self.to_display = ImageTk.PhotoImage(img)
self.label_image = Tkinter.Label(self,image=self.to_display)
self.label_image.grid(column = 0, row = 0, sticky = "NSEW")
self.mainloop()
It seems to work fine, except when I call it from my test program like the one below, it will not seem to allow my test program to continue, even when started in a different thread.
import Image
from viewer import Viewer
import threading
def showimage(im):
view = Viewer(None)
view.show(im)
if __name__ == "__main__":
im = Image.open("gaben.jpg")
t = threading.Thread(showimage(im))
t.start()
print "Program keeps going..."
I think that perhaps my problem is that I should be creating a new thread within the module itself, but I was wanting to just try and keep it simple, as I am new to Python.
Anyway, thanks in advance for any assistance.
edit: To clarity, I am just trying to make a module that will display an image in a Tkinter window, so that I can use this module any time I want to display an image. The problem that I am having is that any time a program uses this module, it cannot resume until the Tkinter window is closed.
Tkinter isn't thread safe, and the general consensus is that Tkinter doesn't work in a non-main thread. If you rewrite your code so that Tkinter runs in the main thread, you can have your workers run in other threads.
The main caveat is that the workers cannot interact with the Tkinter widgets. They will have to write data to a queue, and your main GUI thread will have to poll that queue.
If all you're doing is showing images, you probably don't need threading at all. Threading is only useful when you have a long running process that would otherwise block the GUI. Tkinter can easily handle hundreds of images and windows without breaking a sweat.
From your comments it sound's like you do not need a GUI at all. Just write the image to disk and call an external viewer.
On most systems it should be possible to launch the default viewer using something like this:
import subprocess
subprocess.Popen("yourimage.png")
From what I can tell, Tkinter doesn't like playing in other threads. See this post...I Need a little help with Python, Tkinter and threading
The work around is to create a (possibly hidden) toplevel in your main thread, spawn a separate thread to open images, etc - and use a shared queue to send messages back to the Tk thread.
Are you required to use Tkinter for your project? I like Tkinter. It's "quick and dirty." - but there are (many) cases where other GUI kits are the way to go.
I have tried to run tkinter from a separate thread, not a good idea, it freezes.
There is one solution that worked. Run the gui in the main thread, and send events to the main gui. This is similar example, it just shows a label.
import Tkinter as t
global root;
root = t.Tk()
root.title("Control center")
root.mainloop()
def new_window(*args):
global root
print "new window"
window = t.Toplevel(root)
label = t.Label(window, text="my new window")
label.pack(side="top", fill="both", padx=10, pady=10)
window.mainloop()
root.bind("<<newwin>>",new_window)
#this can be run in another thread
root.event_generate("<<newwin>>",when="tail")