Play an Animated GIF in python with tkinter
Asked Answered
S

4

8

I am wanting to create a virtual pet style game using python3 and tkinter. So far I have the main window and have started putting labels in, but the issue I am having is playing an animated gif. I have searched on here and have found some answers, but they keep throwing errors. The result I found has the index position of the gif using PhotoImage continue through a certain range.

    # Loop through the index of the animated gif
frame2 = [PhotoImage(file='images/ball-1.gif', format = 'gif -index %i' %i) for i in range(100)]

def update(ind):

    frame = frame2[ind]
    ind += 1
    img.configure(image=frame)
    ms.after(100, update, ind)

img = Label(ms)
img.place(x=250, y=250, anchor="center")

ms.after(0, update, 0)
ms.mainloop()

When I run this in terminal with "pyhton3 main.py" I get the following error:

_tkinter.TclError: no image data for this index

What am I overlooking or completely leaving out?

Here is the link to the GitHub repository to see the full project:VirtPet_Python

Thanks in advance!

Seavir answered 3/5, 2017 at 22:12 Comment(1)
Shouldn't you check that ind never goes above 100? Maybe ind %= 100?Edacity
K
24

The error means that you tried to load 100 frames, but the gif has less than that.

Animated gifs in tkinter are notoriously bad. I wrote this code an age ago that you can steal from, but will get laggy with anything but small gifs:

import tkinter as tk
from PIL import Image, ImageTk
from itertools import count

class ImageLabel(tk.Label):
    """a label that displays images, and plays them if they are gifs"""
    def load(self, im):
        if isinstance(im, str):
            im = Image.open(im)
        self.loc = 0
        self.frames = []

        try:
            for i in count(1):
                self.frames.append(ImageTk.PhotoImage(im.copy()))
                im.seek(i)
        except EOFError:
            pass

        try:
            self.delay = im.info['duration']
        except:
            self.delay = 100

        if len(self.frames) == 1:
            self.config(image=self.frames[0])
        else:
            self.next_frame()

    def unload(self):
        self.config(image="")
        self.frames = None

    def next_frame(self):
        if self.frames:
            self.loc += 1
            self.loc %= len(self.frames)
            self.config(image=self.frames[self.loc])
            self.after(self.delay, self.next_frame)

root = tk.Tk()
lbl = ImageLabel(root)
lbl.pack()
lbl.load('ball-1.gif')
root.mainloop()
Kincaid answered 3/5, 2017 at 22:22 Comment(8)
That got me to thinking...I changed the range to 13, since I created this gif and know how many frames are in it. Now it loads and terminal gets stuck into "index out of range" error. I adjusted all the other numbers in the function below it. Is there any way to count the frames and store that in a variable and then call that in the range()?Seavir
No, but you can loop over the frames until you get the EOF (end of file) error, as I did in my code.Kincaid
Ok, I have edited my code and pushed it to GitHub. Right now it loops through the the 13 frames, but the complete image doesn't show up and then I get an index out of range error when it gets through all the frames.Seavir
Those are 2 problems. The complete image not showing up is because the image you have is optimized for space, so each frame is only an update of what's changed from the previous frames. You have to assemble each frame to fix that, or edit the gif to an unoptimized version. In GIMP it's Filters > Animantion > Unoptimize. The other problem is that after 13 frames you try to load frame number 14, and since that does not exist you get the error. You need to check the frame number and loop back to when it's too big. The % operator can help with that. Why don't you want to use my code?Kincaid
My original reason for not using it was that I have had some issues using PIL with python3, but while I was away it seems to have been fixed. Thanks for the help. I am pretty sure I will have more issues and now I feel better about coming to Stack Overflow.Seavir
Thought id put this here as a fix to py37+ transparency and flashing: add seq=[] to start, change self.frames.append to seq.append. finally add ` first = seq[0].convert('RGBA'); self.frames = [ImageTk.PhotoImage(first)]; self.config(image=self.frames[0]); for image in seq[1:]:; self.frames.append(ImageTk.PhotoImage(image.convert('RGBA')));` after self.delay ( ; denotes new lines)Persistent
@ScottPaterson Thanks, but that fix is fairly slow, it will cause long load times and slow down an already slow animation. For performance reasons it would be much better to convert the gif file to a non-compressed format with the appropriate background color.Kincaid
@Kincaid yes it roughly doubles the load time. I have settings to control background colours so setting the animation to a solid colour would not be practical. However for small animations with low frame counts its still useable as a quick fix. 60-80 frames at 256x256 has no noticeable difference in load times (<0.2 seconds).Persistent
H
3

