Creating Python 3 / tkinter 5 window layouts

by Alex
Creating Python 3 / tkinter 5 window layouts

Download the lesson code from GitLab: https://gitlab.com/PythonRu/tkinter-uroki Widgets determine which actions users will be able to perform using the GUI. However, it is important to pay attention to their mutual and individual placement. Effective layouts allow you to intuitively determine the meaning and priority of each graphical element, so that the user is able to quickly figure out how to interact with the program. Layout also determines the look and feel that should be seen throughout the application. For example, if the buttons are located in the upper right corner, they should always be there. While this may seem obvious to developers, end users will be confused if they don’t walk them through the application. In this piece, let’s dive into the different mechanisms that Tkinter offers for shaping the layout, grouping widgets, and managing other attributes like size and indentation.

Grouping widgets with frames

A frame is a rectangular area of a window, usually used in complex layouts. They contain other widgets. Since frames have their own internal indentation, frame and background, it should be noted that a group of widgets is logically connected. Another common feature of frames is the encapsulation of part of the application’s functionality in such a way that the abstraction can hide the implementation details of child widgets. In what follows, both scenarios will be discussed using the example of creating a component that inherits the Frame class and reveals certain information about the included widgets. Let’s create an application that includes two lists, where the first is a list of items and the second is initially empty. Both can be scrolled through. It is also possible to move items between them using the two buttons in the center: Группировка виджетов с фреймами Let’s define a subclass of Frame, which is a scrollable list and two instances of it. Two buttons will also be added to the main window:

import tkinter as tk
class ListFrame(tk.Frame):
def __init__(self, master, items=[]):
super().__init__(master)
self.list = tk.Listbox(self)
self.scroll = tk.Scrollbar(self, orient=tk.VERTICAL,
command=self.list.yview)
self.list.config(yscrollcommand=self.scroll.set)
self.list.insert(0, *items)
self.list.pack(side=tk.LEFT)
self.scroll.pack(side=tk.LEFT, fill=tk.Y)
def pop_selection(self):
index = self.list.curseselection()
if index:
value = self.list.get(index)
self.list.delete(index)
return value
def insert_item(self, item):
self.list.insert(tk.END, item)
class App(tk.Tk):
def __init__(self):
super().__init__()
months = ["January", "February", "March", "April",
"May", "June", "July", "August", "September",
"October", "November", "December"]
self.frame_a = ListFrame(self, months)
self.frame_b = ListFrame(self)
self.btn_right = tk.Button(self, text=">",
command=self.move_right)
self.btn_left = tk.Button(self, text="<",
command=self.move_left)
self.frame_a.pack(side=tk.LEFT, padx=10, pady=10)
self.frame_b.pack(side=tk.RIGHT, padx=10, pady=10)
self.btn_right.pack(expand=True, ipadx=5)
self.btn_left.pack(expand=True, ipadx=5)
def move_right(self):
self.move(self.frame_a, self.frame_b)
def move_left(self):
self.move(self.frame_b, self.frame_a)
def move(self, frame_from, frame_to):
value = frame_from.pop_selection()
if value:
frame_to.insert_item(value)
if __name__ == "__main__":
app = App()
app.mainloop()

How widget grouping works

The ListFrame class has only two methods for interacting with the internal list: pop_selection() and insert_item(). The first returns and removes the currently selected item, or does nothing if no item was selected. The second one inserts the item at the end of the list. These methods are used in the parent class to move an item from one list to another:

def move(self, frame_from, frame_to):
value = frame_from.pop_selection()
if value:
frame_to.insert_item(value)

You can also take advantage of the parent frame’s container features to properly place them with the right internal indentation:

# ...
self.frame_a.pack(side=tk.LEFT, padx=10, pady=10)
self.frame_b.pack(side=tk.RIGHT, padx=10, pady=1

Frames make it easier to manage layout geometry. Another advantage of this approach is the ability to use the geometry manager in the containers of each widget. These can be grid() for widgets in a frame or pack() for stacking the frame in the main window. However, mixing these managers in the same container in Tkinter is not allowed. Because of that the application will simply not work.

Geometry manager pack

In the past materials you could pay attention to the fact that after creating a widget it is not displayed automatically on the screen. You have to call the pack() method for each one. This implies the use of a corresponding geometry manager. This is one of the three managers available in Tkinter and it’s perfect for simple layouts, like when you want to put everything on top of each other or next to each other, for example. Suppose you want to get the following layout for an application: Geometry manager Pack It consists of three lines, where the last one has three widgets next to each other. In this case, Pack will be able to add widgets as required without using additional frames. This will use five Label widgets with different text and backgrounds to help distinguish each rectangular area:

import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
label_a = tk.Label(self, text="Label A", bg="yellow")
label_b = tk.Label(self, text="Label B", bg="orange")
label_c = tk.Label(self, text="Label C", bg="red")
label_d = tk.Label(self, text="Label D", bg="green")
label_e = tk.Label(self, text="Label E", bg="blue")
opts = { 'ipadx': 10, 'ipady': 10, 'fill': tk.BOTH }
label_a.pack(side=tk.TOP, **opts)
label_b.pack(side=tk.TOP, **opts)
label_c.pack(side=tk.LEFT, **opts)
label_d.pack(side=tk.LEFT, **opts)
label_e.pack(side=tk.LEFT, **opts)
if __name__ == "__main__":
app = App()
app.mainloop()

Parameters have also been added to the opts dictionary. They make the dimensions of each area clearer: Geometry manager Pack-2

How the Pack works

To better understand how Pack works, let’s review step by step how the widgets are added to the parent container. It’s worth paying special attention to the value of the side parameter, which defines the relative position of the widget to the next widget in the same container. First, two labels are added at the top of the screen. Let the default value of the side parameter be tk.TOP, let’s set it explicitly anyway to distinguish it from cases where tk.LEFT is used: Geometry Pack Next, we add three more labels with the tk.LEFT value of the side parameter, which results in them being placed next to each other: Geometry Pack 2 The definition of the side label_e does not play a special role, because it is the last widget that is added to the container. It is important to remember that this is the main reason why order is so important when working with Pack. To avoid unexpected results in complex layouts, it’s common practice to keep them within a frame so they don’t overlap. In such cases it is recommended to use the geometry manager Grid, because it allows you to directly set the position of each widget by calling the geometry manager and avoid using additional frames. You can pass not only tk.TOP and tk.LEFT, but also tk.BOTTOM and tk.RIGHT to the side. They will place the widgets in a different order, but this may not be intuitive, since we naturally follow from top to bottom and left to right. For example, if we replace tk.LEFT with tk.RIGHT in the last three widgets, their order will be label_e, label_d and label_c.

Geometry manager Grid

The Grid is the most flexible geometry manager available. It completely redefines the concept of grid, which is traditionally used in user interface design. A grid is a two-dimensional table divided into rows and columns, where each cell represents a space that is available for a widget. Let’s demonstrate how the Grid works using the following layout: Geometry manager Grid It can be represented as a 3×3 table, where the widgets in the second and third columns stretch across two rows, and the widget in the third row takes up all three columns. As in the previous version, we use 5 labels with different backgrounds to illustrate the distribution of cells:

import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
label_a = tk.Label(self, text="Label A", bg="yellow")
label_b = tk.Label(self, text="Label B", bg="orange")
label_c = tk.Label(self, text="Label C", bg="red")
label_d = tk.Label(self, text="Label D", bg="green")
label_e = tk.Label(self, text="Label E", bg="blue")
opts = { 'ipadx': 10, 'ipady': 10 , 'sticky': 'nswe' }
label_a.grid(row=0, column=0, **opts)
label_b.grid(row=1, column=0, **opts)
label_c.grid(row=0, column=1, rowspan=2, **opts)
label_d.grid(row=0, column=2, rowspan=2, **opts)
label_e.grid(row=2, column=0, columnspan=3, **opts)
if __name__ == "__main__":
app = App()
app.mainloop()

We will also pass a parameter dictionary including an internal indent, which will stretch the widgets to all available space inside the cells.

How the Grid works

The positioning of label_a and label_b speaks for itself: they occupy the first and second lines of the first column, respectively (it’s important not to forget that indexing starts from zero): Geometry manager Grid 2 To stretch label_c and label_d over several cells, set value 2 for the rowspan parameter. That way, they will occupy two cells, starting from the position marked by the row and column options. Finally, the columnspan value for label_e will be 3. It is important to remember that, unlike Pack, it is possible to change the order of calls to grid() for each widget without changing the final layout. The parameter sticky defines the boundaries to which the widget should stick. It is expressed in coordinates of the sides of the world: north, south, west and east. In Tkinter, these values are expressed by the constants tk.N, tk.S, tk.W and tk.E, and their combinations: tk.NW, tk.NE, tk.SW and tk.SE. For example, sticky=tk.N aligns the widget at the top border of the cell (north), and sticky=tk.SE at the bottom right corner (south-ease). Since these constants represent the corresponding lowercase characters, the expression tk.N + tk.S + tk.W + tk.E can be written as a string nwse. This means that the widget should expand horizontally and vertically at the same time – similar to the operation of fill=tk.BOTH from Pack. If parameter sticky is not passed, the widget is centered in the cell.

Geometry manager Place

The Place manager allows to set the position and size of the widget in absolute or relative values. Of the three managers this one is the least used. On the other hand, it can work with complex scenarios where there is a need to loosely place a widget or overlap another one. To demonstrate how Place works, let’s repeat the following layout, mixing absolute and relative positions and dimensions: Geometry manager Place The labels to be displayed have different backgrounds and are defined in the order in which they will be placed from left to right and top to bottom:

import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
label_a = tk.Label(self, text="Label A", bg="yellow")
label_b = tk.Label(self, text="Label B", bg="orange")
label_c = tk.Label(self, text="Label C", bg="red")
label_d = tk.Label(self, text="Label D", bg="green")
label_e = tk.Label(self, text="Label E", bg="blue")
label_a.place(relwidth=0.25, relheight=0.25)
label_b.place(x=100, anchor=tk.N,
width=100, height=50)
label_c.place(relx=0.5, rely=0.5, anchor=tk.CENTER,
relwidth=0.5, relheight=0.5)
label_d.place(in_=label_c, anchor=tk.N + tk.W,
x=2, y=2, relx=0.5, relheight=0.5,
relwidth=0.5, relheight=0.5)
label_e.place(x=200, y=200, anchor=tk.S + tk.E,
relwidth=0.25, relheight=0.25)
if __name__ == "__main__":
app = App()
app.mainloop()

If you run this program, you will see label_c and label_d overlapping in the center of the screen. This cannot be achieved with other managers.

How Place works

The first label is placed with a value of 0.25 in the relwidth and relheight parameters. This means that the widget will occupy 25% of its parent’s width and height. By default, the widgets are positioned at x=0 and y=0, and aligned to the northwest, that is, the top left corner of the screen. The second label has an absolute position, x=100. It is aligned to the upper boundary using the anchor parameter, which has a value of tk.N. The absolute size is also defined here with width and height. The third label is centered in the window with relative positioning and the anchor parameter for tk.CENTER. It is important to remember that 0.5 for relx and relwidth indicates half the parent width, and 0.5 for rely and relheight indicates half the parent height. The fourth label is located at the top of label_c. This is done with the passed argument in_ (the suffix is used because in is a reserved keyword in Python). When using in_, you may want to note that the alignment is not geometrically accurate. In this example, you need to add an offset of 2 pixels in each direction to perfectly overlap the bottom right corner of label_c. Finally, the fifth label uses absolute positioning and relative size. As you could see, these sizes are easily switched since the parent container size value is assumed (200 x 200 pixels). However, if you resize the main window, only the relative values will work. This behavior is easy to check. Another important advantage of Place is the ability to combine it with Pack and Grid. For example, imagine that there is a need to dynamically display text on a widget when you right-click on it. It can be represented as a Label widget, which is positioned in a relative position when clicked: It’s best to use other managers in your Tkinter applications, and leave the specialized ones for when you need custom positioning.

Grouping input fields with the LabelFrame widget

The LabelFrame class can be used to group multiple input widgets. It represents a logical entity with a corresponding label. It is usually used in forms and closely resembles the Frame widget. Let’s create a form with a pair of LabelFrame instances, each of which will include the corresponding input widgets: Группировка полей ввода Since the purpose of this example is to show the final layout, let’s add some widgets without saving their references as attributes:

import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
group_1 = tk.LabelFrame(self, padx=15, pady=10,
text="Personal Information")
group_1.pack(padx=10, pady=5)
tk.Label(group_1, text="Name").grid(row=0)
tk.Label(group_1, text="Last Name").grid(row=1)
tk.Entry(group_1).grid(row=0, column=1, sticky=tk.W)
tk.Entry(group_1).grid(row=1, column=1, sticky=tk.W)
group_2 = tk.LabelFrame(self, padx=15, pady=10,
text="Address")
group_2.pack(padx=10, pady=5)
tk.Label(group_2, text="Street").grid(row=0)
tk.Label(group_2, text="City").grid(row=1)
tk.Label(group_2, text="Index").grid(row=2)
tk.Entry(group_2).grid(row=0, column=1, sticky=tk.W)
tk.Entry(group_2).grid(row=1, column=1, sticky=tk.W)
tk.Entry(group_2, width=8).grid(row=2, column=1,
sticky=tk.W)
self.btn_submit = tk.Button(self, text="Send")
self.btn_submit.pack(padx=10, pady=10, side=tk.RIGHT)
if __name__ =="__main__":
app = App()
app.mainloop()

How grouping input fields works

The LabelFrame widget takes the labelWidget parameter to specify the widget to be used as a label. If it is not present, the string passed in the text parameter is displayed. For example, instead of creating an instance with tk.LabelFrame(master, text="Info"), you could replace this with the following instructions:

label = tk.Label(master, text="Info", ...)
frame = tk.LabelFrame(master, labelwidget=label)
# ...
frame.pack()

This will allow you to make any changes, such as adding an image. Note that no geometry manager is used here, because the label is placed by itself when the frame is placed.

Dynamic positioning of the widgets

The Grid is easy to use for both simple and more advanced layouts. It is also a powerful tool for combining with a list of widgets. Let’s look at how we can reduce the number of lines and call the geometry manager with just a few lines thanks to “list comprehension” and the built-in zip and enumerate functions. The application to be created includes four Entry widgets, each with a corresponding label indicating the field value. We will also add a button to display all values. Динамическое расположение виджетов Instead of creating and assigning each widget to a separate attribute, we will work with widget lists. Since the list iterates through an index, you can easily call the grid() method with the corresponding column parameter. Let’s perform aggregation of the list of labels and widgets using the zip function. The button will be created and placed separately, because it has no parameters in common with other widgets:

import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
fields = ["First Name", "Last Name", "Phone Number", "Email"]
labels = [tk.Label(self, text=f) for f in fields]
entries = [tk.Entry(self) for _ in fields]
self.widgets = list(zip(labels, entries))
self.submit = tk.Button(self, text="Print",
command=self.print_info)
for i, (label, entry) in enumerate(self.widgets):
label.grid(row=i, column=0, padx=10, sticky=tk.W)
entry.grid(row=i, column=1, padx=10, pady=5)
self.submit.grid(row=len(fields), column=1, sticky=tk.E,
padx=10, pady=10)
def print_info(self):
for label, entry in self.widgets:
print("{} = {}".format(label.cget("text"), entry.get()))
if __name__ == "__main__":
app = App()
app.mainloop()

You can enter different text in each of the fields and click the “Print” button to make sure that each tuple contains the appropriate label and text.

How Dynamic Arrangement Works

Each list generator iterates through the rows of the field list. Because labels use each item as displayable text, you only need references to the parent container – the underscore implies that the variable value is ignored. Since Python 3, the zip function returns an iterator instead of a list, so the result is an aggregation with the list function. The result is that the widgets attribute contains a list of tuples that can be traversed multiple times:

fields = ["First Name", "Last Name", "Phone", "Email"]
labels = [tk.Label(self, text=f) for f in fields]
entries = [tk.Entry(self) for _ in fields]
self.widgets = list(zip(labels, entries))

Now you need to call the geometry manager for each tuple of widgets. You can use the enumerate function to track the index of each iteration and pass it as a number row:

for i, (label, entry) in enumerate(self.widgets):
label.grid(row=i, column=0, padx=10, sticky=tk.W)
entry.grid(row=i, column=1, padx=10, pady=5)

Note that the syntax for i, (label, entry) in ... was used because you need to unpack the tuple generated by enumerate and then unpack each tuple of the widgets attribute. Inside the print_info() callback function, we’ll go through the widgets to output the text of each label with the corresponding field values. To get the text from the labels, we’ll use the cget() method, which allows us to get the value of a widget parameter by its name.

Related Posts

LEAVE A COMMENT