为什么在函数中创建的Tkinter图像不显示?

87

这段代码有效:

import tkinter

root = tkinter.Tk()
canvas = tkinter.Canvas(root)
canvas.grid(row = 0, column = 0)
photo = tkinter.PhotoImage(file = './test.gif')
canvas.create_image(0, 0, image=photo)
root.mainloop()

它展示给我这个图片。

现在,这段代码编译成功了,但是没有展示图片,而我不知道为什么,因为它和之前的代码一样,都在同一个类中:

import tkinter

class Test:
    def __init__(self, master):
        canvas = tkinter.Canvas(master)
        canvas.grid(row = 0, column = 0)
        photo = tkinter.PhotoImage(file = './test.gif')
        canvas.create_image(0, 0, image=photo)

root = tkinter.Tk()
test = Test(root)
root.mainloop()

3
http://effbot.org 网站已经无法访问。其要点是图像通过引用传递。如果引用指向局部变量,则所引用的内存将被重用,引用将变得陈旧。 存储图像的变量应与出现在其上的 Tk GUI 对象具有相同的作用域(必须具有相同的生命周期)。 - maszoka
@maszoka: effbot.org 可能已经无法访问,但是你仍然可以通过互联网档案馆 wayback machine 阅读链接 为什么我的 Tkinter 图像不显示? - martineau
1
请注意,同样的问题可能会出现在任何使用临时“PhotoImage”的地方,例如在调用序列中,如label = Label(image=ImageTk.PhotoImage(Image.fromarray(data))) - martineau
5个回答

121
变量photo是一个局部变量,在类实例化后会被垃圾回收。解决方案是保存对照片的引用,例如:
self.photo = tkinter.PhotoImage(...)

如果你在Google上搜索"tkinter图片不显示",第一个结果是这个: 为什么我的Tkinter图片不显示?

13
哇,他们认为这是 tkinter 中的一个 bug 吗?他们应该这么认为。 - Tamas Hegedus
4
我找到了一张非常旧的票据,已经关闭但没有修复:https://bugs.python.org/issue632323 - Tamas Hegedus
3
这个例子展示了如何在不使用全局变量的情况下完成它。 - Bryan Oakley
10
我同意这是个漏洞,但显然在将近20年的时间里没有人费心去修复它。已经不记得有多少次看到人们仍然问起它的问题了。 - martineau
2
@Enderman:不行,因为它正在引用自身。一个对象需要有外部引用,否则垃圾回收器可能会将其销毁。 - Bryan Oakley
显示剩余12条评论

9
from tkinter import *
from PIL import ImageTk, Image

root = Tk()

def open_img():
    global img
    path = r"C:\.....\\"
    img = ImageTk.PhotoImage(Image.open(path))
    panel = Label(root, image=img)
    panel.pack(side="bottom", fill="both")
but1 = Button(root, text="click to get the image", command=open_img)
but1.pack()
root.mainloop() 

只需在img定义中添加global,它就会起作用。

3
这个答案对于仅使用函数的程序来说是可以的,但如果像OP的情况一样使用类,则global不是正确的方法。 - Sylvester Kruin

8
问题在于Python会通过一种称为垃圾回收的过程自动删除对变量的引用。解决方法是保存引用或创建新引用。
以下是几种方法:
  1. 使用self来增加引用计数并保存引用。
import tkinter

class Test:
    def __init__(self, master):
        canvas = tkinter.Canvas(master)
        canvas.grid(row = 0, column = 0)
        self.photo = tkinter.PhotoImage(file = './test.gif') # Changes here
        canvas.create_image(0, 0, image=self.photo) # Changes here

root = tkinter.Tk()
test = Test(root)
root.mainloop()

将其保存到列表中以增加引用计数并保存引用。
import tkinter
l=[]
class Test:

    def __init__(self, master):
        canvas = tkinter.Canvas(master)
        canvas.grid(row = 0, column = 0)
        photo = tkinter.PhotoImage(file = './test.gif')
        l.append(photo)
        canvas.create_image(0, 0, image=photo)

root = tkinter.Tk()
test = Test(root)
root.mainloop()

使用方法2时,您可以像我所做的那样创建全局列表,也可以在类内部使用列表。两种方法都可以。

一些有用的链接:


2
作为经验法则,每当您在缩进代码块中创建图像时,需要保存该图像的引用。这是由于 Python 的自动垃圾回收机制,它会在销毁/离开帧/页面/缩进代码块时收集所有引用计数为 0 的对象。
“规范的方法”是在全局命名空间中“有一个图像列表”,并将您的图像引用添加到该列表中。这很方便,但不太高效,应仅用于小型应用程序。”
import tkinter as tk

global_image_list = []
global_image_list.append(tk.PhotoImage(file = 'test.png'))

一种更有效的方法是将属性绑定到您的小部件或类上,以保存该引用,正如Bryan在他的答案中提出的那样。无论你是使用self.image还是widget.image(在widget = tk.Widget(..之前分配),都没有区别。但如果您想在小部件被销毁和垃圾回收后继续使用该图像,这也可能不是正确的方法。
import tkinter as tk

root = tk.Tk()
label = tk.Label(root, text='test')
label.image = tk.PhotoImage(file = 'test.png')
label.configure(image=label.image)

0

只需在函数内部的第一行添加global photo即可。


8
然后您创建第二个“Test”实例,第一个实例失去了它的图像。恭喜。 - Aran-Fey
5
在类中使用global是没有必要的,因为self.可以处理所有相关的操作。请注意,不要更改原始含义。 - Sylvester Kruin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接