First of all, you need to know what is the last range of your GIF file. so by changing the different value of i, you will get it.For my condition is 31. then just need to put the condition.So it will play gif infinitely.

from tkinter import *

root = Tk()

frames = [
    PhotoImage(file="./images/play.gif", format="gif -index %i" % (i))
    for i in range(31)
]

def update(ind):
    frame = frames[ind]
    ind += 1
    print(ind)
    if ind > 30:  # With this condition it will play gif infinitely
        ind = 0
    label.configure(image=frame)
    root.after(100, update, ind)

label = Label(root)
label.pack()
root.after(0, update, 0)
root.mainloop()
Hypogeal answered 8/4, 2020 at 7:38 Comment(0)
M
0

A very simple approach would be to use multithreading.

To run the GIF infinitely in a Tkinter window you should follow the following:

  1. Create a function to run the GIF.
  2. Put your code to run the GIF inside while True inside the function.
  3. Create a thread to run the function.
  4. Run root.mainloop() in the primary flow of the program.
  5. Use time.sleep() to control the speed of your animation.

Refer to my code below:

i=0
ph = ImageTk.PhotoImage(Image.fromarray(imageframes[i]))
imglabel=Label(f2,image=ph)
imglabel.grid(row=0,column=0)

def runthegif(root,i):
    
    while True:
        i = i + 7
        i= i % 150
        
        ph=ImageTk.PhotoImage(PhotoImage(file='images/ball.gif',format='gif -index %i' %i))
        imagelabel=Label(f2,image=ph)
        imagelabel.grid(row=0,column=0)
        time.sleep(0.1)
    


t1=threading.Thread(target=runthegif,args=(root,i))
t1.start()


root.mainloop()
Mcquiston answered 28/6, 2020 at 17:34 Comment(1)
I did change your code block so it explicitly says it is Python code. Also I removed your ending sentence, the reason is, we tend not to leave "chatty" thank you and the like in SO posts. This said, you can always review the history of a post, just click on the link going with "edited ... ago" and you'll lend on a revision page like this one were you can see an history of all chanegs done.Stile
C
-1

I have updated one answer and this is full code

import tkinter as tk
from PIL import Image, ImageTk
from itertools import count

class ImageLabel(tk.Label):
    """a label that displays images, and plays them if they are gifs"""
    def load(self, im):
        if isinstance(im, str):
            im = Image.open(im)
        self.loc = 0
        self.frames = []

        try:
            for i in count(1):
                self.frames.append(ImageTk.PhotoImage(im.copy()))
                im.seek(i)
        except EOFError:
            pass

        try:
            self.delay = im.info['duration']
        except:
            self.delay = 100

        if len(self.frames) == 1:
            self.config(image=self.frames[0])
        else:
            self.next_frame()

    def unload(self):
        self.config(image="")
        self.frames = None

    def next_frame(self):
        self.update()
        if self.frames:
            self.loc += 1
            self.loc %= len(self.frames)
            self.config(image=self.frames[self.loc])
            self.after(self.delay, self.next_frame)

root = tk.Tk()

lbl = ImageLabel(root)

lbl.pack()

lbl.load('C:/AI/Picturse/Animation/idle_animation.gif')

root.mainloop()
Christiniachristis answered 16/10, 2023 at 11:11 Comment(1)
This does not provide an answer to the question. Once you have sufficient reputation you will be able to comment on any post; instead, provide answers that don't require clarification from the asker. - From ReviewInseparable

© 2022 - 2024 — McMap. All rights reserved.