TkinterでGIFを動かす

# 考察

- tkinterのPhotoImageで読み込むと残像がでる
- PillowのImageで開いた状態でseekしてImageTk.PhotoImageで読み込むと残像がでない

検証してないのではっきりしていませんが、残像っていうのは、たぶん、フレームの取りこぼし

#  

以下、

[while方式](#while方式)
[コールバック方式](#コールバック方式)
 

の例を示します

おすすめは`コールバック方式`です

# While方式


init時にGIFのフレームを読み込む。EOFERrrorまたはExecptionとなったら全フームの読み込みが完了したことになる。

load_frameのやり方は２通りあって、1は残像はでず、２は残像がでる


```python
from PIL import Image, ImageTk
import tkinter as tk
from time import sleep
import threading

class GifPlayer(threading.Thread):
    def __init__(self, path: str, label: tk.Label):
        super().__init__(daemon=True)
        self._please_stop = False
        self.path = path
        self.label = label
        self.duration = []  # フレーム表示間隔
        self.frames = []  # 読み込んだGIFの画像フレーム
        self.last_frame_index = None
        # フレームの読み込み
        self.load_frames2()

     # 残像がでない
    def load_frames1(self):
        if isinstance(self.path, str):
            img = Image.open(self.path)
            frames = []

        frame_index = 0
        try:
            while True:
                frames.append(ImageTk.PhotoImage(img.copy()))
                img.seek(frame_index)
                frame_index += 1
        except EOFError:
            self.frames = frames
            self.last_frame_index = frame_index - 1

　　# 残像がでる
    def load_frames2(self):
        frames = []
        frame_index = 0
        try:
            while True:
                frames.append(tk.PhotoImage(file=self.path, format=f'gif -index {frame_index}'))
                frame_index += 1
        except Exception:
            self.frames = frames
            self.last_frame_index = frame_index - 1

    def run(self):
        frame_index = 0
        while not self._please_stop:
            # configでフレーム変更
            self.label.configure(image=self.frames[frame_index])
            frame_index += 1
            # 最終フレームになったらフレームを０に戻す
            if frame_index > self.last_frame_index:
                frame_index = 0
            # 次のフレームまでの秒数
            sleep(0.06)

    def stop(self):
        self._please_stop = True


class TkGif():
    def __init__(self, path, label: tk.Label) -> None:
        self.path = path
        self.label = label

    def play(self):
        self.player = GifPlayer(self.path, self.label)
        self.player.start()

    def stop_loop(self):
        """loopを止める"""
        self.player.stop()


if __name__ == '__main__':

    path = './hoge.gif'

    root = tk.Tk()
    root.geometry('800x800')

    main_frame = tk.Frame(root)
    main_frame.pack()

    label = tk.Label(main_frame)
    label.pack()

    gif_player = TkGif(path, label)
    gif_player.play()

    root.mainloop()
```

# コールバック方式

つぎに、label.after()でコールバックしてラベルの画像フレームを変更する方法を示します

この例では、先のコードのload_frames1メソッドをload_framesメソッドとしていますが、メソッド内で新たにimage.info['duration']を読み込んでgifのフレームの表示間隔を取得してリストへ格納して、コールバックの呼び出し時間(ms)としています。また、durationの指定がないGIFは、KeyErrorになるため、durationがない場合のフラグを立て、最後に（すべてのフレームの読み込みが完了したら）フレーム分のdurationを作成してself.durationへ追加しています。

```python
from PIL import Image, ImageTk
import tkinter as tk
import threading

class GifPlayer(threading.Thread):
    def __init__(self, path: str, label: tk.Label):
        super().__init__(daemon=True)
        self._please_stop = False
        self.path = path
        self.label = label
        self.duration = []  # フレーム表示間隔
        self.frame_index = 0
        self.frames = []  # 読み込んだGIFの画像フレーム
        self.last_frame_index = None
        # フレームの読み込み
        self.load_frames()

    def load_frames(self):
        if isinstance(self.path, str):
            img = Image.open(self.path)
            frames = []
            duration = []
        else:
            return

        frame_index = 0
        is_not_duration = False  # drationの指定がないとき
        try:
            while True:
                frames.append(ImageTk.PhotoImage(img.copy()))
                img.seek(frame_index)
                frame_index += 1
                try:
                    # フレーム表示間隔情報を保存
                    duration.append(img.info['duration']) 
                # durationの指定がないとき
                except KeyError:
                    # フラグをON
                    is_not_duration = True
        except EOFError:
            # フラグがONならフレームぶんのdurationを作成する
            if is_not_duration:
                duration = [60] * len(frames)
            self.frames = frames
            self.duration = duration
            self.last_frame_index = frame_index - 1

    def run(self):
        self.next_frame()

    def next_frame(self):
        if not self._please_stop:
            # configでフレーム変更
            self.label.configure(image=self.frames[self.frame_index])
            self.frame_index += 1
            # 最終フレームになったらフレームを０に戻す
            if self.frame_index > self.last_frame_index:
                self.frame_index = 0
        # durationミリ秒あとにコールバック
        self.label.after(self.duration[self.frame_index], self.next_frame)

    def stop(self):
        self._please_stop = True


class TkGif():
    def __init__(self, path, label: tk.Label) -> None:
        self.path = path
        self.label = label

    def play(self):
        self.player = GifPlayer(self.path, self.label)
        self.player.start()

    def stop_loop(self):
        """loopを止める"""
        self.player.stop()


if __name__ == '__main__':

    path = './hoge.gif'

    root = tk.Tk()
    root.geometry('800x800')

    main_frame = tk.Frame(root)
    main_frame.pack()

    label = tk.Label(main_frame)
    label.pack()

    gif_player = TkGif(path, label)
    gif_player.play()

    root.mainloop()
```