So far, we’ve been arranging widgets in displays by
calling their pack
methods—an
interface to the packer geometry manager in Tkinter. This section
introduces grid
, the most commonly
used alternative to the packer.
As we learned earlier, Tkinter geometry managers work by
arranging child widgets within a parent container widget (parents are
typically Frames
or top-level
windows). When we ask a widget to pack or grid itself, we’re really
asking its parent to place it among its siblings. With pack
, we provide constraints and let the
geometry manager lay out widgets appropriately. With grid
, we arrange widgets in rows and columns
in their parent, as though the parent container widget was a
table.
Gridding is an entirely distinct geometry management system in
Tkinter. In fact, at this writing, pack
and grid
are mutually exclusive for widgets that
have the same parent—within a given parent container, we can either
pack widgets or grid them, but we cannot do both. That makes sense, if
you realize that geometry managers do their jobs as parents, and a
widget can be arranged by only one geometry manager.
At least within one container, though, that means you must pick
either grid
or pack
and stick with it. So why grid, then?
In general, grid
is handy for
laying out form-like displays; arranging input fields in row/column
fashion can be at least as easy as laying out the display with nested
frames. As we’ll see, though, grid
doesn’t offer substantial code or complexity savings compared to
equivalent packer solutions in practice, especially when things like
resizability are added to the GUI picture. In other words, the choice
between the two layout schemes is largely one of style, not
technology.
Let’s start off with the basics; Example 10-18 lays out a table
of Labels
and Entry
fields—widgets we’ve already met.
Here, though, they are arrayed on a grid.
Example 10-18. PP3EGuiTourGridgrid1.py
from Tkinter import * colors = ['red', 'green', 'orange', 'white', 'yellow', 'blue'] r = 0 for c in colors: Label(text=c, relief=RIDGE, width=25).grid(row=r, column=0) Entry(bg=c, relief=SUNKEN, width=50).grid(row=r, column=1) r = r+1 mainloop( )
When run, this script creates the window shown in Figure 10-27, pictured with data typed into a few of the input fields. Once again, this book won’t do justice to the colors displayed on the right, so you’ll have to stretch your imagination a little (or run this script on a computer of your own).
This is a classic input form layout: labels on the left describe data to type into entry fields on the right. Just for fun, this script displays color names on the left and the entry field of the corresponding color on the right. It achieves its nice table-like layout with the following two lines:
Label(...).grid(row=r, column=0) Entry(...).grid(row=r, column=1)
From the perspective of the container window, the label is gridded to columns in the current row number (a counter that starts at 0), and the entry is placed in column 1. The upshot is that the grid system lays out all the labels and entries in a two-dimensional table automatically, with evenly sized columns large enough to hold the largest item in each column.
Time for some compare-and-contrast: Example 10-19 implements the
same sort of colorized input form with both grid
and pack
, to make it easy to see the
differences between the two approaches.
Example 10-19. PP3EGuiTourGridgrid2.py
# add equivalent pack window from Tkinter import * colors = ['red', 'green', 'yellow', 'orange', 'blue', 'navy'] def gridbox(parent): r = 0 for c in colors: l = Label(parent, text=c, relief=RIDGE, width=25) e = Entry(parent, bg=c, relief=SUNKEN, width=50) l.grid(row=r, column=0) e.grid(row=r, column=1) r = r+1 def packbox(parent): for c in colors: f = Frame(parent) l = Label(f, text=c, relief=RIDGE, width=25) e = Entry(f, bg=c, relief=SUNKEN, width=50) f.pack(side=TOP) l.pack(side=LEFT) e.pack(side=RIGHT) if _ _name_ _ == '_ _main_ _': root = Tk( ) gridbox(Toplevel( )) packbox(Toplevel( )) Button(root, text='Quit', command=root.quit).pack( ) mainloop( )
The basic label and entry widgets are created the same way by these two functions, but they are arranged in very different ways:
With pack
, we use
side
options to attach labels
and rows on the left and right, and create a Frame
for each row (itself attached to
the parent’s top).
With grid
, we instead
assign each widget a row
and
column
position in the
implied tabular grid of the parent, using options of the same
name.
The difference in the amount of code required for each scheme
is roughly a wash: the pack
scheme must create a Frame
per
row, but the grid
scheme must
keep track of the current row number. Running the script makes the
windows in Figure
10-28.
Notice that the prior script passes a brand-new Toplevel
to each form constructor function
so that the grid
and pack
versions wind up in distinct
top-level windows. Because the two geometry managers are mutually
exclusive within a given parent, we have to be careful not to mix
them carelessly. For instance, Example 10-20 is able to put
both the packed and the gridded widgets on the same window, but only
by isolating each in its own Frame
container widget.
Example 10-20. PP3EGuiTourGridgrid2-same.py
################################################################## # can't grid and pack in same parent container (e.g., root window) # but can mix in same window if done in different parent frames; ################################################################## from Tkinter import * from grid2 import gridbox, packbox root = Tk( ) Label(root, text='Grid:').pack( ) frm = Frame(root, bd=5, relief=RAISED); frm.pack(padx=5, pady=5) gridbox(frm) Label(root, text='Pack:').pack( ) frm = Frame(root, bd=5, relief=RAISED); frm.pack(padx=5, pady=5) packbox(frm) Button(root, text='Quit', command=root.quit).pack( ) mainloop( )
We get a composite window when this runs with two forms that look identical (Figure 10-29), but the two nested frames are actually controlled by completely different geometry managers.
On the other hand, the sort of code in Example 10-21 fails badly,
because it attempts to use pack
and grid
within the same
parent—only one geometry manager can be used on any one
parent.
Example 10-21. PP3EGuiTourGridgrid2-fails.py
################################################################## # FAILS-- can't grid and pack in same parent (root window) ################################################################## from Tkinter import * from grid2 import gridbox, packbox root = Tk( ) gridbox(root) packbox(root) Button(root, text='Quit', command=root.quit).pack( ) mainloop( )
This script passes the same parent (the top-level window) to each function in an effort to make both forms appear in one window. It also utterly hangs the Python process on my machine, without ever showing any windows at all (on Windows 98, I had to resort to Ctrl-Alt-Delete to kill it). Geometry manager combinations can be subtle until you get the hang of this; to make this example work, for instance, we simply need to isolate the grid box in a parent container all its own to keep it away from the packing going on in the root window:
root = Tk( )frm = Frame(root)
frm.pack( )
# this worksgridbox(frm)
# gridbox must have its own parent in which to grid packbox(root) Button(root, text='Quit', command=root.quit).pack( ) mainloop( )
Again, today you must either pack
or grid
within one parent, but not both. It’s
possible that this restriction may be lifted in the future, but it
seems unlikely given the disparity in the two window manager
schemes; try your Python to be sure.
And now, some practical bits: the grids we’ve seen so far are
fixed in size; they do not grow when the enclosing window is resized
by a user. Example 10-22
implements an unreasonably patriotic input form with both grid
and pack
again, but adds the configuration
steps needed to make all widgets in both windows expand along with
their window on a resize.
Example 10-22. PP3EGuiTourGridgrid3.py
# add label and resizing from Tkinter import * colors = ['red', 'white', 'blue'] def gridbox(root): Label(root, text='Grid').grid(columnspan=2) r = 1 for c in colors: l = Label(root, text=c, relief=RIDGE, width=25) e = Entry(root, bg=c, relief=SUNKEN, width=50) l.grid(row=r, column=0, sticky=NSEW) e.grid(row=r, column=1, sticky=NSEW) root.rowconfigure(r, weight=1) r = r+1 root.columnconfigure(0, weight=1) root.columnconfigure(1, weight=1) def packbox(root): Label(root, text='Pack').pack( ) for c in colors: f = Frame(root) l = Label(f, text=c, relief=RIDGE, width=25) e = Entry(f, bg=c, relief=SUNKEN, width=50) f.pack(side=TOP, expand=YES, fill=BOTH) l.pack(side=LEFT, expand=YES, fill=BOTH) e.pack(side=RIGHT, expand=YES, fill=BOTH) root = Tk( ) gridbox(Toplevel(root)) packbox(Toplevel(root)) Button(root, text='Quit', command=root.quit).pack( ) mainloop( )
When run, this script makes the scene in Figure 10-30. It builds distinct pack and grid windows again, with entry fields on the right colored red, white, and blue (or for readers not working along on a computer: gray, white, and an arguably darker gray).
This time, though, resizing both windows with mouse drags makes all their embedded labels and entry fields expand along with the parent window, as we see in Figure 10-31.
Now that I’ve shown you what these windows do, I
need to explain how they do it. We learned earlier how to make
widgets expand with pack
: we
use expand
and fill
options to increase space
allocations and stretch into them. To make expansion work for
widgets arranged by grid
, we
need to use different protocols. Rows and columns must be marked
with a weight to make them expandable, and widgets must also be
made sticky so that they are stretched within their allocated grid
cell:
With pack
, we make
each row expandable by making the corresponding Frame
expandable, with expand=YES
and fill=BOTH
. Gridders must be a bit
more specific: to get full expandability, call the grid
container’s rowconfigure
method for each row and its columnconfigure
for each column.
To both methods, pass a weight option with a value greater
than zero to enable rows and columns to expand. Weight
defaults to zero (which means no expansion), and the grid
container in this script is just the top-level window. Using
different weights for different rows and columns makes them
grow at proportionally different rates.
With pack
, we use
fill
options to stretch
widgets to fill their allocated space horizontally or
vertically, and anchor
options to position widgets within their allocated space.
With grid
, the sticky
option serves the roles of
both fill
and anchor
in the packer. Gridded
widgets can optionally be made sticky on one side of their
allocated cell space (such as anchor
) or more than one side to
make them stretch (such as fill
). Widgets can be made sticky
in four directions—N
,
S
, E
, and W
, and concatenations of these
letters specify multiple-side stickiness. For instance, a
sticky setting of W
left
justifies the widget in its allocated space (such as a
packer anchor=W
), and
NS
stretches the widget
vertically within its allocated space (such as a packer
fill=Y
).
Widget stickiness hasn’t been useful in examples thus
far because the layouts were regularly sized (widgets were
no smaller than their allocated grid cell space), and
resizes weren’t supported at all. Here, this script
specifies NSEW
stickiness
to make widgets stretch in all directions with their
allocated cells.
Different combinations of row and column weights and sticky
settings generate different resize effects. For instance, deleting
the columnconfig
lines in the
grid3
script makes the display
expand vertically but not horizontally. Try changing some of these
settings yourself to see the sorts of effects they produce.
There is one other big difference in how the grid3
script configures its windows.
Both the grid
and the pack
windows display a label on the top
that spans the entire window. For the packer scheme, we simply
make a label attached to the top of the window at large (remember,
side
defaults to TOP
):
Label(root, text='Pack').pack( )
Because this label is attached to the window’s top before any row frames are, it appears across the entire window top as expected. But laying out such a label takes a bit more work in the rigid world of grids; the first line of the grid implementation function does it like this:
Label(root, text='Grid').grid(columnspan=2
)
To make a widget span across multiple columns, we pass
grid
a columnspan
option with a spanned-column
count. Here, it just specifies that the label at the top of the
window should stretch over the entire window—across both the label
and the entry columns. To make a widget span across multiple rows,
pass a rowspan
option instead.
The regular layouts of grids can be either an asset or a
liability, depending on how regular your user interface will be;
these two span settings let you specify exceptions to the rule
when needed.
So which geometry manager comes out on top here? When
resizing is factored in, as in this script, gridding actually
becomes slightly more complex (in fact, gridding requires three
extra lines of code here). On the other hand, grid
is nice for simple forms, and your
grids and packs may vary.
So far, we’ve been building two-column arrays of labels and
input fields. That’s typical of input forms, but the Tkinter grid
manager is capable of configuring much grander matrixes. For
instance, Example 10-23
builds a five-row by four-column array of labels, where each label
simply displays its row and column number (row.col
). When run, the window in Figure 10-32 appears
on-screen.
Example 10-23. PP3EGuiTourGridgrid4.py
# simple 2D table from Tkinter import * for i in range(5): for j in range(4): l = Label(text='%d.%d' % (i, j), relief=RIDGE) l.grid(row=i, column=j, sticky=NSEW) mainloop( )
If you think this is starting to look like it might be a way
to program spreadsheets, you may be on to something. Example 10-24 takes this idea
a bit further and adds a button that prints the table’s current
input field values to the stdout
stream (usually, to the console window).
Example 10-24. PP3EGuiTourGridgrid5.py
# 2D table of input fields from Tkinter import * rows = [] for i in range(5): cols = [] for j in range(4): e = Entry(relief=RIDGE) e.grid(row=i, column=j, sticky=NSEW) e.insert(END, '%d.%d' % (i, j)) cols.append(e) rows.append(cols) def onPress( ): for row in rows: for col in row: print col.get( ), print Button(text='Fetch', command=onPress).grid( ) mainloop( )
When run, this script creates the window in Figure 10-33 and saves away all the grid’s entry field widgets in a two-dimensional list of lists. When its Fetch button is pressed, the script steps through the saved list of lists of entry widgets, to fetch and display all the current values in the grid. Here is the output of two Fetch presses—one before I made input field changes, and one after:
C:...PP3EGuiTourGrid>python grid5.py
0.0 0.1 0.2 0.3
1.0 1.1 1.2 1.3
2.0 2.1 2.2 2.3
3.0 3.1 3.2 3.3
4.0 4.1 4.2 4.3
0.0 0.1 0.2 42
1.0 1.1 1.2 43
2.0 2.1 2.2 44
3.0 3.1 3.2 45
4.0 4.1 4.2 46
Now that we know how to build and step through arrays of input fields, let’s add a few more useful buttons. Example 10-25 adds another row to display column sums and adds buttons to clear all fields to zero and calculate column sums.
Example 10-25. PP3EGuiTourGridgrid5b.py
# add column sums, clearing from Tkinter import * numrow, numcol = 5, 4 rows = [] for i in range(numrow): cols = [] for j in range(numcol): e = Entry(relief=RIDGE) e.grid(row=i, column=j, sticky=NSEW) e.insert(END, '%d.%d' % (i, j)) cols.append(e) rows.append(cols) sums = [] for i in range(numcol): l = Label(text='?', relief=SUNKEN) l.grid(row=numrow, column=i, sticky=NSEW) sums.append(l) def onPrint( ): for row in rows: for col in row: print col.get( ), print print def onSum( ): t = [0] * numcol for i in range(numcol): for j in range(numrow): t[i]= t[i] + eval(rows[j][i].get( )) for i in range(numcol): sums[i].config(text=str(t[i])) def onClear( ): for row in rows: for col in row: col.delete('0', END) col.insert(END, '0.0') for sum in sums: sum.config(text='?') import sys Button(text='Sum', command=onSum).grid(row=numrow+1, column=0) Button(text='Print', command=onPrint).grid(row=numrow+1, column=1) Button(text='Clear', command=onClear).grid(row=numrow+1, column=2) Button(text='Quit', command=sys.exit).grid(row=numrow+1, column=3) mainloop( )
Figure 10-34
shows this script at work summing up four columns of numbers; to get
a different size table, change the numrow
and numcol
variables at the top of the
script.
And finally, Example 10-26 is one last extension that is coded as a class for reusability, and adds a button to load the table from a datafile. Datafiles are assumed to be coded as one line per row, with whitespace (spaces or tabs) between each column within a row line. Loading a file of data automatically resizes the table GUI to accommodate the number of columns in the table.
Example 10-26. PP3EGuiTourGridgrid5c.py
# recode as an embeddable class from Tkinter import * from PP3E.Gui.Tour.quitter import Quitter # reuse, pack, and grid class SumGrid(Frame): def _ _init_ _(self, parent=None, numrow=5, numcol=5): Frame._ _init_ _(self, parent) self.numrow = numrow # I am a frame container self.numcol = numcol # caller packs or grids me self.makeWidgets(numrow, numcol) # else only usable one way def makeWidgets(self, numrow, numcol): self.rows = [] for i in range(numrow): cols = [] for j in range(numcol): e = Entry(self, relief=RIDGE) e.grid(row=i+1, column=j, sticky=NSEW) e.insert(END, '%d.%d' % (i, j)) cols.append(e) self.rows.append(cols) self.sums = [] for i in range(numcol): l = Label(self, text='?', relief=SUNKEN) l.grid(row=numrow+1, column=i, sticky=NSEW) self.sums.append(l) Button(self, text='Sum', command=self.onSum).grid(row=0, column=0) Button(self, text='Print', command=self.onPrint).grid(row=0, column=1) Button(self, text='Clear', command=self.onClear).grid(row=0, column=2) Button(self, text='Load', command=self.onLoad).grid(row=0, column=3) Quitter(self).grid(row=0, column=4) # fails: Quitter(self).pack( ) def onPrint(self): for row in self.rows: for col in row: print col.get( ), print print def onSum(self): t = [0] * self.numcol for i in range(self.numcol): for j in range(self.numrow): t[i]= t[i] + eval(self.rows[j][i].get( )) for i in range(self.numcol): self.sums[i].config(text=str(t[i])) def onClear(self): for row in self.rows: for col in row: col.delete('0', END) col.insert(END, '0.0') for sum in self.sums: sum.config(text='?') def onLoad(self): from tkFileDialog import * file = askopenfilename( ) if file: for r in self.rows: for c in r: c.grid_forget( ) for s in self.sums: s.grid_forget( ) filelines = open(file, 'r').readlines( ) self.numrow = len(filelines) self.numcol = len(filelines[0].split( )) self.makeWidgets(self.numrow, self.numcol) row = 0 for line in filelines: fields = line.split( ) for col in range(self.numcol): self.rows[row][col].delete('0', END) self.rows[row][col].insert(END, fields[col]) row = row+1 if _ _name_ _ == '_ _main_ _': import sys root = Tk( ) root.title('Summer Grid') if len(sys.argv) != 3: SumGrid(root).pack() # .grid( ) works here too else: rows, cols = eval(sys.argv[1]), eval(sys.argv[2]) SumGrid(root, rows, cols).pack( ) mainloop( )
Notice that this module’s SumGrid
class is careful not to either
grid or pack itself. In order to be attachable to containers where
other widgets are being gridded or packed, it leaves its own
geometry management ambiguous and requires callers to pack or grid
its instances. It’s OK for containers to pick either scheme for
their own children because they effectively seal off the
pack-or-grid choice. But attachable component classes that aim to be
reused under both geometry managers cannot manage themselves because
they cannot predict their parent’s policy.
This is a fairly long example that doesn’t say much else about gridding or widgets in general, so I’ll leave most of it as suggested reading and just show what it does. Figure 10-35 shows the initial window created by this script after changing the last column and requesting a sum.
By default, the class makes the 5 × 5 grid here, but we can pass in other dimensions to both the class constructor and the script’s command line. When you press the Load button, you get the standard file selection dialog we met earlier on this tour (Figure 10-36).
The datafile grid-data1.txt contains seven rows and six columns of data:
C:...PP3EGuiTourGrid>type grid5-data1.txt
1 2 3 4 5 6
1 2 3 4 5 6
1 2 3 4 5 6
1 2 3 4 5 6
1 2 3 4 5 6
1 2 3 4 5 6
1 2 3 4 5 6
Loading this into our GUI makes the dimensions of the grid
change accordingly—the class simply reruns its widget construction
logic after erasing all the old entry widgets with the grid_forget
method.[*] Figure
10-37 captures the scene after a file load.
The grid5-data2.txt datafile has the same
dimensions but contains expressions in two of its columns, not just
simple numbers. Because this script converts input field values with
the Python eval
built-in
function, any Python syntax will work in this table’s fields, as
long as it can be parsed and evaluated within the scope of the
onSum
method:
C:...PP3EGuiTourGrid>type grid5-data2.txt
1 2 3 2*2 5 6
1 3-1 3 2<<1 5 6
1 5%3 3 pow(2,2) 5 6
1 2 3 2**2 5 6
1 2 3 [4,3][0] 5 6
1 {'a':2}['a'] 3 len('abcd') 5 6
1 abs(-2) 3 eval('2+2') 5 6
Summing these fields runs the Python code they contain, as seen in Figure 10-38. This can be a powerful feature; imagine a full-blown spreadsheet grid, for instance—field values could be Python code “snippets” that compute values on the fly, call functions in modules, and even download current stock quotes over the Internet with tools we’ll meet in the next part of this book.
It’s also a potentially dangerous tool—a field might just
contain an expression that erases your hard drive! If you’re not
sure what expressions may do, either don’t use eval
(convert with more limited built-in
functions like int
and float
instead) or see Chapter 18 for details on the Python
rexec
restricted-execution mode
module.
Of course, this still is nowhere near a true spreadsheet program; further mutations toward that goal are left as exercises. I should also point out that there is more to gridding than we have time to present fully here. For instance, by creating subframes that have grids of their own, we can build up more sophisticated layouts in much the same way as nested frames arranged with the packer. For now, let’s move on to one last widget survey topic.
[*] grid_forget
unmaps
gridded widgets and so effectively erases them from the display.
Also see the pack_forget
widget and window withdraw
methods used in the after
event “alarm” examples of the next section for other ways to
erase and redraw GUI components.
3.145.14.132