Python Forum
[Tkinter] Getting Tkinter Grid Sizing Right the first time
Thread Rating:
  • 1 Vote(s) - 5 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[Tkinter] Getting Tkinter Grid Sizing Right the first time
#1
Hello,

Introduction:

This is a subject that is asked over and over again.

Laying out a tkinter GUI and having it resize properly can be a daunting task.
It doesn't have to be, if a few simple rules are followed.

The basis for the code presented here came from the tutorial at http://www.tkdocs.com/tutorial/grid.html
My methods are somewhat different, so although the code presented here will look familiar, it has been modified quite a bit.
You may wish to visit the original tutorial as I feel it is superior to mine, however it doesn't hurt to have another view.
 
1. First it's in a class. I find this simplifies usage and keeps it containerized.
2. I have added a method for displaying all widgets, their attributes, data types,
    and current values. This is a great help while debugging.

A lot of programmers use pack geometry exclusively for their GUI's.
This works fine for many applications, but almost always will have you pulling
your hair out when the GUI starts to become complicated.

I don't even think of place geometry, unless I am building an instrument panel or similar application
where the only things I want moving are the values in dials, meters, and other indicators.

I often use a combination of pack and grid.
Pack for containers, and grid for their contents
This can make resizing difficult if not impossible, so if you need to resize don't do it (unless you like pain)

The very best method for resizable GUI's that can grow large with ease is the grid geometry.
When done properly, it gives a really finished look and feel to your GUI.
This tutorial will use the grid geometry.


I will also use ttk because it allows for styling themes  and other advanced features.
I won't be using these too much in this tutorial, but will be expanding the code and using
the style capability, as well as option file in future tutorials.

<--->

Design Layout:

The first thing you want to do is layout your window design. I have found that
a spreadsheet is ideal for this purpose

   

Gui Screenshot:

Screenshot:
   

Source Code:
#
# Laying out a tkinter grid
#
# Please also find two images:
#    GridLayout.png and screenshot.png
# Credit: Modified by Larz60+ From the original:
#    'http://www.tkdocs.com/tutorial/grid.html'
#    
from tkinter import *
from tkinter import ttk


class ResizableWindow:
    def __init__(self, parent):
        self.parent = parent
        self.f1_style = ttk.Style()
        self.f1_style.configure('My.TFrame', background='#334353')
        self.f1 = ttk.Frame(self.parent, style='My.TFrame', padding=(3, 3, 12, 12))  # added padding

        self.f1.grid(column=0, row=0, sticky=(N, S, E, W))  # added sticky
        self.f2 = ttk.Frame(self.f1, borderwidth=5, relief="sunken", width=200, height=100)
        self.namelbl = ttk.Label(self.f1, text="Name")
        self.name = ttk.Entry(self.f1)

        self.onevar = BooleanVar()
        self.twovar = BooleanVar()
        self.threevar = BooleanVar()

        self.onevar.set(True)
        self.twovar.set(False)
        self.threevar.set(True)

        self.one = ttk.Checkbutton(self.f1, text="One", variable=self.onevar, onvalue=True)
        self.two = ttk.Checkbutton(self.f1, text="Two", variable=self.twovar, onvalue=True)
        self.three = ttk.Checkbutton(self.f1, text="Three", variable=self.threevar, onvalue=True)
        self.ok = ttk.Button(self.f1, text="Okay")
        self.cancel = ttk.Button(self.f1, text="Cancel")

        self.f1.grid(column=0, row=0, sticky=(N, S, E, W))  # added sticky
        self.f2.grid(column=0, row=0, columnspan=3, rowspan=2, sticky=(N, S, E, W))  # added sticky
        self.namelbl.grid(column=3, row=0, columnspan=2, sticky=(N, W), padx=5)  # added sticky, padx
        self.name.grid(column=3, row=1, columnspan=2, sticky=(N, E, W), pady=5, padx=5)  # added sticky, pady, padx
        self.one.grid(column=0, row=3)
        self.two.grid(column=1, row=3)
        self.three.grid(column=2, row=3)
        self.ok.grid(column=3, row=3)
        self.cancel.grid(column=4, row=3)

        # added resizing configs
        self.parent.columnconfigure(0, weight=1)
        self.parent.rowconfigure(0, weight=1)
        self.f1.columnconfigure(0, weight=3)
        self.f1.columnconfigure(1, weight=3)
        self.f1.columnconfigure(2, weight=3)
        self.f1.columnconfigure(3, weight=1)
        self.f1.columnconfigure(4, weight=1)
        self.f1.rowconfigure(1, weight=1)

    def get_widget_attributes(self):
        all_widgets = self.f1.winfo_children()
        for widg in all_widgets:
            print('\nWidget Name: {}'.format(widg.winfo_class()))
            keys = widg.keys()
            for key in keys:
                print("Attribute: {:<20}".format(key), end=' ')
                value = widg[key]
                vtype = type(value)
                print('Type: {:<30} Value: {}'.format(str(vtype), value))


