tkinter button function calls: multiple presses calling same function multiple times - python

I'm trying to make a Raspberry Pi Media Player, using omxplayer and tkinter.
I grab upto 16 video from a USB drive(or in local folder) and display them as thumbnails on a grid of buttons. When a button is pressed, it plays that video with omxplayer in fullscreen(at which point you cant access the tkinter window anymore until the video ends). Basically I want to enable selection of video only when there isn't a video playing already.
The problem I'm facing is with multiple presses on one button, or presses on other buttons before the video gets to play. This causes all the videos to queue up and immediately play one after another. I don't want subsequent presses to register until the first video (played from the first button press) is done playing. I try to set a boolean variable video_is_playing, and check its state in the function call, but it never enters the else case.
I tried to disable all buttons after you press any of them, then enabling them all after the video is done playing, I tried to use a variable to check if a video is done playing, and pretty much anything else I could think of doing to prevent multiple button presses.
Here is some parts of my code (sorry if it seems long, but I think everything included is relevant):
class TkinterGUI:
def __init__(self):
self.folder_name="videos"
self.vid_path = f"{os.path.split(os.getcwd())[0]}/{os.path.split(os.getcwd())[1]}/{self.folder_name}/"
self.videos = []
self.video_is_playing = False
self.vidbuttons = []
for f in os.listdir(f"{self.vid_path}"):
if '.mp4' in f:
self.videos.append(f)
self.videos.sort()
self.videos_iterator = iter(self.videos)
def pack_thumbnail(self, path, button):
#putting video thumbnail in button with imageio
pass
def disable_buttons(self, window):
for b in self.vidbuttons:
b.config(state=tk.DISABLED)
window.update()
print(">>all buttons diabled")
def enable_buttons(self, window):
for b in self.vidbuttons:
b.config(state=tk.NORMAL)
window.update()
print(">>all buttons enabled")
def play_vid(self, i, j, window):
try:
self.disable_buttons(window)
if self.video_is_playing == False:
self.video_is_playing=True
k = (i*4)+j
video = self.videos[k]
path = f"{self.vid_path}/{video}"
print(f">>now playing: {video} of duration {self.vid_duration(path)}")
omxp = Popen(['omxplayer', path])
omxp.wait()
print(f"video {video} is done playing!")
else:
print("a video seems to be playing already")
return
except Exception as e:
print(e)
finally:
self.video_is_playing = False
self.enable_buttons(window)
def video_player_window(self):
window = tk.Tk()
window.attributes("-fullscreen", True)
#left side frame(blank for now)
frame1 = tk.Frame(master=window, width=200, height=100, bg="white")
frame1.pack(fill=tk.Y, side=tk.LEFT)
#main video player frame(contains 4x4 grid of video thumbnails)
frame2 = tk.Frame()
for i in range(4):
frame2.columnconfigure(i, weight=1, minsize=75)
frame2.rowconfigure(i, weight=1, minsize=50)
for j in range(4):
frame = tk.Frame(master=frame2, relief=tk.FLAT, borderwidth=1)
frame.grid(row=i, column=j, padx=5, pady=5)
vid=next(self.videos_iterator, "end")
print(vid)
if vid != "end":
button = tk.Button(master=frame, highlightcolor="black", text=f"Row {i}\nColumn {j}", command= partial(self.play_vid, i, j, window))
self.pack_thumbnail(self.vid_path+f"{vid}", button)
button.pack(padx=5, pady=5)
self.vidbuttons.append(button)
else:
img = Image.open(f"vidnotfound.png")
img = img.resize((424, 224))
image = ImageTk.PhotoImage(img)
label = tk.Label(master=frame, text=f"Row {i}\nColumn {j}", image=image)#, compound='center')
label.image = image
label.pack(padx=5, pady=5)
frame2.pack()
window.bind("<Escape>", lambda x: window.destroy())
window.mainloop()
tkin = TkinterGUI()
tkin.video_player_window()
I used functools.partial() to pass i, j indices to the play_vid function, so I can use those indices to know which video from the list to play. Here is everything I imported:
import tkinter as tk
import imageio
from PIL import ImageTk, Image
from pathlib import Path
from functools import partial
import subprocess
from subprocess import Popen
on a side note: is there a better way to accomplish what I want to do with the button grid? I'd like each button to call the same function but play different videos, is there any attribute I could use or anything?

Related

How can I Iterate through a list of Images and Display them in Tkinter

So, my goal is to create a sort of slideshow within Tkinter. I have a list of images like Images = ["1.png", "2.png", ...], I want to be able to iterate through the list and display each image in a Tkinter window, the concept is simple and as follows:
Display Image 1
30 Second Delay
Display Image 2
30 Second Delay
Display Image 3
I have managed to iterate the images using a button press, however, I do not want to have to click a button as it is meant to imitate a slideshow, I also attempted looping a function but the time.sleep() function does not delay in the correct way because of how Tkinter behaves.
I managed to achieve the above using mostly source code from here, and I would appreciate a little hand achieving the above.
My Code:
from tkinter import *
from PIL import ImageTk, Image
Window = Tk()
Window.geometry("1920x1080")
Window.resizable(0, 0)
Label1 = Label(Window)
Label1.pack()
Images = iter(["1.png", "2.png", "3.png", "4.png", "5.png",
"6.png", "7.png", "8.png", "9.png", "10.png"])
def Next_Image(Val):
try:
Image1 = next(Images)
except StopIteration:
return
Image1 = ImageTk.PhotoImage(Image.open(Image1))
Label1.Image = Image1
Label1["image"] = Image1
Button1 = Button(text = "Next image", command =
lambda:Next_Image(1))
Button1.place(x = 50, y = 50)
Next_Image(1)
Window.mainloop()
I also attempted to use .after(), however, it did not display each image, it skipped from the first image to the last straight away with the compounded delay.
for x in range(1, 11):
Window.after(1000, lambda : Next_Image(1))
You need to create a function that gets the image off of the list and displays it, and then uses after to call itself again in a second. Your main program needs to call this exactly once, and then it will run until it runs out of things to do.
Here's a working example that uses a text string for simplicity, but it should be obvious how to modify it to use images.
import tkinter as tk
images = iter(["1.png", "2.png", "3.png", "4.png", "5.png",
"6.png", "7.png", "8.png", "9.png", "10.png"])
def next_image():
try:
image = next(images)
label.configure(text=image)
root.after(1000, next_image)
except StopIteration:
return
root = tk.Tk()
label = tk.Label(root, width = 40, height=4)
label.pack()
next_image()
root.mainloop()
You can use .after() to switch image periodically:
from itertools import cycle
...
# use cycle() instead of iter()
Images = cycle([f"{i}.png" for i in range(1, 5)])
...
def next_image():
# use next() to get next image in the cycle list
Label1.image = ImageTk.PhotoImage(file=next(Images))
Label1['image'] = Label1.image
# switch image again after 1 second
Label1.after(1000, next_image)
next_image() # start the loop
Window.mainloop()
This worked, Thank you #acw1668 and #Bryan Oakley.
from tkinter import *
from PIL import ImageTk, Image
Window = Tk()
Window.geometry("1920x1080")
Window.resizable(0, 0)
Label1 = Label(Window)
Label1.pack()
Images = iter(["1.png", "2.png", "3.png", "4.png", "5.png", "6.png",
"7.png", "8.png", "9.png", "10.png"])
def Next_Image(Val):
try:
Image1 = next(Images)
except StopIteration:
return
Image1 = ImageTk.PhotoImage(Image.open("BuyingConfig\\" + Image1))
Label1.Image = Image1
Label1["image"] = Image1
Window.after(3000, lambda:Next_Image(1))
Window.after(0, lambda:Next_Image(1))
Window.mainloop()

GUI for On / Off Display using Tkinter Python

I am trying to create a function where if I press "1" my image changes, but if I press 1 again the image reverts back to how it was. This process should go on indefinitely.
I am able to do this if the user clicks on a button. For example, the below code simply uses config to change the function that should be run each time button is clicked.
# run this "thing" each time seat is activated
def activate(name):
name.config(image=active)
name.config(command=lambda: deactivate(name))
# run this "thing" each time seat is deactivated
def deactivate(name):
name.config(image=deactive)
name.config(command=lambda: activate(name))
x = Button(root, image=deactive, command=lambda: thing(x))
But I am unable to achieve the same if I bind a key instead:
from tkinter import *
# window config
root = Tk()
root.title("Example - on / off")
root.geometry("1080x600")
# on and off images
inactive = PhotoImage(file='green.png')
active = PhotoImage(file='red.png')
# functions to change the image using config
def something(event):
my_label.config(image=active)
def something2():
my_label.config(image=inactive)
# label which stores the image
my_label = Label(root, image=inactive)
my_label.pack()
# invisible button bind to key "1"
my_button = Button(root)
my_button.bind_all(1, something)
my_button.pack()
root.mainloop()
I would welcome any thoughts on this or an indication if I am approaching this completely wrong.
Thanks
You can simplify the logic by using itertools.cycle() and next() functions:
from tkinter import *
from itertools import cycle
root = Tk()
root.title("Example - on / off")
root.geometry("1080x600")
# on and off images
images = cycle((PhotoImage(file='green.png'), PhotoImage(file='red.png')))
def something(event):
my_label.config(image=next(images))
my_label = Label(root, image=next(images))
my_label.pack()
root.bind('1', something)
root.mainloop()

Create functions to pack one widget and remove all others when different keys are pressed

I am making a controller for a car with Python and I was going to have 3 separate images to represent whether the wheels are turning left, right, or neutral. I need only one of these images to be shown at a time.
So far I have used bind to trigger functions because I haven't seen any other way to do so. I have looked into pack and pack_forget but I don't know how I could trigger them to be activated by other widgets (since I am using bind).
import tkinter as tk
win = tk.Tk()
def forwards(event):
print("going forwards...")
def left(event):
print("turning left...")
def right(event):
print("turning right...")
def backwards(event):
print("going backwards...")
neutralImage = tk.PhotoImage(file="neutral.gif")
leftImage = tk.PhotoImage(file="turnedLeft.gif")
rightImage = tk.PhotoImage(file="turnedRight.gif")
neutralPosition = tk.Label(win, image=neutralImage)
leftPosition = tk.Label(win, image=leftImage)
rightPosition = tk.Label(win, image=rightImage)
win.bind("w", forwards)
win.bind("a", left)
win.bind("d", right)
win.bind("s", backwards)
I have identified the problem as the following: I can't hide or show the widgets unless it is them that I press the button over.
Instead of having three widgets what you can do is replace the image of the same widget when you need it.
import tkinter as tk
def changeImage(imageLabelWidget, newImage):
imageLabelWidget.configure(image=newImage)
imageLabelWidget.image = newImage
win = tk.Tk()
neutralImage = tk.PhotoImage(file="neutral.gif")
leftImage = tk.PhotoImage(file="turnedLeft.gif")
rightImage = tk.PhotoImage(file="turnedRight.gif")
neutralPosition = tk.Label(win, image=neutralImage)
neutralPosition.image = neutralImage
neutralPosition.pack()
win.bind("w", lambda event, imageLabelWidget=neutralPosition, newImage=neutralImage:
changeImage(imageLabelWidget, newImage))
win.bind("a", lambda event, imageLabelWidget=neutralPosition, newImage=leftImage:
changeImage(imageLabelWidget, newImage))
win.bind("d", lambda event, imageLabelWidget=neutralPosition, newImage=rightImage:
changeImage(imageLabelWidget, newImage))
win.mainloop()

I don't know why this simple piece of python code isn't running

My idea in this code is running an app with Tkinter that 'lights on" a Seven Segment Display depending on which number I press on my keyboard.
import tkinter as tk
import keyboard
import time
from PIL import ImageTk, Image
def main():
window = tk.Tk()
window.title("AutoSegment")
window.geometry("459x767")
path=r"C:\Users\The Man Himself\Desktop\SSG\welcome.jpg"
img = ImageTk.PhotoImage(Image.open(path))
panel = tk.Label(window, image = img).pack(side = "bottom", fill = "both", expand = "yes")
listener()
tk.mainloop()
def set(name):
path=r"C:\Users\The Man Himself\Desktop\SSG\%s.jpg" %name
img = ImageTk.PhotoImage(Image.open(path))
panel = tk.Label(window, image = img).pack(side = "bottom", fill = "both", expand = "yes")
listener()
tk.mainloop()
def listener():
while True:
try:
if keyboard.is_pressed('1'):
set("1")
break
elif keyboard.is_pressed('2'):
set("2")
break
elif keyboard.is_pressed('3'):
set("3")
break
elif keyboard.is_pressed('4'):
set("4")
break
elif keyboard.is_pressed('5'):
set("5")
break
elif keyboard.is_pressed('6'):
set("6")
break
elif keyboard.is_pressed('7'):
set("7")
break
elif keyboard.is_pressed('8'):
set("8")
break
elif keyboard.is_pressed('9'):
set("9")
break
elif keyboard.is_pressed('0'):
set("0")
break
except:
set("error")
main()
I have not worked with the keyboard module, but I can show you how to work without it.
A couple of things; window is created inside a function which means that the name window is local to that function. Instead create the window in the global scope. Also the function set() is a builtin function and if you redefine it you will not be able to access the builtin function. I have called it set_display() instead.
As you will change the image in panel it's better to create it in the global namespace. Also, to be able to change it you must keep a reference, i.e. give it the name panel and then pack it. Otherwise the name panel will point to the return value from pack() which is = None.
When you later change the image in the label in the function set_display() you must also save a reference to the image in the label, explicitly commented in my example code.
Then I use bind() to hook the keyboard which is a standard method in tkinter widgets. After that I start mainloop() which waits until a key is pressed and then calls keypress().
import tkinter as tk
from PIL import ImageTk, Image
def set_display(name):
path = r"C:\Users\The Man Himself\Desktop\SSG\%s.jpg" %name
img = ImageTk.PhotoImage(Image.open(path))
panel.config(image=img) # Load new image into label
panel.image = img # Save reference to image
def keypress(event):
if event.char == '': # Shift, ctrl etc, returns empty char
set_display('error')
elif event.char in '1234567890': # Hook all numbers
set_display(event.char)
else:
set_display('error')
window = tk.Tk()
window.title("AutoSegment")
window.geometry("459x767")
# Create Seven Segment Display label in global namespace
path = r"C:\Users\The Man Himself\Desktop\SSG\welcome.jpg"
img = ImageTk.PhotoImage(Image.open(path))
panel = tk.Label(window, image=img)
panel.pack(side="bottom", fill="both", expand="yes")
window.bind('<KeyPress>', keypress)
window.mainloop()

Exit Tks mainloop in Python?

I'm writing a slideshow program with Tkinter, but I don't know how to go to the next image without binding a key.
import os, sys
import Tkinter
import Image, ImageTk
import time
root = Tkinter.Tk()
w, h = root.winfo_screenwidth(), root.winfo_screenheight()
root.overrideredirect(1)
root.geometry("%dx%d+0+0" % (w, h))
root.focus_set()
root.bind("<Escape>", lambda e: e.widget.quit())
image_path = os.path.join(os.getcwd(), 'images/')
dirlist = os.listdir(image_path)
for f in dirlist:
try:
image = Image.open(image_path+f)
tkpi = ImageTk.PhotoImage(image)
label_image = Tkinter.Label(root, image=tkpi) # ?
label_image.place(x=0,y=0,width=w,height=h)
root.mainloop(0)
except IOError:
pass
root.destroy()
I would like to add a time.sleep(10) "instead" of the root.mainloop(0) so that it would go to the next image after 10s. Now it changes when I press ESC. How can I have a timer there?
edit: I should add that I don't want another thread that does a sleep even though it works.
You can try
root.after(10*1000, root.quit)
There's no need to do a loop over your images -- you're already running in a loop (mainloop) so take advantage of it. The typical way to do this is to create a method that draws something, waits for a period of time, then calls itself. This isn't recursion, it's just telling the main loop "after N seconds, call me again".
Here's a working example:
import glob
import Tkinter
class Slideshow:
def __init__(self, pattern="*.gif", delay=10000):
root = Tkinter.Tk()
root.geometry("200x200")
# this label will be used to display the image. Make
# it automatically fill the whole window
label = Tkinter.Label(root)
label.pack(side="top", fill="both", expand=True)
self.current_image = None
self.image_label = label
self.root = root
self.image_files = glob.glob(pattern)
self.delay = delay # milliseconds
# schedule the first image to appear as soon after the
# the loop starts as possible.
root.after(1, self.showImage)
root.mainloop()
def showImage(self):
# display the next file
file = self.image_files.pop(0)
self.current_image = Tkinter.PhotoImage(file=file)
self.image_label.configure(image=self.current_image)
# either reschedule to display the file,
# or quit if there are no more files to display
if len(self.image_files) > 0:
self.root.after(self.delay, self.showImage)
else:
self.root.after(self.delay, self.root.quit)
def quit(self):
self.root.quit()
if __name__ == "__main__":
app=Slideshow("images/*.gif", 1000)

Categories