2

I am trying to display the animation from my gif image. From my previous question, I discovered that Tkinter doesn't animate images automatically. My Tk interface shows the first frame of the image, and when I click the button to play its animation, it does nothing. It's likely something to do with the command associated with the button. Here's the code:

from Tkinter import *
import Tkinter

root = Tk()

photo_path = "/users/zinedine/downloads/091.gif"
photo = PhotoImage(
    file = photo_path
    )

def run():
    frame = 1
    while True:
        try:
            photo = PhotoImage(
                file = photo_path,
                format = "gif - {}".format(frame)
                )
            frame = frame + 1

        except Exception: # This because I don't know what exception it would raise
            frame = 1
            break

picture = Label(image = photo)
picture.pack()
picture.configure(run())

animate = Button(
    root,
    text = "animate",
    command = run()
    )
animate.pack()

root.geometry("250x250+100+100")
root.mainloop()
Community
  • 1
  • 1
Zizouz212
  • 4,908
  • 5
  • 42
  • 66
  • For starters, the `picture.configure(run())` is wrong. A widget `configure()` methods requires arguments of the form `w.configure(option=value, ...)`. What you're doing is calling the `run()` function yourself before `configure` itself is called. Secondly, the answer you accepted to your other question said to use "timer events to switch the frame being shown" and you're not doing this. – martineau Feb 15 '15 at 21:48
  • But, what timer event would I use then? Something like `time.sleep`? – Zizouz212 Feb 15 '15 at 21:48
  • Since I'm not the author of the answer, I'm not precisely sure what they meant. It could have been using the `repeatinterval` option for [`Button` widgets](http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/button.html) with its `confgiure()` method or possibly the [`w.after(delay_ms, callback=None, *args)`](http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/universal.html) universal widget method. – martineau Feb 15 '15 at 22:03
  • Don't worry. Hopefully someone in the community will be able to provide a simple, yet clear answer. I'm quite new to GUI programming so if there is some weird way, than I'd be more than happy to use it. But, I just have one question then, do you have to use the timer when playing the animation? In my eyes, the time needed to load the animation is already more than enough time to wait from frame to frame. – Zizouz212 Feb 15 '15 at 22:14

2 Answers2

4

You can use the universal Tk widget after() method to schedule a function to run after a specified delay given in milliseconds. This only happens once, so typically the function itself also calls after() to perpetuate the process.

In the code below a custom AnimatedGif container class is defined which loads and holds all the frames of animated sequence separately in a list which allows quick (random) access to them using [] indexing syntax. It reads individual frames from the file using the -index indexvalue image format suboption mentioned on the photo Tk manual page.

I got the test image shown below from the Animation Library website.

test image

Here's how things should look when it's initially started.

screenshot of program window at start up

You should be able use the same technique to animate multiple images or those that are attached to other kinds of widgets, such as Button and Canvas instances.

try:
    from tkinter import *
except ImportError:
    from Tkinter import *  # Python 2


class AnimatedGif(object):
    """ Animated GIF Image Container. """
    def __init__(self, image_file_path):
        # Read in all the frames of a multi-frame gif image.
        self._frames = []

        frame_num = 0  # Number of next frame to read.
        while True:
            try:
                frame = PhotoImage(file=image_file_path,
                                   format="gif -index {}".format(frame_num))
            except TclError:
                break
            self._frames.append(frame)
            frame_num += 1

    def __len__(self):
        return len(self._frames)

    def __getitem__(self, frame_num):
        return self._frames[frame_num]


def update_label_image(label, ani_img, ms_delay, frame_num):
    global cancel_id
    label.configure(image=ani_img[frame_num])
    frame_num = (frame_num+1) % len(ani_img)
    cancel_id = root.after(
        ms_delay, update_label_image, label, ani_img, ms_delay, frame_num)

def enable_animation():
    global cancel_id
    if cancel_id is None:  # Animation not started?
        ms_delay = 1000 // len(ani_img)  # Show all frames in 1000 ms.
        cancel_id = root.after(
            ms_delay, update_label_image, animation, ani_img, ms_delay, 0)

def cancel_animation():
    global cancel_id
    if cancel_id is not None:  # Animation started?
        root.after_cancel(cancel_id)
        cancel_id = None


root = Tk()
root.title("Animation Demo")
root.geometry("250x125+100+100")
ani_img = AnimatedGif("small_globe.gif")
cancel_id = None

animation = Label(image=ani_img[0])  # Display first frame initially.
animation.pack()
Button(root, text="start animation", command=enable_animation).pack()
Button(root, text="stop animation", command=cancel_animation).pack()
Button(root, text="exit", command=root.quit).pack()

