Title: Make a skinned form in Python, Part 1
This is the first post in a series that builds a skinned form in Python. Eventually we'll be able to change skins at run time. For now, this example shows the tkinter widgets that we'll use.
|
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.
|
Widgets
The picture below shows how the widgets are arranged.
Refer to the picture above as you read the following code to see how the widgets are created. Pay attention to the widgets' Expand and Fill parameters so you can understand how they arrange themselves when the window resizes. Also notice how the code sets each widget's cursor. For example, the se_canvas widget displays the bottom_right_corner cursor so you know that you can use that widget to adjust the window's bottom right corner. I might have chosen different cursors if they were available, but these work pretty well.
class MoveBorderlessWindowApp:
def __init__(self):
self.window = tk.Tk()
self.window.title('skinned_form_part1')
self.window.protocol('WM_DELETE_WINDOW', self.kill_callback)
self.window.geometry('400x300')
# Remove the window's borders.
self.window.overrideredirect(True)
self.window.configure(borderwidth=0, highlightthickness=0)
# Make the window topmost.
# (It's not in the task list so otherwise it's hard to find.)
self.window.attributes("-topmost", True)
# Initially don't ignore drag.
self.ignore_drag = False
# Make cyan pieces tansparent.
self.window.wm_attributes('-transparentcolor', 'cyan')
self.window.configure(bg='cyan')
# Resizing rectangles.
size = 10
# Top.
top_frame = tk.Frame(self.window, bg='cyan', highlightthickness=0)
top_frame.pack(side=tk.TOP, fill=tk.X)
self.nw_canvas = tk.Canvas(top_frame, bg='yellow',
cursor='top_left_corner',
width=3*size, height=3*size,
highlightthickness=0)
self.nw_canvas.pack(side=tk.LEFT)
self.kill_rect = self.nw_canvas.create_rectangle(
size, size, 2 * size, 2 * size, fill='black')
self.nw_canvas.tag_bind(self.kill_rect, '<Button-1>',
self.kill_clicked)
self.nw_canvas.tag_bind(self.kill_rect, '<Enter>',
self.kill_mouse_enter)
self.nw_canvas.tag_bind(self.kill_rect, '<Leave>',
self.kill_mouse_leave)
self.ne_canvas = tk.Canvas(top_frame, bg='yellow',
cursor='top_right_corner',
width=3*size, height=3*size,
highlightthickness=0)
self.ne_canvas.pack(side=tk.RIGHT)
self.n_canvas = tk.Canvas(top_frame, bg='orange',
cursor='top_side',
height=size, highlightthickness=0)
self.n_canvas.pack(side=tk.TOP, fill=tk.X)
self.drag_canvas = tk.Canvas(top_frame, bg='light blue',
cursor='fleur',
height=2*size, highlightthickness=0)
self.drag_canvas.pack(side=tk.TOP, fill=tk.X)
self.drag_canvas.create_text(3, 2, text='skinned_form1', fill='red',
anchor='nw')
# Bottom.
bottom_frame = tk.Frame(self.window, bg='cyan', highlightthickness=0)
bottom_frame.pack(side=tk.BOTTOM, fill=tk.X)
self.sw_canvas = tk.Canvas(bottom_frame, bg='yellow',
cursor='bottom_left_corner',
width=size, height=size)
self.sw_canvas.pack(side=tk.LEFT)
self.se_canvas = tk.Canvas(bottom_frame, bg='yellow',
cursor='bottom_right_corner',
width=size, height=size)
self.se_canvas.pack(side=tk.RIGHT)
self.s_canvas = tk.Canvas(bottom_frame, bg='orange',
cursor='bottom_side',
height=size)
self.s_canvas.pack(side=tk.BOTTOM, fill=tk.X)
# Left.
self.w_canvas = tk.Canvas(self.window, bg='orange',
cursor='left_side',
width=size)
self.w_canvas.pack(side=tk.LEFT, fill=tk.Y)
# Right.
self.e_canvas = tk.Canvas(self.window, bg='orange',
cursor='right_side',
width=size)
self.e_canvas.pack(side=tk.RIGHT, fill=tk.Y)
# Bind mouse events and set highlightthickness to 0.
canvases = [self.drag_canvas, self.nw_canvas, self.n_canvas,
self.ne_canvas, self.e_canvas, self.se_canvas,
self.s_canvas, self.sw_canvas, self.w_canvas]
for widget in canvases:
widget.bind('<Button-1>', self.mouse_down)
widget.bind('<B1-Motion>', self.mouse_drag)
widget.config(highlightthickness=0)
# Central frame.
self.center_frame = tk.Frame(self.window, bg='plum1', borderwidth=0)
self.center_frame.pack(side=tk.TOP, expand=True, fill=tk.BOTH)
# Build the main non-border widgets.
self.make_widgets()
# Display the window.
self.window.focus_force()
self.window.mainloop()
The code first creates the window and sets some of its properties. After setting the usual things like the window's size, the code sets overrideredirect, gives it a zero border width, and a zero highlight thickness. That removes the window's borders and title bar.
Unfortunately, removing the borders and a title bar also removes the window from the task list, so it can be hard to find the window. To make it easier to find the window, the code then sets the window's topmost attribute to True so it remains above all other windows. Not a perfect solution, but the best I know how to do.
The program also sets the window's transparentcolor to cyan so any parts of the program that are cyan are invisible on the screen. This example doesn't use any cyan parts, but you could make pieces cyan to give the window a distinctive shape. We'll do that in later posts in this series.
Next, the code creates the widgets shown in the picture above. First it makes top_frame attached to the top of the window and with Fill=tk.X so it expands horizontally to fill the top of the window.
The program then creates the nw_canvas widget at the left of top_frame. Notice that the code sets the canvas's highlightthickness to 0. If you don't do that, the widgets have small gaps between them (which is really annoying).
Inside nw_canvas the code creates a rectangle and saves its ID as self.kill_rect. This rectangle performs the service normally provided by the "X" button in the form's upper right corner (at least in Windows), so the user can click it to close the window. You'll see how that works later.
To enable kill_rect to work, the code binds <Button-1> (mouse down), <Enter> (mouse enter), <Leave> (mouse leave) events on the nw_canvas widget. You'll see their code shortly.
Next, the program creates the ne_canvas widget, also inside top_frame and placed on the frame's right side.
The code then creates the n_canvas and drag_canvas widgets, stacking them above each other in the middle of top_frame.
Now the program performs similar steps to make bottom_frame and its contents sw_canvas, se_canvas, and s_canvas. It then creates w_canvas and e_canvas on the left and right sides of the window.
When you click and drag any of the edge canvases, the program needs to move and/or resize the window. For example, you can click and drag the se_canvas in the lower right to change the window's width and height. If you click and drag the nw_canvas in the window's upper left corner, you also change the position of the window's upper left corner so you both resize and move the window.
To manage all of that resizing and moving, the program binds the <Button-1> (mouse down) and <B1-Motion> (mouse drag) events for all of the move/resize canvases. It also sets their highlightthickness values to 0, again to prevent annoying gaps between them.
Having finished arranging the move/resize widgets, the program creates the center_frame. It calls make_widgets to put widgets on that frame and displays the window.
kill_rect
The kill_rect rectangle uses three events to let the user close the window. The following code executes when the mouse moves over the rectangle.
def kill_mouse_enter(self, event):
# Display the pirate cursor.
self.nw_canvas.config(cursor='pirate')
When the mouse moves over the rectangle, the program changes the cursor of the nw_canvas widget (which contains kill_rect) to the "pirate" cursor. That makes it display a skull and crossbones as a clue that clicking there will close the window. Feel free to use a different cursor or leave the cursor unchanged if you prefer.
When the cursor leaves the kill_rect, the following code executes.
def kill_mouse_leave(self, event):
# Display the NW cursor.
self.nw_canvas.config(cursor='top_left_corner')
This code resets the nw_canvas widget's cursor to its usual top_left_corner. Now if the mouse is over nw_canvas but not over kill_rect, you see the top_left_corner cursor.
If you click on kill_rect, the following code executes.
def kill_clicked(self, event):
'''The user clicked the close button. Kill the window.'''
self.ignore_drag = True
self.kill_callback()
self.ignore_drag = False
This code sets self.ignore_drag to True to tell other parts of the program (which I'll describe in the next post) to ignore mouse movement. That prevents the form from trying to resize when you click kill_rect. It calls self.kill_callback so the program can try to close just as if you had pressed Alt+F4. After that call returns, the code sets self.ignore_drag back to False so future drags can resize the window (if self.kill_callback didn't destroy it).
Here's self.kill_callback.
def kill_callback(self):
# Destroy the tkinter window.
if messagebox.askyesno('Close',
'Do you really want to close the program?'):
self.window.destroy()
This code displays a message box asking if you really want to close the program. If you click Yes (or press Y or Enter), the program kills the window.
make_widgets
The last part of the example that I want to cover in this post is the following make_widgets method. A program that uses this skinning system should use this method to create any widgets that are not part of the window movement and resizing system.
def make_widgets(self):
'''Make the main app widgets.'''
# Make a button in the center of the frame.
close_button = tk.Button(self.center_frame, text='Close', width=7,
command=self.kill_callback)
close_button.grid(row=1, column=1)
self.center_frame.columnconfigure(0, weight=1)
self.center_frame.columnconfigure(2, weight=1)
self.center_frame.rowconfigure(0, weight=1)
self.center_frame.rowconfigure(2, weight=1)
This code creates a Close button centered in the center_frame. If you click that button, the program calls the kill_callback method described earlier to display the message box and possibly kill the window.
Conclusion
That's about enough for this post. It explains how the program creates the widgets that the program uses to let the user move and resize the window. In my next post, I'll explain how those widgets do their jobs.
Meanwhile, download the example to experiment with it and to see additional details.
|