1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TkinterでGIFを動かす

Last updated at Posted at 2022-07-10

考察

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

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

以下、

while方式
コールバック方式

の例を示します

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

While方式

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

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

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
            # 最終フレームになったらフレームを0に戻す
            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へ追加しています。

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
            # 最終フレームになったらフレームを0に戻す
            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()
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?