【Python】Tkinter で画像などのオブジェクトが描画できない時の対処法

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 で文字列が出力されるかの確認です。

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_imageprint を実行し、その後に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 をグローバル変数として扱ってやれば問題は解決します。

対処法1
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 オブジェクトはその参照がなくなるまで残り続けます。

グローバル変数が参照し続ける様子

これにより、関数終了後に遅れて行われる画像描画時にも描画するオブジェクトが残っていることになるので、ちゃんと画像の描画ができるようになります。

画像の描画時にオブジェクトがある様子

オブジェクトをクラスのインスタンスに参照させる

あとは下記のようにクラスのインスタンスにオブジェクトを参照させておくのも有効です。

対処法2
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()

drawDraw オブジェクト(Draw クラスのインスタンス)を参照するグローバル変数です。

グローバル変数はアプリ終了まで残り続ける変数です(おそらく)。

なのでグローバル変数に参照されているオブジェクトは、参照されている限りはアプリが終了するまで残り続けます

さらに、そのオブジェクトの image 変数により PhotoImage オブジェクトを参照させておけば、この PhotoImage オブジェクトもアプリが終了するまで残り続けることになり、描画を正常に動作させることができます(draw.image に参照され続けているのでガベージコレクションが動作しない)。

スポンサーリンク

参考:ガベージコレクションを動作させて描画してみる

ちなみに mainloop 前に drawdraw.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()

drawDraw オブジェクトを参照しなくなるため、その Draw オブジェクトがガベージコレクションにより削除されます。

さらにその Draw オブジェクトが参照していた PhotoImage オブジェクトも参照されなくなるため、ガベージコレクションにより削除されます。

これにより、実際に画像描画が行われる際には、描画しようとしている PhotoImage オブジェクトが既に削除されているため、画像の描画が行われないことになります。

まとめ

このページでは、Tkinter で画像等のオブジェクトが描画できない場合の対処法について解説しました。

スクリプトは上手く書けていそうなのに描画できない場合は、まずはこのページで解説した下記問題が発生していないかどうかを確認すると良いです。

  • 実際の描画が行われる時に既にオブジェクトが削除されている

対処法は描画行われるまでオブジェクトが残るようにすることです。例えば下記により解決できます。

  • オブジェクトをグローバル変数に参照させる
  • オブジェクトをクラスのインスタンスに参照させる

スクリプトとしては一見問題なさそうに見えるのが厄介なところですね…。割とハマりやすい問題だと思います。

同じ問題に遭遇した方は、是非このページを参考にして解決してみてください!

2 COMMENTS

村田厚志

ありがとうございます。とても参考になりました。
関数を使わないでimageファイルをcanvasに描くことはできたのに、
関数を使うと描画されない。
Canvas.create_image()で描画されると思っていたので、1日中悩みました。
tkinter.PhotoImage()で作られたimageファイルがローカル変数なので、関数の処理が終わったら、消えてしまうのですね。

返信する
daeu

村田厚志さん

コメントありがとうございます!
お役に立てたようで私としても嬉しいです。

私も結構ハマりました…。
表示する画像やオブジェクトが消えないようにグローバル変数やクラスのメンバで参照させておくことを意識してプログラミングする必要がありそうです。

また困ったことなどあれば質問いただければと思います!出来る限り協力します!

返信する

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です