結論:

画像を表示している間はその PhotoImage インスタンスを保持しておく必要がある。
(厳密には tkinter.Image のサブクラスのインスタンス)
保持していないと画像は表示されない。

環境:

tkinter.TkVersion: 8.6
python3 --version: Python 3.8.10

問題:

次のコードは期待通りに動く。赤いウィンドウにうずまき模様を書いたgif画像を表示するもの。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import tkinter

base64_encoded_gif = (
    "R0lGODdhMgAyAIABAP8A/7LN1SwAAAAAMgAyAAACaYyPqcvtD6OctNqLs968+w+G4kiW5omm6"
    "sq27gvH8hwDAYDnrm0fPcu7BVvDIfBmMK54ut9S4VTlplEajTndIX1a5vbYU0q34hS2jKpW01"
    "Sc9Z0UZonf+ti7PhXtZjI/LYemB7XTlvdSAAA7")

window = tkinter.Tk()
canvas = tkinter.Canvas(window, bg="red")
canvas.place(x=0, y=0)

def func():
    photo = tkinter.PhotoImage(data=base64_encoded_gif)
    canvas.create_image(0, 0, image=photo, anchor=tkinter.NW)

    global photo_holder
    photo_holder = photo

func()

window.mainloop()

コードから photo_holder = photo を消すと photo は描画されない(期待に反する)。エラーも出ない。

mainloop()が動いている最中でも同様の問題が起きる。
例えば次のような場合

threading.Threadを使った別スレッドで、新たなPhotoImageを作成し
canvas.itemconfig(created_image, image=new_photoimage)などで表示画像を変更しようとした場合でも、
new_photoimageの中身であるPhotoImageインスタンスを保持する変数がスコープからいなくなると、期待した描画は起こらない。エラーも出ない。

window.mainloop() 内のイベント処理がされる前にphotoの中身が解放されてしまうため(たぶん)。
( 解放されるとしたらfunc()を抜けたタイミングか? )

解放されないようにphotoの中身であるPhotoImageインスタンスを保持しておく必要がある。
上の例ではグローバルスコープ変数にPhotoImageインスタンスを入れることで解放を抑制している。

PhotoImageの初期化方法にかかわらずこの問題は起きる。
例えば:

  • tkinter.PhotoImage(file="image.png")
  • PIL.ImageTk.PhotoImage(file="image2.png")
  • PIL.ImageTk.PhotoImage(image=rgb_pil_array)

・・・なんで?:

  • pythonにはC++のようなスコープ抜け時の解放処理はない(そもそもpythonオブジェクトプリミティブ型を除いてC++で言えばポインター扱い。だからC++だったとしてもデストラクタは走らない)
  • だったとしても create_image を通じてtkphotoを保持するはず(←tkinterにこの仕様が抜けている?)
  • create_image の結果は int か str であってオブジェクトではない