2

I decided to make a class for easily displaying animated gifs in Tkinter, which does work, but the process of gathering all the frames dynamically pretty much always takes a noticeable chunk of time and prevents anything else from happening until it has finished. I was wondering if there would be any way to speed it up or at a more efficient way of doing the same thing.

Here is the code for the class:

from tkinter import *

class animation:
    def __init__(self,*args,**kwargs):
        self.root=args[0]
        self.label=Label(self.root)
        self.label.grid()
        self.image=kwargs["image"]
        try:
            self.delay=kwargs["delay"]
        except KeyError:
            self.delay=20

        self.frames=[]
        x=0
        while True:
            try:
                img=PhotoImage(file=self.image,
                               format="gif -index {}".format(x))
                self.frames.append(img)
                x+=1
            except:
                break


    def animate(self,y):
        try:
            self.label.configure(image=self.frames[y])
            self.root.after(self.delay,lambda:self.animate(y+1))
        except IndexError:
            self.label.configure(image=self.frames[0])
            self.root.after(self.delay,lambda:self.animate(1))

and here is how it would be used:

from tkinter import *
from modules.animation import animation

root=Tk()

cosmog=animation(root,image="cosmog.gif").animate(0)
cosmoem=animation(root,image="cosmoem.gif").animate(0)
lunala=animation(root,image="lunala.gif").animate(0)

root.mainloop()
mikosuki
  • 37
  • 5
  • maybe create class which read all images before you start tkinter. Or use thread to read files. – furas Dec 17 '17 at 00:34
  • It won't help very much, but you should load the image once, and then pull all the frames out, rather than loading the image from the HDD for every frame. [Here's how I did that](https://stackoverflow.com/a/43770948/2229945). Also, notice the proper way to create a tkinter widget: by subclassing a current widget. That way you can treat it like any other widget in your code (pack, grid, tkraise, etc). – Novel Dec 17 '17 at 01:20
  • FWIW, instead of `self.root.after(self.delay,lambda:self.animate(1))` you can use `self.root.after(self.delay, self.animate, 1)` – Bryan Oakley Dec 17 '17 at 05:11
  • Thank you for everyones help – mikosuki Dec 18 '17 at 14:48

1 Answers1

0

Try like this with threading:

import threading
import time

def __init__(self, *args, **kwargs):
    self.frames=[]
    self.run_loop = True
    self.x=0
    self.taken = []
    self.order = []

    while self.run_loop:
        threading.Thread(target=self.add_image).start()

def add_image(self):
    try:
        if self.x not in self.taken: # make sure not several threads add the same image index
            self.taken.append(self.x)
            self.x+=1
        else:
           return

        # save local x before creating image, for the order
        # all the other threads will increment self.x while Image is created
        x = self.x   

        img=PhotoImage(file=self.image, format="gif -index {}".format(self.x))

        self.frames.append(img)
        self.order.append(x) # keep track of the order

        if len(self.frames) == len(self.taken): # when finish
            self.frames = [x for _,x in sorted(zip(self.order,self.frames))]  # sort the frames to the order

    except:
        self.run_loop = False

I made a simple runable example without tkinter, using time.sleep(random amount of time), to simulate PhotoImage:

import threading
import time
from random import randint

class Test:

    frames=[]
    run_loop = True
    x=0
    taken = []
    order = []
    time = 0

    def __init__(self):
        while self.run_loop:
            threading.Thread(target=self.add_image).start()

    def add_image(self):
        if self.x < 100:
            if self.x not in self.taken: # make sure not several threads add the same image index
                self.taken.append(self.x)
                self.x+=1
            else:
               return

            x = self.x
            t = randint(1,10)/10.0
            self.time += t

            time.sleep(t) # PhotoImage random time.sleep 0 to 1 second

            self.order.append(x)
            self.frames.append(x)

            if len(self.frames) == len(self.taken):
                print("Frames before sort")
                print(self.frames)

                self.frames = [x for _,x in sorted(zip(self.order,self.frames))]

                print("\nFrames after sort")
                print(self.frames)

                print("\nTime used combined: {} seconds".format(self.time))


        else:
            self.run_loop = False


t = Test()

This test shows a combined time used, to be around 50 seconds.
With a 100 threads, it does it in 1 second. The amount of time for the longest time.sleep, wich is 0 to 1 second.
So for you, it should not take more than the one longest PhotoImage call

el3ien
  • 5,362
  • 1
  • 17
  • 33