def main():
    root = Tk()
    rw = ResizableWindow(root)
    rw.get_widget_attributes()
    root.mainloop()


if __name__ == '__main__':
    main()
Towards the end of the __init__ method, there are several column and row configure statements
These, along with the 'sticky' arguments are what tells the resizer how to distribute the weight for each section of the gui.

Sticky: (Credit: http://infohost.nmt.edu/tcc/help/pubs/tk.../grid.html)


The sticky part by itself doesn't do the trick, it is not a resize instruction, but has the effect of resizing within a cell.
In other words, It's purpose is to show where in the grid cell you want the widget to stick.

N for top, if the widget is smaller than the cell (the usual condition), widget will still be in the middle of the cell. Extra verticle space
will be totally on the bottom of the cell.

W + E will stretch the widget horizontally to go the entire distance of the cell from left to right (padding excepted - more on this later)
N + S will stretch the widget vertically to go the entire distance of the cell from top to bottom (padding excepted)
N + S + E + W will stretch in all directions (padding excepted).

sticky can be written two different ways. There are the built-in (to tkinter) constants N, S, E, W or place the values n, s, e, w in a string
as follows:
    Widget(..., sticky= N + S)
    Widget(..., sticky='ns')
rowconfigute and columnconfigure:

To actually control resizing of the window, you need rowconfigure  and columnconfigure which provide scale information.
the syntax is

widget.columnconfigure(N, option=value, ...)

Inside widget, configure column N so that the given option has the given value.

and

widget.rowconfigure(N, option=value, ...)

Inside widget, configure row N so that the given option has the given value.

Available options are:
  • minsize - The column or row's minimum size in pixels. If there is nothing in the given column or row, it will not appear, even if you use this option.
  • pad - A number of pixels that will be added to the given column or row, over and above the largest cell in the column or row.
  • weight - To make a column or row stretchable, use this option and supply a value that gives the relative weight of this column or row when distributing the extra space. For example, if a widget w contains a grid layout, these lines will distribute three-fourths of the extra space to the first column and one-fourth to the second column:
        w.columnconfigure(0, weight=3)    w.columnconfigure(1, weight=1)
  • If this option is not used, the column or row will not stretch. 
<-->
Determining weight:

The weight option sets the relative weight for dispensing any extra spaces among rows/columns.
A weight of zero indicates that the cellis not to deviate from it's requested size, otherwise the
number indicates the overall or relative growth rate in comparison to the weights of all the other
rows/columns. If the weight is 3, it grows at three times the rate of a row/column with a weight one
one when the window is stretched.

The first two instances are:
        self.parent.columnconfigure(0, weight=1)
        self.parent.rowconfigure(0, weight=1)
These are for the parent directory (which in this case is root, but doesn't have to be, it could also

be a portion of a larger GUI.

Both the X and the Y scaling remain constant over the entire widget, so only one rowconfigure
and one columnconfigure as required for the parent.

The first controls the resizing of columns (vertical) for the entire parent widget and
the second controls the resizing of rows (horizontal) for the entire parent widget

the zero in each refers to column and row respectfully. a weight of 1 will be applied to the full range of both
the scaling remains the same for each row and/or column until specifically changed.

<--->

For this GUI, controling the expansion of  self.f1 and only self.f1 will do the trick as all widgets inside f1 expand as a
group. This frame will need 1 rowconfigure (row expansion is symmetrical across the entire window) and
5 columnconfigures (column scale changes in five places).


self.f1 is set up as a container for all of the widgets in the GUI:
        self.parent = parent
        self.f1_style = ttk.Style()
        self.f1_style.configure('My.TFrame', background='#334353')
        self.f1 = ttk.Frame(self.parent, style='My.TFrame', padding=(3, 3, 12, 12))  # added padding

        self.f1.grid(column=0, row=0, sticky=(N, S, E, W))  # added sticky
This widget is not completely visible (on the display), because it 'contains' all of the other widgets,

and is displayed underneath the widgets it contains. The Frame (self.f1)  is the same size as the parent
widget less borders.

Note the sticky - contains all four compass directions which indicates fill the current allotted space.

The resizing associated with this frame is:
        self.f1.columnconfigure(0, weight=3)
        self.f1.columnconfigure(1, weight=3)
        self.f1.columnconfigure(2, weight=3)
        self.f1.columnconfigure(3, weight=1)
        self.f1.columnconfigure(4, weight=1)
        self.f1.rowconfigure(1, weight=1)
Since all of the widgets are contained within this frame, all resizing is associated with this frame(self.f1)
Again the first argument associated relates to the column number or row number

The following columns of f1 must be weighted
Output:
column weight affects: ------ ------ -------------------------    0      3   self.f2    1      3   self.f2    2      3   self.one, self.two, self.three    3      1   self.ok    4      1   self.cancel
Take a look at the design image above (click to expand) to see how these fit.

Padding:

At this point, the window should be looking pretty good.

But some of widgets seem to be running into  each other, giving an unfinished look.

Here's where padding comes in.

When a widget (such as Label) has text, can contain other
widgets, and for other reasons has something that may be placed inside the widget, it will
have two types of padding, internal as well as external.

padx and pady refer to external padding, and ipadx and ipady refer to internal padding.
Unfortunately this is not always the the case for padx and pady.
Some widgets use these names for internal padding Frame is one of them. These are usually
specified in the Frame statement, separate from the grid statement

you can use the padx, pady, ipadx and ipady in the grid statement. This also allows for a
two element list so different spacing can be placed on the left or right, or on top or bottom.

The value is always in pixels.

It is also possible to add padding to rowconfigure and columncomfigure. If done here, the
padding will be around the entire row or column

Extras - get_widget_attributes

    def get_widget_attributes(self):
        all_widgets = self.f1.winfo_children()
        for widg in all_widgets:
            print('\nWidget Name: {}'.format(widg.winfo_class()))
            keys = widg.keys()
            for key in keys:
                print("Attribute: {:<20}".format(key), end=' ')
                value = widg[key]
                vtype = type(value)
                print('Type: {:<30} Value: {}'.format(str(vtype), value))
Output:
Output:
Widget Name: TFrame Attribute: borderwidth          Type: <class 'int'>                  Value: 5 Attribute: padding              Type: <class 'str'>                  Value: Attribute: relief               Type: <class '_tkinter.Tcl_Obj'>     Value: sunken Attribute: width                Type: <class 'int'>                  Value: 200 Attribute: height               Type: <class 'int'>                  Value: 100 Attribute: takefocus            Type: <class 'str'>                  Value: Attribute: cursor               Type: <class 'str'>                  Value: Attribute: style                Type: <class 'str'>                  Value: Attribute: class                Type: <class 'str'>                  Value: Widget Name: TLabel Attribute: background           Type: <class 'str'>                  Value: ...
You can use this to provise list of each widget and it's attributes.

Hope you find this useful -- Larz60+
Reply


Messages In This Thread
Getting Tkinter Grid Sizing Right the first time - by Larz60+ - Nov-06-2016, 06:34 AM

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020