[Rod Stephens Books]
Index Books Python Examples About Rod Contact
[Mastodon] [Bluesky]
[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: Make a skinned form in Python, Part 3

[A skinned form built in Python]

The previous posts in this series explained how you can use widgets to let the user move and resize a window that doesn't have borders. This post shows how you can use images to give that program a distinctive appearance.

Note that I have only tested this in Windows 11. It should work in Linux- and fruit-based operating systems, but I make no promises.

When you want to load a skin, you call the app's load_skin method passing it the name of the directory that holds the skin images. Files with names like ne.png, sw.png, and s.png hold the images that should be displayed in the northeast, southwest, and south canvas widgets respectively. The goal is to have all of the information needed to draw the skin stored in image files within the skin directory.

The following code shows the load_skin method.

def load_skin(self, path): '''Load the skin images in the given directory.''' # Make a new list to permanently hold images. self.photo_images = [] # make sure the path ends with /. if not path.endswith('/'): path += '/' # Load the corner images. self.load_corner(self.ne_canvas, path + 'ne.png') self.load_corner(self.nw_canvas, path + 'nw.png') self.load_corner(self.sw_canvas, path + 'sw.png') self.load_corner(self.se_canvas, path + 'se.png') # Load the side images. self.load_side(self.w_canvas, path + 'w.png', True) self.load_side(self.e_canvas, path + 'e.png', True) self.load_side(self.n_canvas, path + 'n.png', False) self.load_side(self.s_canvas, path + 's.png', False) # Load the kill button. self.load_kill_image(self.nw_canvas, path + 'nw_close.png') self.load_kill_image(self.ne_canvas, path + 'ne_close.png') self.load_kill_image(self.sw_canvas, path + 'sw_close.png') self.load_kill_image(self.se_canvas, path + 'se_close.png') # Fill the drag canvas. self.load_filled(self.drag_canvas, path + 'drag.png') # Size the drag canvas. hgt = self.nw_canvas.winfo_height() - self.n_canvas.winfo_height() self.drag_canvas.config(height=hgt) # Set the middle frame's color. image = Image.open(path + 'c.png') fill = rgb_to_tk_color(image.getpixel((0, 0))) self.center_frame.config(background=fill) # Draw text on drag_canvas. tk.Tk.update(self.drag_canvas) x = self.drag_canvas.winfo_width() / 2 y = self.drag_canvas.winfo_height() / 2 image = Image.open(path + 'title_color.png') fill = rgb_to_tk_color(image.getpixel((0, 0))) self.text_id = self.drag_canvas.create_text( x, y, text=self.window.title(), font=('Helvetica', 12, 'bold'), fill=fill, anchor=tk.CENTER)

This code first creates a photo_images list to hold the images that the program will display. Those images must remain in memory for the widgets to be able to display them, so the list is part of the main app class.

Next, the code adds a slash to the skin directory name if it doesn't already have one.

The program then calls the load_corner, load_side, load_kill_image, and load_filled methods to load the various images. I'll describe those methods shortly.

The code sizes the drag_canvas so it takes up any unused space at the top of the window. For example, take a look at the picture at the top of this post. The yellow and black striped bar at the top is the north resize widget. The drag_canvas is the blue area below that. The drag_canvas is sized so it plus the north resize widget have the same combined height as the northwest and northeast corner widgets.

Next the method loads the c.png image and uses the color of its upper left pixel to set the background color of the center frame widget.

The final part of this method draws the window's title on the drag area. First it gets that area's size and calculates its center point. It then gets the color of the pixel in the upper left of the image file title_color.png and draws the text with that color. If you would rather position the title in the drag area's upper left corner or somewhere else, you can change this code to do so.

load_corner

The following load_corner method loads an image into one of the corner resize widgets.

def load_corner(self, canvas, filepath): '''Load the image into the corner canvas and size the canvas to fit.''' # Delete any existing images. canvas.delete(tk.ALL) # Load and display the image. image = tk.PhotoImage(file=filepath) self.photo_images.append(image) canvas.create_image(0, 0, image=image, anchor="nw") # Size the canvas to fit. canvas.config(width=image.width(), height=image.height())

This is the simplest of the image-loading methods. It first deletes any existing images from the canvas widget. It then loads the image file, adds the image to the photo_images list described earlier, and then uses create_image to display the image on the canvas. It finishes by sizing the canvas to fit the image.

load_side

The following load_side method loads an image into the left, right, top, or bottom side widget.

def load_side(self, canvas, filepath, is_vertical): '''Load the image into the side canvas and tile the image.''' # Delete any existing images. canvas.delete(tk.ALL) # Load the image. image = tk.PhotoImage(file=filepath) self.photo_images.append(image) # Size the canvas to fit. if is_vertical: # Set the canvas's width. canvas.config(width=image.width()) # Tile the image. canvas_hgt = 2000 image_hgt = image.height() for y in range(0, canvas_hgt, image_hgt): canvas.create_image(0, y, image=image, anchor="nw") else: # Set the canvas's height. canvas.config(height=image.height()) # Tile the image. canvas_wid = 2000 image_wid = image.width() for x in range(0, canvas_wid, image_wid): canvas.create_image(x, 0, image=image, anchor="nw")

This method deletes any previous images and then loads the image, saving it in the photo_images list.

If the side is vertical (the left or right side), the code sets the canvas's width equal to the image's width. It then uses a loop to draw copies of the image vertically across the canvas to fill 2000 pixels vertically. (I tried making the program draw only the necessary number of images, but it was too slow to redraw the images when the form resized without causing a lot of flicker. Drawing 200 pixel's worth of image makes the result big enough for any size on my screen. If you have a monster 96" screen, you may need to increase the number 2000.)

If the side is horizontal (the top or bottom side), the code follows similar steps to draw the images across 2000 horizontal pixels.

load_kill_image

The following load_kill_image method loads an image to close the form, replacing the normal X that appears in a window's upper right corner (at least in Windows).

def load_kill_image(self, canvas, filepath): '''Load and center the image in the canvas.''' # Do not delete existing images! # Try to load the image. try: image = tk.PhotoImage(file=filepath) self.photo_images.append(image) except: return # Get canvas's dimensions. tk.Tk.update(canvas) canvas_wid = canvas.winfo_width() canvas_hgt = canvas.winfo_height() x = int((canvas_wid - image.width()) / 2) y = int((canvas_hgt - image.height()) / 2) # Display the image. kill_rect = canvas.create_image(x, y, image=image, anchor="nw") # Set the kill rectangle's mouse event handlers. canvas.tag_bind(kill_rect, '', self.kill_clicked) canvas.tag_bind(kill_rect, '', self.kill_mouse_enter) canvas.tag_bind(kill_rect, '', self.kill_mouse_leave) # Save a reference to this canvas and its normal cursor. self.kill_canvas = canvas self.kill_canvas_cursor = canvas.cget('cursor')

The program assumes that the kill image will be placed above one of the window's corners, so this method does not delete any existing images.

It first tries to load the image. If it cannot (probably because the image file doesn't exist), the code just returns.

If it successfully loads the image, the code calculates the coordinates needed to center the image on its canvas and uses create_image to display the image.

The code then binds the events needed to tell when the user clicks the image. It also saves the canvas and that canvas's cursor for use in the following kill_mouse_enter and kill_mouse_leave event handlers.

def kill_mouse_enter(self, event): # Display the pirate cursor. self.kill_canvas.config(cursor='pirate') def kill_mouse_leave(self, event): # Display the normal cursor. self.kill_canvas.config(cursor=self.kill_canvas_cursor)

When the mouse moves over the kill rectangle image, the kill_mouse_enter event handler makes the kill canvas display the pirate cursor. When the mouse leaves that canvas, the kill_mouse_leave event handler restores the canvas's normal cursor.

You may recall that the load_skin method called load_kill_image four times like this:

# Load the kill button. self.load_kill_image(self.nw_canvas, path + 'nw_close.png') self.load_kill_image(self.ne_canvas, path + 'ne_close.png') self.load_kill_image(self.sw_canvas, path + 'sw_close.png') self.load_kill_image(self.se_canvas, path + 'se_close.png')

This lets you put the kill image over any of the window's four corners; just name the file accordingly. The load_kill_image method tries to load them all and ignores any errors when it tries to load a file that doesn't exist.

load_filled

The following load_filled method loads an image and tiles a canvas with it.

def load_filled(self, canvas, filepath): '''Fill this canvas with the image.''' # Delete any existing images. canvas.delete(tk.ALL) canvas_wid = 2000 canvas_hgt = 2000 # Load the image. image = tk.PhotoImage(file=filepath) self.photo_images.append(image) image_wid = image.width() image_hgt = image.height() # Tile the canvas with the image. for y in range(0, canvas_hgt, image_hgt): for x in range(0, canvas_wid, image_wid): canvas.create_image(x, y, image=image, anchor="nw")

This code uses a technique similar to the one used by load_side to tile the image. This time, however, it tiles the image in both the X and Y directions. The program calls this method to fill the drag area with its image.

Conclusion

This example uses the files in a directory to load the parts of the window's skin at run time. This is pretty convenient because it lets you change a program's skin without modifying its code. The hardest part is giving the images the necessary sizes. For example, the north corner images should have the same heights, and the south corner images should have the same heights.

In the next and final post in this series, I'll show how you can change skins at runtime.

© 2025 Rocky Mountain Computer Consulting, Inc. All rights reserved.