Title: Make buttons grow and shrink when the mouse moves over them with tkinter in Python
My previous post Add buttons to a row container made with tkinter in Python explained how you can use a Frame widget to manage other widgets in a row. This post shows another interesting trick that row containers can do: it makes buttons grow and shrink as the mouse moves over them.
Making the Buttons
For this example, I decided to use labels instead of actual buttons so I could have more control over their appearance. In particular, I wanted to reuse the buttons from my post Make glassy button images in Python.
The following code creates the frame and the labels that act as buttons.
def build_ui(self):
'''Build the user interface.'''
# Button large and large sizes.
self.button_small_width = 30
self.button_large_width = 50
margin = 5
frame = tk.Frame(self.window)
frame.pack(side=tk.TOP, anchor=tk.CENTER, padx=margin, pady=margin,
expand=False)
# Add a blank label to force the frame to keep this height.
label = tk.Label(frame, height=4)
label.pack(side=tk.LEFT, padx=(2))
for i in range(1, 11):
self.make_button(frame, i)
# Add another blank label to keep things symmetric.
label = tk.Label(frame, height=4)
label.pack(side=tk.LEFT, padx=(2))
This method first creates the Frame widget that acts as the row container holding the buttons. It packs the frame at the top of the window and anchors it to the center.
I don't want the frame to change its height when the buttons change sizes because that makes the buttons that aren't under the mouse jump up and down in a really annoying fashion. Normally you might call pack_propagate(False) to make the frame not propagate changes when its children change, but I do want the frame to arrange those children so that's not a great solution.
Fortunately there's an easy workaround. The code places a blank label in the frame that determines the frame's minimum height. As long as the buttons don't grow taller than this label, the frame will keep a fixed height as desired. (To see what I mean about the buttons jumping up and down annoyingly, comment out the code that creates this label and the other blank label that we'll create shortly.)
Next, the code enters a loop where it calls make_button 10 times to create the buttons. It then creates another blank label to keep the labels symmetric. (If we don't create this second blank label, the buttons will be pushed slightly to the right by the first blank label.)
Making Buttons
The following code shows the make_button method.
def make_button(self, frame, i):
'''Make button number i.'''
# Load the button's picture.
picture = Image.open(f'button_{i}.png')
# Make large and small pictures.
large_picture = ImageTk.PhotoImage(
picture.resize(
(self.button_large_width, self.button_large_width),
Image.Resampling.LANCZOS))
small_picture = ImageTk.PhotoImage(
picture.resize(
(self.button_small_width, self.button_small_width),
Image.Resampling.LANCZOS))
# Create the "label."
label = tk.Label(frame, image=small_picture)
label.pack(side=tk.LEFT, padx=(2))
# Save parameters for later.
label.number = i
label.small_picture = small_picture
label.large_picture = large_picture
# Bind events.
label.bind('', self.enlarge_button)
label.bind('', self.shrink_button)
label.bind('', self.label_clicked)
This method first opens the image file holding the button's image. The file's name has the format button_4.png and it contains an image created by my earlier post Make glassy button images in Python.
The code then creates large and small resized versions of the image. The program will display those when the mouse is over the button or not over it. The code converts the resized images into PhotoImage objects.
Next, the method creates the Label widget that will act as a button. Initially it sets the label's image property to the small image.
Later, the label will need to have access to its large and small pictures, and to its button number, so the code saves those values in label properties. This also makes non-volatile places to store the images, which you may know if necessary for tkinter to display an image. If an image isn't stored permanently, it may be garbage collected so it won't be available later when tkinter needs to display it.
Finally, the method binds the label to the Enter, Leave, and Button-1 events. The following section describes those event handlers.
Enter, Leave, and Button-1
When the mouse moves onto one of the button labels, the following code executes.
def enlarge_button(self, event):
label = event.widget
label.configure(image=label.large_picture)
This code gets the label that launched the event and sets its image property to its large picture.
When the mouse leaves one of the button labels, the following code executes.
def shrink_button(self, event):
label = event.widget
label.configure(image=label.small_picture)
This code is very similar to the enlarge_button event handler. It gets the label that launched the event and sets its image property to its small picture.
Finally, the following code executes when the mouse clicks on a button label.
def label_clicked(self, event):
print(f'Button {event.widget.number} clicked')
This code just prints a message saying which button was clicked.
Of course the button wasn't really "clicked," the mouse was just pressed down on it. If you like, you can implement the full assortment of button behaviors. For example, you can make it display a "pressed" image when you press the mouse down over it, display the "unpressed" image if the user then moves the mouse off of the button, redisplay the "pressed" image if the user moves the mouse back over the button, and finally invoke the click event handler if the user releases the mouse while above the button. For most programs, that's probably a lot more work than it's worth.
Conclusion
If you use a Frame widget as a row container, it's not too hard to make pop buttons. No, you definitely don't need them, but they do add an extra bit of interest to an otherwise mundane user interface element.
Download the example to experiment with it and to see additional details.
|