Layout
The application layout in Tkinter is controlled with geometry managers. Tkinter has three of them:
.pack()
.place()
.grid()
Each window or Frame in your application can use only one geometry manager. However, different frames can use different geometry managers, even if they’re assigned to a frame or window using another geometry manager.
.pack()
The .pack()
geometry manager uses a packing algorithm to place widgets in a Frame or window in a specified order. For a given widget, the packing algorithm has two primary steps:
- Compute a rectangular area called a parcel that’s just tall (or wide) enough to hold the widget and fills the remaining width (or height) in the window with blank space.
- Center the widget in the parcel unless a different location is specified.
.pack()
is powerful, but it can be difficult to visualize. The best way to get a feel for .pack()
is to look at some examples. See what happens when you pack three Label widgets into a frame:
import tkinter as tk
window = tk.Tk()
frame1 = tk.Frame(master=window, width=100, height=100, bg="red")
frame1.pack()
frame2 = tk.Frame(master=window, width=50, height=50, bg="yellow")
frame2.pack()
frame3 = tk.Frame(master=window, width=25, height=25, bg="blue")
frame3.pack()
window.mainloop()
.pack()
places each Frame below the previous one by default, in the order that they’re assigned to the window:
Each Frame is placed at the topmost available position. Therefore, the red Frame is placed at the top of the window. Then the yellow Frame is placed just below the red one and the blue Frame just below the yellow one.
More Info
There are three invisible parcels, each containing one of the three Frame widgets. Each parcel is as wide as the window and as tall as the Frame that it contains. Because no anchor point was specified when .pack() was called for each Frame, they’re all centered inside of their parcels. That’s why each Frame is centered in the window.
Fill
.pack()
accepts some keyword arguments for more precisely configuring widget placement. For example, you can set the fill keyword argument to specify in which direction the frames should fill. The options are tk.X to fill in the horizontal direction, tk.Y to fill vertically, and tk.BOTH to fill in both directions. Here’s how you would stack the three frames so that each one fills the whole window horizontally:
import tkinter as tk
window = tk.Tk()
frame1 = tk.Frame(master=window, height=100, bg="red")
frame1.pack(fill=tk.X)
frame2 = tk.Frame(master=window, height=50, bg="yellow")
frame2.pack(fill=tk.X)
frame3 = tk.Frame(master=window, height=25, bg="blue")
frame3.pack(fill=tk.X)
window.mainloop()
Notice that the width is not set on any of the Frame widgets. The width is no longer necessary because each frame sets .pack()
to fill horizontally, overriding any width you may set.
The window produced by this script looks like this:
One of the nice things about filling the window with .pack()
is that the fill is responsive to window resizing.
Side
The side keyword argument of .pack()
specifies on which side of the window the widget should be placed. These are the available options:
- tk.TOP
- tk.BOTTOM
- tk.LEFT
- tk.RIGHT
If you don’t set side, then .pack()
will automatically use tk.TOP
and place new widgets at the top of the window, or at the topmost portion of the window that isn’t already occupied by a widget. For example, the following script places three frames side by side from left to right and expands each frame to fill the window vertically:
import tkinter as tk
window = tk.Tk()
frame1 = tk.Frame(master=window, width=200, height=100, bg="red")
frame1.pack(fill=tk.Y, side=tk.LEFT)
frame2 = tk.Frame(master=window, width=100, bg="yellow")
frame2.pack(fill=tk.Y, side=tk.LEFT)
frame3 = tk.Frame(master=window, width=50, bg="blue")
frame3.pack(fill=tk.Y, side=tk.LEFT)
window.mainloop()
This time, you have to specify the height keyword argument on at least one of the frames to force the window to have some height.
The resulting window looks like this:
Just like when you set fill=tk.X
to make the frames responsive when you resized the window horizontally, you can set fill=tk.Y
to make the frames responsive when you resize the window vertically:
To make the layout truly responsive, you can set an initial size for your frames using the width and height attributes. Then, set the fill keyword argument of .pack()
to tk.BOTH
and set the expand keyword argument to True:
import tkinter as tk
window = tk.Tk()
frame1 = tk.Frame(master=window, width=200, height=100, bg="red")
frame1.pack(fill=tk.BOTH, side=tk.LEFT, expand=True)
frame2 = tk.Frame(master=window, width=100, bg="yellow")
frame2.pack(fill=tk.BOTH, side=tk.LEFT, expand=True)
frame3 = tk.Frame(master=window, width=50, bg="blue")
frame3.pack(fill=tk.BOTH, side=tk.LEFT, expand=True)
window.mainloop()
When you run the above script, you’ll see a window that initially looks the same as the one you generated in the previous example. The difference is that now you can resize the window however you want, and the frames will expand and fill the window responsively.
.place()
You can use .place()
to control the precise location that a widget should occupy in a window or Frame. You must provide two keyword arguments, x and y, which specify the x- and y-coordinates for the top-left corner of the widget. Both x and y are measured in pixels, not text units.
Keep in mind that the origin, where x and y are both 0, is the top-left corner of the Frame or window. So, you can think of the y argument of .place()
as the number of pixels from the top of the window, and the x argument as the number of pixels from the left edge of the window.
Here’s an example of how it works:
import tkinter as tk
window = tk.Tk()
frame = tk.Frame(master=window, width=150, height=150)
frame.pack()
label1 = tk.Label(master=frame, text="I'm at (0, 0)", bg="red")
label1.place(x=0, y=0)
label2 = tk.Label(master=frame, text="I'm at (75, 75)", bg="yellow")
label2.place(x=75, y=75)
window.mainloop()
- Lines 5 and 6 create a new Frame widget called frame, measuring 150 pixels wide and 150 pixels tall, and pack it into the window with
.pack()
. - Lines 8 and 9 create a new Label called label1 with a red background and place it in frame1 at position (0, 0).
- Lines 11 and 12 create a second Label called label2 with a yellow background and place it in frame1 at position (75, 75).
Here’s the window that the code produces:
Note that if you run this code on a different operating system that uses different font sizes and styles, then the second label might become partially obscured by the window’s edge. That’s why .place()
isn’t used often. In addition to this, it has two main drawbacks:
- Layout can be difficult to manage with
.place()
. This is especially true if your application has lots of widgets. - Layouts created with
.place()
aren’t responsive. They don’t change as the window is resized.
One of the main challenges of cross-platform GUI development is making layouts that look good no matter which platform they’re viewed on, and .place()
is a poor choice for making responsive and cross-platform layouts.
.grid()
The geometry manager you’ll likely use most often is .grid()
, which provides all the power of .pack() in a format that’s easier to understand and maintain.
.grid() works by splitting a window or Frame into rows and columns. You specify the location of a widget by calling .grid() and passing the row and column indices to the row and column keyword arguments, respectively. Both row and column indices start at 0, so a row index of 1 and a column index of 2 tells .grid() to place a widget in the third column of the second row.
The following script creates a 3 × 3 grid of frames with Label widgets packed into them:
import tkinter as tk
window = tk.Tk()
for i in range(3):
for j in range(3):
frame = tk.Frame(
master=window,
relief=tk.RAISED,
borderwidth=1
)
frame.grid(row=i, column=j)
label = tk.Label(master=frame, text=f"Row {i}\nColumn {j}")
label.pack()
window.mainloop()
Here’s what the resulting window looks like: