A common problem with Tkinter is that in order to use images in Labels and Buttons, you need a reference to the PhotoImage object... somehow.
I have written a wrapper class around Button to add my own functionalities, because I want to use GIFs instead of images, and I want to be able to switch between gifs when I press the button (or use a keyboard hotkey). The first GIF runs fine and loops perfectly. When I switch to the second GIF, I get the error message, saying _tkinter.TclError: image "pyimage48 ... pyimage55" doesn't exist. It looks like the following:
from tkinter import *
from PIL import ImageTk, Image
class AnimatedButton(Button)
def __init__(self, master, size, img_paths):
self.size = size
self.seq_count = len(img_paths) # Number of gif files
self.sequences = []
for path in img_paths:
gif, delay = loadGif(path)
# Create a tuple of all frames in a gif, with the delay between frames. Store this tuple in self.sequences
self.sequences.append(([ImageTk.PhotoImage(frame) for frame in gif], delay))
self.delay = self.sequences[0][1]
self.current_sequence = self.sequences[0][0]
self.image = self.current_sequence[0]
self.seq_id = 0 # Sequence counter
self.frame_id = 0 # Frame counter
Button.__init__(self, master, image=self.image, width=size, height=size)
self.cancel = self.after(self.delay, self.play)
def play(self):
self.image = self.current_sequence[self.frame_id]
self.config(image=self.image)
# More stuff below to loop through the frames etc.
What is strange is that I don't have any of this with my other Button class, MyButton, also a wrapper class.
class MyButton(Button):
def __init__(self, master, size, img_paths):
self.image_count = len(img_paths)
self.image_id = 0
self.size = size
self.images = []
for path in img_paths:
try:
im = Image.open(path)
except:
print("Could not open file {}".format(path))
photo_image = ImageTk.PhotoImage(im, image_mode)
self.images.append(photo_image)
self.image = self.images[0]
Button.__init__(self, master, image=self.image, width=size,
height=size)
Most Google searches came up with the fact that you shouldn't use two tkinter.Tk() calls, but I am only using one (Yes, I made sure).
Any ideas are very much appreciated!
Thanks to the hint by stovfl in the comments above, I was missing [0] in play():
Correct code should be:
def play(self):
self.image = self.current_sequence[self.frame_id][0]
self.config(image=self.image)
# More stuff below to loop through the frames etc.
Related
I have 2 classes, first one is Game where's located the main window and its settings, the second one is Piece that have some parameters to establish a Button with an image on its surface using Pillow. From the main class Game I planned call the other class to create objects on this window(root). Everything works fine(1st pic) untill I call my setter from any of these classes to change the image of the Button(Piece), it doesn't appear though the path to the new file is absolutely correct and the button becomes enabled if I'm not wrong and it's not clickable (2nd pic)
How can I resolve the problem, cos I need this possibility to change pictures on the button for my tic-toe game?
from tkinter import *
from PIL import Image
from PIL import ImageTk
class Game:
ICON = "./rsc/logo.ico"
KREST_IMAGE = "./rsc/krest.png"
DEFAULT_IMAGE = "./rsc/1.png"
def __init__(self):
self.root = Tk()
self.root.title("Tic-tac-toe")
self.root.iconbitmap(Game.ICON)
self.root.resizable(False,False)
self.root.geometry("300x300+500+500")
self.c = Piece()
self.root.mainloop()
class Piece:
def __init__(self):
self.img = Game.DEFAULT_IMAGE
self.piece = Button(image = self._img)
self.img = Game.KREST_IMAGE
# for ex. calling again the setter. Writing direct path instead of Game.KREST_IMAGE doesn't help.
self.piece.pack()
#property
def img(self):
return self._img
#img.setter
def img(self, path):
self.temp = Image.open(path)
self.temp = self.temp.resize((20,20), Image.ANTIALIAS)
self.temp = ImageTk.PhotoImage(self.temp)
self._img = self.temp
the picture
I tried to create a slideshow in python to loop over 5500 images faster than I could manually. I used tkinter and the parameter slide_interval should do the job. The slideshow will indeed be longer if I set slide_interval=5000 for example, but it makes no differnce if I set it to 500,50 or 5, it will still take approximately same number of seconds to display each image, while what I would be interested in would be 1 or 0.5 seconds spent per image.
Here is the code:
#!/usr/bin/env python3
"""Display a slideshow from a list of filenames"""
import os
import tkinter
from itertools import cycle
from PIL import Image, ImageTk
class Slideshow(tkinter.Tk):
"""Display a slideshow from a list of filenames"""
def __init__(self, images, slide_interval):
"""Initialize
images = a list of filename
slide_interval = milliseconds to display image
"""
tkinter.Tk.__init__(self)
self.geometry("+0+0")
self.slide_interval = slide_interval
self.images = None
self.set_images(images)
self.slide = tkinter.Label(self)
self.slide.pack()
def set_images(self, images):
self.images = cycle(images)
def center(self):
"""Center the slide window on the screen"""
self.update_idletasks()
w = self.winfo_screenwidth()
h = self.winfo_screenheight()
size = tuple(int(_) for _ in self.geometry().split('+')[0].split('x'))
x = w / 2 - size[0] / 2
y = h / 2 - size[1] / 2
self.geometry("+%d+%d" % (x, y))
def set_image(self):
"""Setup image to be displayed"""
self.image_name = next(self.images)
filename, ext = os.path.splitext(self.image_name)
self.image = ImageTk.PhotoImage(Image.open(self.image_name))
def main(self):
"""Display the images"""
self.set_image()
self.slide.config(image=self.image)
self.title(self.image_name)
self.center()
self.after(self.slide_interval, self.start)
def start(self):
"""Start method"""
self.main()
self.mainloop()
if __name__ == "__main__":
slide_interval = 1
import glob
images = glob.glob("traralgon/*.jpg")
# start the slideshow
slideshow = Slideshow(images, slide_interval)
slideshow.start()
Something like this maybe:
import tkinter as tk
class Application(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.title("Slideshow")
self.geometry("256x256")
self.resizable(width=False, height=False)
self.current_slide = tk.Label(self)
self.current_slide.pack()
self.duration_ms = 1000
def set_image_directory(self, path):
from pathlib import Path
from PIL import Image, ImageTk
from itertools import cycle
image_paths = Path(path).glob("*.jpg")
self.images = cycle(zip(map(lambda p: p.name, image_paths), map(ImageTk.PhotoImage, map(Image.open, image_paths))))
def display_next_slide(self):
name, self.next_image = next(self.images)
self.current_slide.config(image=self.next_image)
self.title(name)
self.after(self.duration_ms, self.display_next_slide)
def start(self):
self.display_next_slide()
def main():
application = Application()
application.set_image_directory("dir/to/images")
application.start()
application.mainloop()
if __name__ == "__main__":
import sys
sys.exit(main())
You'll have to ask yourself if you want to load all images upfront before the slideshow starts, and just keep them in memory (this could take some time depending on how many images you have), or if you want to load images only when they should be displayed (if the interval between images is especially short, you may notice that loading the next image slows things down).
The problem is that you're calling self.main() and self.mainloop() on every interval. That will cause huge performance problems and will probably crash after only a second or two. You should never call mainloop() more than once, and there's no point in recreating the entire UI on every loop.
Instead, you need to write a function that gets the image and then configures the existing label rather than recreating the whole GUI on each iteration.
Example:
def main(self):
...
self.next_image()
def next_image(self):
self.image_name = next(self.images)
filename, ext = os.path.splitext(self.image_name)
self.image = tkinter.PhotoImage(file=self.image_name)
self.slide.configure(image=self.image)
self.after(self.slide_interval, self.next_image)
I am making a game for the repl.it game jam. I am trying to put an image on a canvas that was created in a different class from the class that the image is being created on. The canvas has successfully displayed text and a button with the image that I need, but create_image is not working.
This might be an issue with resolution, but I cannot check that right now, the image is 1920 x 1080 and so is the game.
I've already tried create_image.
Full program (including images)
class game(Application):
"""Where game elements are to be held"""
def __init__(self, players, location, sWidth, sHeight, canvas):
"""Initiate variables & start game"""
#Initiate variables
self.players = players
self.location = location
self.screenWidth = sWidth
self.screenHeight = sHeight
self.Canvas1 = canvas
self.LOCATIONS = Application.LOCATIONS
self.font = Application.font
#Gathering Images
self.map1BG = PhotoImage(file = "polasib.gif")
#Debugging
print("Loading Map", self.location\
, "\nPlayers:", self.players)
self.createLevel(self.location)
def createUI(self, players):
"""Creates the UI that all levels will have"""
self.Canvas1.create_text(self.screenWidth/2, self.screenHeight/16, fill = "white", \
font = (self.font, self.screenWidth//34), text = self.LOCATIONS[self.location - 1])
def createLevel(self, location):
"""Creates the elements of the level"""
if self.location == 1:
#Polasi b
print("Creating Polasi b Level")
self.createUI(self.players)
self.Canvas1.create_image(self.screenWidth/2, self.screenHeight/2, image = self.map1BG, \
anchor = NW)
Expectation: I expect the image to load (and that it will require some realignment)
Result: No image appears but everything else added (as a test) works.
Since you did not save the reference to the instance of game, it will be destroyed after exiting Application.gameScreen(). Therefore the reference of the image of create_image will be lost and no image will be shown. Try assigning the instance of game to an instance variable of Application, like below:
self.running_game = game(self.players, self.mapChosen, self.screenWidth, self.screenHeight, self.Canvas1)
Each OS has an activity indicator. OS X and iOS has a flower that lights up and fades each pedal in a circular pattern. Windows has a spinning blue disk thing. Android has a gray disk thing (I think it can be in a variety of other colors, too - IDK, I don't use Android much.)
What's the best way to use these icons in Tkinter? Is there some built in widget that provides this? Is there maybe a variable that I can point an Image widget at to get it to display this icon (and animate it?)
I know Tkinter provides a Progress Bar, which has an indeterminate mode. I don't want to use that - I need something that fits in a small square area, not a long rectangular area. The activity indicator as described in the first paragraph will be perfect.
Or is my best option going to be to just roll my own canvas animation?
This should be enough for what you trying to do. I would than create my own animation or download from the internet frame by frame which you want to display. Canvas are more for interactivity with the user thats why I am using a label for displaying the images...
import tkinter as tk
class Activity(tk.Label):
def __init__(self, master = None, delay = 1000, cnf = {}, **kw):
self._taskID = None
self.delay = delay
return super().__init__(master, cnf, **kw)
# starts the animation
def start(self):
self.after(0, self._loop)
# calls its self after a specific <delay>
def _loop(self):
currentText = self["text"] + "."
if currentText == "....":
currentText = ""
self["text"] = currentText
self._taskID = self.after(self.delay, self._loop)
# stopps the loop method calling its self
def stop(self):
self.after_cancel(self._taskID)
# Depends if you want to destroy the widget after the loop has stopped
self.destroy()
class AcitivityImage(Activity):
def __init__(self, imagename, frames, filetype, master = None, delay = 1000, cnf = {}, **kw):
self._frames = []
self._index = 0
for i in range(frames):
self._frames.append(tk.PhotoImage(file = imagename+str(i)+'.'+str(filetype)))
return super().__init__(master, delay, cnf, **kw)
def _loop(self):
self["image"] = self._frames[self._index]
# add one to index if the index is less then the amount of frames
self._index = (self._index + 1 )% len(self._frames)
self._taskID = self.after(self.delay, self._loop)
root = tk.Tk()
root.geometry("500x500")
# create a activity image widget
#root.b = AcitivityImage("activity", 3, "png", root)
#root.b.pack()
# create the activity widget
root.a = Activity(root, 500, bg = "yellow")
root.a.pack()
# start the loop
root.a.start()
#root.b.start()
# stop the activity loop after 7 seconds
root.after(7000, root.a.stop)
#root.after(8000, root.b.stop)
root.mainloop().
i'm new on Tkinter and i'm trying to make an animated button.
I'm using the enter-leave events but the click on button it's not responding very well.
My code is:
imagePath = "Resources/"
imagelist = ["boton_1.gif","boton_2.gif","boton_3.gif","boton_4.gif","boton_5.gif","boton_6.gif",
"boton_7.gif","boton_8.gif","boton_9.gif","boton_10.gif","boton_11.gif","boton_12.gif",
"boton_13.gif","boton_14.gif","boton_15.gif","boton_16.gif"]
giflist = []
for imagefile in imagelist:
photo = PhotoImage(file=imagePath+imagefile)
giflist.append(photo)
self.photo=giflist[0]
button = Button(buttonFrame, image=self.photo,background='orange',activebackground='lightsalmon',
command=lambda: controller.show_frame(ListPlayerPage))
button.pack(pady=5)
def enter(event):
self.clickOnButton1 = True
for i in range(1,8):
button.config(image=giflist[i])
button.update()
time.sleep(0.1)
if self.clickOnButton1 == False:
break
while (self.clickOnButton1):
for i in range (9,15):
button.config(image=giflist[i])
button.update()
time.sleep(0.08)
if self.clickOnButton1 == False:
break
def leave(event):
self.clickOnButton1 = False
button.config(image=self.photo)
button.update()
button.bind("<Enter>",enter)
button.bind("<Leave>",leave)
Thanks!!
Part of the problem is definitely related to the fact you're calling sleep. As a good rule of thumb you should never call sleep in the main thread of a GUI. It prevents the GUI from processing all events, including screen refreshes.
Generally speaking, you should also avoid calling update. It can result in nested event loops, if during the processing of update you end up calling a method that again calls update.
Here's a really simple example of solution that creates a button that can be animated. It uses after to iterate over a list of text strings, one new string every half second. This example will animate forever, but you can easily have it show each item only once. This modifies the text to make the example shorter, but you can easily modify it to change images instead of text.
import Tkinter as tk # use tkinter for python 3.x
class AnimatedButton(tk.Button):
def __init__(self, *args, **kwargs):
tk.Button.__init__(self, *args, **kwargs)
self._job = None
def cancel_animation(self):
if self._job is not None:
self.after_cancel(self._job)
self._job = None
def animate(self, textlist):
text = textlist.pop(0)
textlist.append(text)
self.configure(text=text)
self._job = self.after(500, self.animate, textlist)
You use it like any other Button, but you can call animate to start animation and cancel_animate to cancel it:
button = AnimatedButton(root, width=10)
data = ["one","two","three","four","five","six"]
button.bind("<Enter>", lambda event: button.animate(data))
button.bind("<Leave>", lambda event: button.cancel_animation())
I followed the Bryan Oakley example and found a nice solution!
First of all, this is an animated button with a bit complex animation. I have 16 images. The firts one is the base image. Then i have eight images that are the first part of the animation. The rest of the images are the loop part of the animation.
When you put the mouse over the button, the animation starts.
Here is the code!:
import Tkinter as tk # use tkinter for python 3.x
root = tk.Tk()
root.geometry("300x200")
class AnimatedButton(tk.Button):
def __init__(self, *args, **kwargs):
tk.Button.__init__(self, *args, **kwargs)
self._job = None
self.i = 1
def cancel_animation(self,image):
self.configure(image=image)
self.i = 1
if self._job is not None:
self.after_cancel(self._job)
self._job = None
def animate(self, imagelist):
image = imagelist[self.i]
self.i+=1
if self.i == (len(imagelist)-1):
self.i = 9
self.configure(image=image)
self._job = self.after(80, self.animate, imagelist)
imagePath = "Resources/"
imagelist = ["boton_1.gif","boton_2.gif","boton_3.gif","boton_4.gif","boton_5.gif","boton_6.gif",
"boton_7.gif","boton_8.gif","boton_9.gif","boton_10.gif","boton_11.gif","boton_12.gif",
"boton_13.gif","boton_14.gif","boton_15.gif","boton_16.gif"]
giflist = []
for imagefile in imagelist:
photo = tk.PhotoImage(file=imagePath+imagefile)
giflist.append(photo)
image = giflist[0]
button = AnimatedButton(root,image = image)
button.bind("<Enter>", lambda event: button.animate(giflist))
button.bind("<Leave>", lambda event: button.cancel_animation(image))
button.pack()
root.mainloop()
Thank's!!!