Tkinter Canvas
クラスの create_image
を使っていて「画像が描画できない」という問題にハマってしまったので、このページに備忘録として対処法を残しておこうと思います。
今回は create_image
に焦点を当てて解説していますが、おそらく他の Canvas
クラスのメソッドにおいても同様のことが言えると思います。
何かしらのオブジェクトの描画がうまくいかない時はぜひ参考にしてください。
おそらく同じ問題で困っている人はたくさんいるはず…!
問題
問題が発生したのは下記の Python スクリプトです。
Tkinter Canvas
クラスの create_image
を使ってキャンバスに画像を描画するスクリプトになっています。
import tkinter
def draw(canvas):
# 画像の読み込み
image = tkinter.PhotoImage(file="cute_cat.png")
# キャンバスへの画像の描画
canvas.create_image(
400, 200,
image=image
)
# Tkオブジェクト作成
root = tkinter.Tk()
# キャンバスの作成と配置
canvas = tkinter.Canvas(
root,
width=800,
height=400,
bg="#DDDDDD"
)
canvas.pack()
# 描画の実行
draw(canvas)
# メインループ
root.mainloop()
が、実際にスクリプトを実行してみると、画像が描画されません…。
試しに下記に Python スクリプトを書き換えてみました。draw
関数呼び出しをやめて直で画像を描画するように変更しています。
import tkinter
# Tkオブジェクト作成
root = tkinter.Tk()
# キャンバスの作成と配置
canvas = tkinter.Canvas(
root,
width=800,
height=400,
bg="#DDDDDD"
)
canvas.pack()
# 描画の実行
# 画像の読み込み
image = tkinter.PhotoImage(file="cute_cat.png")
# キャンバスへの画像の描画
canvas.create_image(
400, 200,
image=image
)
# メインループ
root.mainloop()
すると、画像が描画されました!!
うーん、処理の流れは同じなはずなんですが…。
原因
原因は下記のようです。
- 実際の描画が行われる時に既にオブジェクトが削除されている
原因を調べるために下記のスクリプトをまず試してみました。create_image
ではなく print
で文字列が出力されるかの確認です。
import tkinter
def test_print():
# テキストの作成
text = "これは表示されるよね?"
# テキストのprint
print(text)
# Tkオブジェクト作成
root = tkinter.Tk()
# キャンバスの作成と配置
canvas = tkinter.Canvas(
root,
width=800,
height=400,
bg="#DDDDDD"
)
canvas.pack()
# 文字列出力のテスト
test_print()
# メインループ
root.mainloop()
実行すると、ターミナルに文字列が表示されました。
これは表示されるよね?
うーん、同じ関数の作りでも、print
だと出力されるけど create_image
だと画像の描画はダメみたい。なぜ?
次に下記スクリプトを試してみました。ボタンをクリックすると create_image
と print
を実行し、その後に5秒間の sleep
を入れるスクリプトになっています。
import tkinter
def draw(canvas):
# 画像の読み込み
image = tkinter.PhotoImage(file="cute_cat.png")
# キャンバスへの画像の描画
canvas.create_image(
400, 200,
image=image
)
def button_func(canvas, image):
# キャンバスへの画像の描画
canvas.create_image(
400, 200,
image=image
)
# 文字列の出力
print("表示されるタイミング")
import time
time.sleep(5)
# Tkオブジェクト作成
root = tkinter.Tk()
# キャンバスの作成と配置
canvas = tkinter.Canvas(
root,
width=800,
height=400,
bg="#DDDDDD"
)
canvas.pack()
# 画像の読み込み
image = tkinter.PhotoImage(file="cute_cat.png")
# ボタンの作成と配置
button = tkinter.Button(
root,
text="ボタン",
command=lambda: button_func(canvas, image)
)
button.pack()
# メインループ
root.mainloop()
実行すると、下記のような結果になりました。なるほど!!
print
での文字列の出力:ボタンクリック直後に実行されたcreate_image
での画像の描画:ボタンクリックの5秒後に実行された
つまり、実際に画像の描画は create_image
実行時ではなく、遅れて実行されるということです。おそらくですが mainloop
に制御が戻った時に実際の描画が行われるのだと推測しています。
で、画像が描画できなかった一番最初のスクリプトに戻って考えてみます。下記は関数部分のみを再掲したものです。
import tkinter
def draw(canvas):
# 画像の読み込み
image = tkinter.PhotoImage(file="cute_cat.png")
# キャンバスへの画像の描画
canvas.create_image(
400, 200,
image=image
)
この関数ではまず PhotoImage
オブジェクトが生成され、ローカル変数 image
によって参照されます。
そして create_image
メソッドでは、この image
が参照する PhotoImage
オブジェクトが使用されています。
この create_image
メソッド実行時に PhotoImage
オブジェクトは存在していますが、create_image
メソッド実行後に関数が終了するので 、ローカル変数 image
が削除(解放)されます。
さらに image
からのみ参照されていた PhotoImage
オブジェクトは誰からも参照されなくなったので、不要と考えられ削除(解放)されてしまいます(ガベージコレクション)。
関数終了時には自動的にローカル変数が削除されます
さらに、そのローカル変数のみから参照されていたオブジェクトは誰からも参照されなくなるのでガベージコレクションにより自動的に削除されます
で、前述の通り画像の実際の描画は create_image
実行時ではなく、遅れて行われます。つまり、この実際の描画が行われる時には描画すべきオブジェクトはすでに削除されてしまったことになります。
そして、すでに削除されてしまったオブジェクトを描画しようとしたために、画像が描画されなかったのだと考えられます。
オブジェクトは関数終了時だけでなく、参照されなくなった時などにガベージコレクションが動作して自動的に削除されますので、この辺りも注意が必要です。
スポンサーリンク
対処法
原因は前述の通り下記です。
- 実際の描画が行われる時に既にオブジェクトが削除されている
したがって「実際の描画が行われる時にオブジェクトが残っている」ようにスクリプトを記述すれば問題は解決できます。
Python では、参照されていないオブジェクトはガベージコレクションが動作して自動的に削除されるようになっています。
しかし、逆にずっと参照されているオブジェクトは残り続けます。これを利用してスクリプトを記述すれば画像の描画を確実に行えるようになります。
オブジェクトをグローバル変数に参照させる
例えば下記のように変数 image
をグローバル変数として扱ってやれば問題は解決します。
import tkinter
def draw(canvas):
global image
# 画像の読み込み
image = tkinter.PhotoImage(file="cute_cat.png")
# キャンバスへの画像の描画
canvas.create_image(
400, 200,
image=image
)
# Tkオブジェクト作成
root = tkinter.Tk()
# キャンバスの作成と配置
canvas = tkinter.Canvas(
root,
width=800,
height=400,
bg="#DDDDDD"
)
canvas.pack()
# 描画の実行
draw(canvas)
# メインループ
root.mainloop()
image
はグローバル変数なので、関数終了時に削除されません。
さらに関数終了しても image
は PhotoImage
オブジェクトを参照し続けていますので、PhotoImage
オブジェクトはその参照がなくなるまで残り続けます。
これにより、関数終了後に遅れて行われる画像描画時にも描画するオブジェクトが残っていることになるので、ちゃんと画像の描画ができるようになります。
オブジェクトをクラスのインスタンスに参照させる
あとは下記のようにクラスのインスタンスにオブジェクトを参照させておくのも有効です。
import tkinter
class Draw():
def __init__(self, canvas):
# 画像の読み込み
self.image = tkinter.PhotoImage(file="cute_cat.png")
# キャンバスへの画像の描画
canvas.create_image(
400, 200,
image=self.image
)
# Tkオブジェクト作成
root = tkinter.Tk()
# キャンバスの作成と配置
canvas = tkinter.Canvas(
root,
width=800,
height=400,
bg="#DDDDDD"
)
canvas.pack()
# 描画の実行
draw = Draw(canvas)
# メインループ
root.mainloop()
draw
は Draw
オブジェクト(Draw
クラスのインスタンス)を参照するグローバル変数です。
グローバル変数はアプリ終了まで残り続ける変数です(おそらく)。
なのでグローバル変数に参照されているオブジェクトは、参照されている限りはアプリが終了するまで残り続けます。
さらに、そのオブジェクトの image
変数により PhotoImage
オブジェクトを参照させておけば、この PhotoImage
オブジェクトもアプリが終了するまで残り続けることになり、描画を正常に動作させることができます(draw.image
に参照され続けているのでガベージコレクションが動作しない)。
スポンサーリンク
参考:ガベージコレクションを動作させて描画してみる
ちなみに mainloop
前に draw
や draw.image
の参照先を None
に変更すると、画像が描画されなくなってしまいます(下記は draw
の参照先を None
に変更した例)。
import tkinter
class Draw():
def __init__(self, canvas):
# 画像の読み込み
self.image = tkinter.PhotoImage(file="cute_cat.png")
# キャンバスへの画像の描画
canvas.create_image(
400, 200,
image=self.image
)
# Tkオブジェクト作成
root = tkinter.Tk()
# キャンバスの作成と配置
canvas = tkinter.Canvas(
root,
width=800,
height=400,
bg="#DDDDDD"
)
canvas.pack()
# 描画の実行
draw = Draw(canvas)
draw = None
# メインループ
root.mainloop()
draw
が Draw
オブジェクトを参照しなくなるため、その Draw
オブジェクトがガベージコレクションにより削除されます。
さらにその Draw
オブジェクトが参照していた PhotoImage
オブジェクトも参照されなくなるため、ガベージコレクションにより削除されます。
これにより、実際に画像描画が行われる際には、描画しようとしている PhotoImage
オブジェクトが既に削除されているため、画像の描画が行われないことになります。
まとめ
このページでは、Tkinter で画像等のオブジェクトが描画できない場合の対処法について解説しました。
スクリプトは上手く書けていそうなのに描画できない場合は、まずはこのページで解説した下記問題が発生していないかどうかを確認すると良いです。
- 実際の描画が行われる時に既にオブジェクトが削除されている
対処法は描画行われるまでオブジェクトが残るようにすることです。例えば下記により解決できます。
- オブジェクトをグローバル変数に参照させる
- オブジェクトをクラスのインスタンスに参照させる
スクリプトとしては一見問題なさそうに見えるのが厄介なところですね…。割とハマりやすい問題だと思います。
同じ問題に遭遇した方は、是非このページを参考にして解決してみてください!
ありがとうございます。とても参考になりました。
関数を使わないでimageファイルをcanvasに描くことはできたのに、
関数を使うと描画されない。
Canvas.create_image()で描画されると思っていたので、1日中悩みました。
tkinter.PhotoImage()で作られたimageファイルがローカル変数なので、関数の処理が終わったら、消えてしまうのですね。
村田厚志さん
コメントありがとうございます!
お役に立てたようで私としても嬉しいです。
私も結構ハマりました…。
表示する画像やオブジェクトが消えないようにグローバル変数やクラスのメンバで参照させておくことを意識してプログラミングする必要がありそうです。
また困ったことなどあれば質問いただければと思います!出来る限り協力します!