[Rod Stephens Books]
Index Books Python Examples About Rod Contact
[Mastodon] [Bluesky] [Facebook]
[Build Your Own Python Action Arcade!]

[Build Your Own Ray Tracer With Python]

[Beginning Database Design Solutions, Second Edition]

[Beginning Software Engineering, Second Edition]

[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

Title: Use gradient brushes to make button images in Python

[This example lets you make button images in Python]

I sometimes need to annotate pictures for books, and in those cases it's helpful to have some numbered button-like shapes that I can use. They're also useful for when I need actual buttons in a graphical application.

This example uses two gradient brushes to produce images that look sort of like recessed buttons. Drawing the buttons isn't too hard, but there are a few steps along the way. This post explains how the program:

  • Lets you select colors
  • Gets button-drawing parameters
  • Draws samples
  • Makes button images
  • Saves button images into files

Selecting Colors

When you click one of the program's color buttons, an event handler for that button executes. For example, the following code executes when you click the UL Color button.

def ul_button_clicked(self): '''Let the user pick the upper left color.''' self.pick_bg_color(self.ul_button)

This code calls the following pick_bg_color method, passing it the button that was clicked.

def pick_bg_color(self, button): '''Let the user pick this button's background color.''' current_color = button.cget('bg') color = colorchooser.askcolor(title='Pick Color', initialcolor=current_color) if color[1] is not None: # Set the button's background color. button.config(bg=color[1]) # Give it a contrasting foreground color. button.config(fg=contrasting_color(color[1])) # Redraw the sample. self.draw_sample()

[The colorchooser dialog lets you select a color] This method gets the button's current background color and then uses it to initialize and display tkinter's colorchooser dialog shown on the right. Click a color or use the text boxes on the right to set the color's Hue/Saturation/Luminance or Red/Green/Blue color components. When you click OK, the colorchooser returns a two-tuple giving the selected color first as a tuple holding its red, green, and blue components and second as an RGB hexadecimal string. For example, the following shows the result when I selected blue.

((0, 0, 255), '#0000ff')

The chooser returns (None, None) if you cancel the dialog, so the code checks that the first result isn't None. If you clicked OK, the code sets the button's background color to the RGB value you selected.

The code sets the button's foreground color to the result of the following contrasting_color function, which picks a color that will contrast with the background color. (Black text on a black background is hard to read.)

def contrasting_color(hex_string): '''Return black or white depending on the input color.''' # Give it a contrasting foreground color. r, g, b = hex_to_rgb(hex_string) if r + g + b < 1.5 * 255: # It's a dark color. Return white. return 'white' else: # It's a bright color. Return black. return 'black'

The contrasting_color function adds the color's red, green, and blue color components. If that total is less than half of the total possible value 3 * 255, then the color is fairly dark so the function returns white. Otherwise, if the color isn't dark, the function returns black.

Here's a summary of this part of the program.

  1. When you click a color button, the program calls pick_bg_color
  2. The pick_bg_color method displays a colorchooser. If you pick a color and click OK:
    1. The code sets the button's background color to the color you selected
    2. The program calls contrasting_color to get a color that contrasts with the background color and makes the button use the contrasting color as its foreground (text) color.
Now that you know how the program lets you pick colors, let's look at how it gets the button image parameters.

get_params

The program needs to get the drawing parameters at two times: when it draws a sample and when it saves button images into a file. Because it needs to get the parameters in multiple places, it makes sense to write a method to get the parameters. (In fact, it's often useful to have a separate method to load parameters so the code that uses those parameters doesn't need to be cluttered with parameter error handling code.)

The following code shows the get_params method that gets the button's drawing parameters.

def get_params(self): # Get parameters. ul_color = self.ul_button.cget('bg') lr_color = self.lr_button.cget('bg') fg_color = self.fg_button.cget('bg') try: width = int(self.width_var.get()) except: raise Exception('Invalid Width', 'Width must be an integer.') try: border = int(self.border_var.get()) except: raise Exception('Invalid Border', 'Border must be an integer.') # Make sure width > 2 * border. if width <= 2 * border: raise Exception('Invalid Width/Border', 'Width must be more than twice border.') # Make the scaled font. try: font_size = float(self.font_size_var.get()) except: raise Exception('Invalid Font Size', 'Font size must be a number.') font = ImageFont.truetype('times.ttf', font_size * self.SCALE) # Return the parameters. return ul_color, lr_color, fg_color, width, border, font_size, font

This method gets the background colors of the three color selection buttons. It then gets the desired button width and border thickness. If either of those isn't an integer, the method raises an exception.

When you create an Exception object, you usually pass a message into its constructor. More generally, however, the constructor can take multiple arguments. This code uses that feature to pass an error title and message into the exception object. I'll show how the program uses those shortly.

The get_params method also verifies that the button's width is more than twice its border thickness. It then gets the desired font size and uses it to create the button's font.

If get_params gets this far, it returns all of the parameters.

draw_sample

The following code shows one of the places where the program calls get_params.

def draw_sample(self): '''Draw a sample button.''' # Get parameters. try: ul_color, lr_color, fg_color, width, border, font_size, font = \ self.get_params() except Exception as e: if len(e.args) == 1: messagebox.showerror('Error', e.args[0]) else: messagebox.showerror(e.args[0], e.args[1]) return

This code calls get_params. If there's an exception, it displays the exception's title and message in a message box.

Earlier the code passed two arguments to the Exception class's constructor. In that case, the object stores the arguments in its args property. This code uses that property to recover the exception title and message.

If there is no exception, the code calls make_button_image to create a button image. It then displays the image in the label self.image_label.

make_button_image

The code that lets you pick colors and that handles various errors is important, but the following make_button_image method is the real heart of the program.

def make_button_image(self, num, ul_color, lr_color, fg_color, width, border, font): '''Draw a button for this number.''' wid = width * self.SCALE image = Image.new('RGBA', (wid, wid), color=None) dr = ImageDraw.Draw(image) # Convert the colors into PIL colors. ul_pil_color = ImageColor.getrgb(ul_color) + (255,) lr_pil_color = ImageColor.getrgb(lr_color) + (255,) # Draw the button. # Outer circle. radius = cx = cy = wid / 2 bounds = [cx - radius, cy - radius, cx + radius - 1, cy + radius - 1] start_point = (bounds[0], bounds[1]) end_point = (bounds[2], bounds[3]) brush = LinearGradientBrush(start_point, end_point, ul_pil_color, lr_pil_color) pil_fill_ellipse(image, brush, bounds) # Inner circle. radius -= border * self.SCALE bounds = [cx - radius, cy - radius, cx + radius - 1, cy + radius - 1] start_point = (bounds[0], bounds[1]) end_point = (bounds[2], bounds[3]) brush = LinearGradientBrush(start_point, end_point, lr_pil_color, ul_pil_color) pil_fill_ellipse(image, brush, bounds) # Text. dr.text(xy=(cx, cy), text=f'{num}', fill=fg_color, font=font, anchor='mm') # Scale to desired size. image = image.resize((width, width), resample=Image.LANCZOS) # Convet to PhotoImage and return. photo_image = ImageTk.PhotoImage(image) return image, photo_image

To make the image look nice, the program uses antialiasing as described in the post Provide antialiasing with PIL and Python. To do that, it enlarges the image that it draws by the factor self.SCALE. (I set that value to 4 when the program starts.)

The make_button_image method multiples the button's width by self.SCALE. It then creates a new image with that size and with a transparent background. It also creates an ImageDraw object that the method will use to draw on the new image.

Next, the code calls ImageColor.getrgb to convert the UL and LR colors from hexadecimal strings into RGB tuples. It adds the value 255 to the end of the tuples because we're working with RGBA images. (The last components, alpha, gives a pixel's opacity, so 255 means the pixel is completely opaque.)

