考察
- tkinterのPhotoImageで読み込むと残像がでる
- PillowのImageで開いた状態でseekしてImageTk.PhotoImageで読み込むと残像がでない
検証してないのではっきりしていませんが、残像っていうのは、たぶん、フレームの取りこぼし
以下、
の例を示します
おすすめはコールバック方式
です
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()