3

I'm trying to build a tkinter button with an image as background inside an object. It doesn't make any sense why the second implementation doesn't work !

Here are 3 very simple examples ; Who can explain the reason why the second implementation is not working?

(Python 3.6.4 :: Anaconda, Inc.)

1. Button created globally.

Works like a charm...

from tkinter import *
from PIL import Image, ImageTk
from numpy import random
w = Tk()
def cb():
    print("Hello World")
image = ImageTk.PhotoImage(image=Image.fromarray(random.random((50,50))))
b = Button(w, text="text", command=cb, image=image)
b.pack()
w.mainloop()

2. Button created inside the object A with a background image

The button doesn't work when clicked and doesn't display the image :(. There's clearly a problem but I don't understand it...

from tkinter import *
from PIL import Image, ImageTk
from numpy import random
w = Tk()
class A():
    def __init__(self, w):
        image = ImageTk.PhotoImage(image=Image.fromarray(random.random((50,50))))
        b = Button(w, text="text", command=self.cb, image=image)
        b.pack()

    def cb(self):
        print("Hello World")

a = A(w)
w.mainloop()

3. Button created inside the object A without a background image

The button works properly, but I would like to display the image as well

from tkinter import *
from PIL import Image, ImageTk
from numpy import random
w = Tk()
class A():
    def __init__(self, w):
        image = ImageTk.PhotoImage(image=Image.fromarray(random.random((50,50))))
        b = Button(w, text="text", command=self.cb)#, image=image)
        b.pack()

    def cb(self):
        print("Hello World")

a = A(w)
w.mainloop()
Jav
  • 1,445
  • 1
  • 18
  • 47
  • Tricky indeed...and if you declare your `image` outside the class it works...maybe [this](https://stackoverflow.com/questions/17760871/python-tkinter-photoimage) question could help... – toti08 Nov 14 '18 at 14:28
  • However if you make it a class variable it works.... – toti08 Nov 14 '18 at 14:29
  • Simple answer is you need to add a special argument to the button that tells it to use both the image and text. `compound`. And change `image` to `self.image`. – Mike - SMT Nov 14 '18 at 14:32
  • 1
    @toti08 indeed… and actually, it's in the doc of `PhotoImage` – Jav Nov 14 '18 at 14:32
  • 1
    @toti08 just to clarify you mean a class attribute. A variable is what they are using now. The class attribute is what we create with `self.`. – Mike - SMT Nov 14 '18 at 14:42
  • @Mike-SMT yes, true, sorry for the mess... – toti08 Nov 14 '18 at 14:43

2 Answers2

2

You have 2 problems here.

The first problem is the image is not being saved after __init__. You probably know you need to save a reference to the image for it to be used in tkinter. You may not know that in a class if you do not assign the image to a class attribute it will not save the image after __init__.

So to fix the first issue you need change this:

image = ImageTk.PhotoImage(image=Image.fromarray(random.random((50,50))))

To this:

# add self. to make it a class attribute and keep the reference alive for the image.
self.image = ImageTk.PhotoImage(image=Image.fromarray(random.random((50,50))))

The 2nd issue you may not notice here is that your text will not display while loading an image. This is because you need to add the argument compound in order for tkinter to display both an image and text in the button. That said you also need to update the image argument to include the new self.image.

So change this:

b = Button(w, text="text", command=self.cb, image=image)

To this:

# added compound and change text color so you can see it.
b = Button(w, compound="center" , text="text", fg="white", command=self.cb, image=self.image)

Results:

enter image description here

Mike - SMT
  • 14,784
  • 4
  • 35
  • 79
  • Thx. I realize my question was unclear about the "text". I don't need/want to make a text appear. I should have removed that option from the Button in the two first code snippets. Anyway, thanks for your answer which explains everything – Jav Nov 15 '18 at 08:48
1

I think I understood what happened. Thanks to the linked question what's happening in the second case is that your image gets garbage collected once the __init__ method is finished. As a result your image is not available anymore to the root application, so it cannot be binded to it. The way to solve it is to make it a class attribute:

class A():
    def __init__(self, w):
        self.image = ImageTk.PhotoImage(image=Image.fromarray(random.random((50,50))))
        b = Button(w, text="text", command=self.cb, image=self.image)
        b.pack()

    def cb(self):
        print("Hello World")
toti08
  • 2,448
  • 5
  • 24
  • 36
  • This will correct the image issue but does not display text. So this is not quite a complete solution for the issue with the 2nd and 3rd block's of code. – Mike - SMT Nov 14 '18 at 14:43
  • @Mike-SMT in the original question the issue was with the image and the functioning of the button. I honestly didn't check the text displayed in the button... – toti08 Nov 14 '18 at 14:45
  • Well the OP states *`The button works properly, but I would like to display the image as well`* so with that I would think the OP wants both the text and image to work. And considering they have both a text and image argument they likely want to use both. – Mike - SMT Nov 14 '18 at 14:46
  • That's because in example #3 he commented the `image=image` part out...at least I interpreted like this... – toti08 Nov 14 '18 at 14:47