root.mainloop()
martineau
  • 119,623
  • 25
  • 170
  • 301
  • It Works! However, while it does _animate_ the image, it does it really fast. With my image being 44 frames, it feels as if it goes at 44 fps instead of the normal, natural speed. I was hoping to change this by configuring `ms_delay` to a keyword argument and set it to something like `0.5` but it doesn't accept floats and even modifying to an integer (e.g. 1) doesn't seem to do anything. Otherwise, you are a pure genius! – Zizouz212 Feb 18 '15 at 02:45
  • Doubtful. Yes, determining the delay to use will be tricky. The `1000 // len(ani_img)` in my answer effectively attempts to compute an amount that would allow all the frames to be displayed within one second — which was OK for the test image I used which had only 13 frames. The "proper" way to do it would be to use the delay value in the Graphics Control Extension (GCE) block before each frame in the gif file. To get that info you'll need to use something in addition to the `Tkinter` module...maybe the `PIL`, or write your own code to parse the file. – martineau Feb 18 '15 at 03:40
3

Here's an alternative version of my previous answer. Although also based on the universal Tk widget after() method, it uses the PIL (or the pillow fork of it) module to read the gif image file. With PIL it's not only easy to extract each frame from the file, but also to get the delay (or "duration") between frames of the animation directly from the gif file — which eliminates guessing what it should be for different files.

try:
    from tkinter import *
except ImportError:
    from Tkinter import *
from PIL import Image, ImageSequence, ImageTk


class AnimatedGif(object):
    """ Animated GIF Image Container. """
    def __init__(self, image_file_path):
        # Read in all the frames of a multi-frame gif image.
        self._frames = []
        img = Image.open(image_file_path)
        for frame in ImageSequence.Iterator(img):
            photo = ImageTk.PhotoImage(frame)
            photo.delay = frame.info['duration'] * 10  # Add attribute.
            self._frames.append(photo)

    def __len__(self):
        return len(self._frames)

    def __getitem__(self, frame_num):
        return self._frames[frame_num]


def update_label_image(label, ani_img, frame_num):
    """ Change label image to given frame number of AnimatedGif. """
    global cancel_id
    frame = ani_img[frame_num]
    label.configure(image=frame)
    frame_num = (frame_num+1) % len(ani_img)  # Next frame number.
    cancel_id = root.after(frame.delay, update_label_image, label, ani_img, frame_num)

def enable_animation():
    """ Start animation of label image. """
    global cancel_id
    if cancel_id is None:  # Animation not started?
        cancel_id = root.after(ani_img[0].delay, update_label_image, animation, ani_img, 0)

def cancel_animation():
    """ Stop animation of label image. """
    global cancel_id
    if cancel_id is not None:  # Animation started?
        root.after_cancel(cancel_id)
        cancel_id = None


root = Tk()
root.title("Animation Demo")
root.geometry("250x125+100+100")
ani_img = AnimatedGif("small_globe.gif")
cancel_id = None

animation = Label(image=ani_img[0])  # Display first frame initially.
animation.pack()
Button(root, text="start animation", command=enable_animation).pack()
Button(root, text="stop animation", command=cancel_animation).pack()
Button(root, text="exit", command=root.quit).pack()

root.mainloop()
martineau
  • 119,623
  • 25
  • 170
  • 301
  • It's giving me an `AttributeError`. Here's the traceback: `Traceback (most recent call last): File "", line 1, in anigif = AnimatedGif(img_path) File "", line 5, in __init__ self._load() File "", line 13, in _load img = Image.open(self.filepath) AttributeError: class Image has no attribute 'open'` – Zizouz212 Feb 22 '15 at 21:38
  • That doesn't make sense because `Image.open()` is a standard `Image` class method. The documentation for it is [here](http://effbot.org/imagingbook/image.htm#tag-Image.open). Perhaps you have an outdated version. You can download Python 2.x installers for the original module [here](http://effbot.org/downloads/#pil). However I strongly suggest you use the newer `Pillow` version which has installers for both Python 2.x & 3.x (and is still actively supported) — the installers for the `Pillow` fork are currently [here](https://pypi.python.org/pypi/Pillow/2.7.0). – martineau Feb 22 '15 at 22:30
  • The documentation for `Pillow` version of `Image.Open()` is [here](http://pillow.readthedocs.org/reference/Image.html) — it's says essentially the same as the original since `Pillow` is mostly backwards compatible with the original `PIL` module. – martineau Feb 22 '15 at 22:36