Jack Frye Jack Frye - 6 months ago 44
Python Question

Hide a Button Under an Image TKinter

I am wondering how within a

Frame
I can hide a button under an image without distorting the image size. I assume I will need to use gridding but I am not sure how to make the image fit into that without its aspect ratio going crazy. It does not seem possible to create invisible buttons in a grid with
grid()
, then pack an image on top with
pack()
. Any ideas?

I have tried:

self.go_to_hydra = Button(self, text="Go to",
command=lambda: self.controller.show(Hydra_Level, self))
self.go_to_hydra.grid(row=20, column=20, rowspan=10, columnspan=10)
self.map_picture = PhotoImage(file=r"images/rsz_archipelago.gif")
self.image = Label(self, image=self.map_picture)
self.image.grid(row=0, column=0, columnspan=40, rowspan=40)


When I grid the button first, it appears on top and works. When I grid it second, it does not appear, which makes sense, but when I click on where it was, no event is triggered.

Answer

You can't hide elements under each other like that with Tkinter. However, you can obtain similar results by defining rectangles representing different "button" areas of the image and determining which one, if any, was clicked yourself. In Tkinter terms I made the image a big button and bound the <Button-1> click events to it.

This requires manually figuring-out the coordinates of the various regions in the image. Fortunately this can usually be done fairly easily by loading the image into an image editor and recording the x and y values of the corners of each region you want to define and putting them into your script. For the demo code below, I divided my test image up into the six regions shown in red below:

test image with red rectangles superimposed

from collections import namedtuple
import Tkinter as tk

Rect = namedtuple('Rect', 'x0, y0, x1, y1')

class ImageMapper(object):
    def __init__(self, image, img_rects):
        self.width, self.height = image.width(), image.height()
        self.img_rects = img_rects

    def find_rect(self, x, y):
        for i, r in enumerate(self.img_rects):
            if (r.x0 <= x <= r.x1) and (r.y0 <= y <= r.y1):
                return i
        else:
            return None

class Demo(tk.Frame):
    def __init__(self, master=None):
        tk.Frame.__init__(self, master)
        self.grid()
        self.create_widgets()

    def create_widgets(self):
        self.location = tk.StringVar()
        self.msg = tk.Message(self, textvariable=self.location, width=100)
        self.msg.grid(row=0, column=0)

        self.picture = tk.PhotoImage(file="archipelago2.gif")
        img_rects = [Rect(0, 26, 80, 78),
                     Rect(89, 26, 183, 78),
                     Rect(119, 120, 168, 132),
                     Rect(126, 74, 219, 125),
                     Rect(134, 135, 219, 164),
                     Rect(0, 148, 21, 164)]
        self.imagemapper = ImageMapper(self.picture, img_rects)
        self.image = tk.Button(self, image=self.picture, borderwidth=0)
        self.image.bind('<Button-1>', self.image_click)
        self.image.grid(row=1, column=0)

        self.quitButton = tk.Button(self, text='Quit', command=self.quit)
        self.quitButton.grid(row=2, column=0)

    def image_click(self, event):
        hit = self.imagemapper.find_rect(event.x, event.y)
        self.location.set('rect[{}] clicked'.format(hit) if hit is not None else '')

app = Demo()
app.master.title('Image Mapper')
app.mainloop()

Here's some screenshots of it running:

screenshots of demo running

(You can download a copy of the image being used from here).