The code then finally starts drawing. It fills the entire button with a linear gradient brush that starts in the button's upper left corner and that ends in the button's lower right corner. (For information on linear gradient brushes, see my post Make a LinearGradientBrush class for use with PIL in Python.)

The function then adjusts its bounds variable so it defines the area inside the button's border. To do that, it sets the area's radius equal to the original radius minus the border thickness times the scale factor. Having adjusted bounds, the code fills the button's center with another linear gradient brush with the positions of its colors reversed. The two brushes together give the button its three-dimensional appearance.

After drawing the button's background, the code draws the button's text on top. It then resizes the image to its desired size (this is the antialiasing part). Finally, it returns the image and the image converted into a PhotoImage.

Saving Images

The code up to now displays a button image. When you click the Save button, the program uses the following code to save a series of button images into files.

def save_button_clicked(self): '''Save the button image into a file.''' # Get parameters. try: ul_color, lr_color, fg_color, width, border, font_size, font = \ self.get_params() except Exception as e: messagebox.showerror(e.args[0], e.args[1]) return # Make and save the images. num1 = int(self.num1_var.get()) num2 = int(self.num2_var.get()) for num in range(num1, num2 + 1): image, photo_image = self.make_button_image( num, ul_color, lr_color, fg_color, width, border, font) filename = f'button_{num}.png' print(filename) image.save(filename) print('Saved')

This code first calls get_params to get the button-drawing parameters. It gets the start and stop button numbers that it should draw and then loops through those numbers.

For each button number, the code calls make_button_image to make the button's image and saves it into a file. For example, it saves the button 1 image in the file button_1.png.

Conclusion

This example creates button images but it also demonstrates a few useful techniques such as letting the user select colors, using a method to get parameters, raising and handling exceptions that have titles and messages, drawing button images, and saving those images into files. Here's a picture of File Explorer showing 10 buttons.
[Ten buttons displayed in File Explorer]
My next post will use a similar program to create button images that provide a different button style. Meanwhile, download the example to experiment with it and to see additional details.
© 2025 Rocky Mountain Computer Consulting, Inc. All rights reserved.