The GTK+ toolkit and its PyGTK binding package have several convenient containers. For putting a list of widgets together, the horizontal gtk.HBox or the vertical gtk.VBox often suffice. And for aligning in both dimensions, the gtk.Table generally works fine. But in some cases, you might want to achieve nicer-looking results by setting finer requirements.
In general, placing and resizing widgets is not a computation-intensive task, but it may involve various considerations and details. Therefore, creating a manager via interpreted language has the benefit of rapidly trying various methods and settings. The container I describe in this article could encourage PyGTK developers to write or enhance other containers. Alternatively, it could spur the implementation of similar container policies in other GUI toolkits.
Let's look at an implementation of a table-container in which the pixel size of its rows and columns can expand via assigned weights. But before we dive into the details, let's review some examples.
Each of the following three examples has a different set of button children with an attachment policy. Each example is first shown given a small pixel space, followed by a more generous pixel space allowance.
- Here we have a single button for which we want to leave exactly 20 pixels on
its left and equally share whatever extra horizontal pixel space we get between
the button itself and the margin to its right. On the vertical dimension, we
distribute extra space in a ratio of (1, 4, 2) corresponding to (top margin,
button itself, bottom margin).
Figure 1. Single button within a small space
Figure 2. Single button within a bigger space
- Now we have two buttons that grow in ratios of 2:3 and 1:3 of extra
horizontal space.
Figure 3. Two buttons within a small space
Figure 4. Two buttons within a bigger space
- Next, it's multi-columns requirements where we want the "middle7/8" button to
receive 7/8 of possible extra horizontal pixels.
Figure 5. Multi-columns requirements within a small space
Figure 6. Multi-columns requirements within a bigger space
The WTable widget is similar to the standard gtk.Table. It derives from gtk.Container. The main differences are:
- No need to pre-determine the numbers of rows and columns.
- The
attachmethod of gtk.Table has the optionalxoptions=GTK.EXPAND|GTK.FILL, yoptions=GTK.EXPAND|GTK.FILL, xpadding=0, ypadding=0parameters, while ourWTable.attachmethod has an optionalglueparameter that I describe below.
Listing 1. WTable constructor
class WTable(GTK.Container):
def __init__(self, maxsize=None):
GTK.Container.__init__(self)
self.gChildren = []
self.maxsize = maxsize or (gdk.screen_width(), gdk.screen_height())
|
By default, self.maxsize in Listing 1 puts virtually
no restriction on the width and height of WTable.
Our attach method has the following interface:
Listing 2. WTable.attach interface
def attach(self, widget, leftright=(0,1), topbottom=(0,1), glue=None):
|
The leftright and
topbottom are STL (Standard Template Library)-like half-open ranges for
column(s) and row(s) respectably. A range [a,b) or (a,b] is half-open in that it
is "open" at one end (doesn't include its limit point) and "closed" at the other
(does include its limit point). Describing the glue
parameter requires a note about how I'm using the (0,1)
indices.
The horizontal and vertical dimensions are treated symmetrically. Rather than
multiplying code, we use the following dimension enumeration:
(X, Y) = (0, 1). We use it in loops, indexed two-sized
arrays. We also use similar (0,1) enumeration for left
and right, top and bottom margins, and their associated Spring classes (more on
that later).
Note that the gtk.Requisition (an object containing information about the
desired space requirements of a widget) has the width
and height fields, and the allocation is specified by
gtk.gdk.Rectangle (an object holding data about a rectangle) that has the
x, y,
width, and height fields
that do not use the convenient indexing.
When WTable attaches a widget, a child object is created and added to the
WTable's gChildren list. The child has the following
class definition:
Listing 3. Child
class Child:
"Child of WTable. Adoption data"
def __init__(self, w, leftright=(0,1), topbottom=(0,1), glue=None):
self.widget = w
# lrtb: 2x2 tuple:( Cols[begin, end), Rows[Begin, end) )
self.lrtb = (leftright, topbottom)
self.glue = glue
|
It holds the data passed to the WTable attach method.
The Glue class deals with both dimensions. It consists of two one-dimensional
Glue1D methods:
Listing 4. Glue
class Glue:
"2-Dimensional Glue"
def __init__(self, xglue=None, yglue=None):
self.xy = (xglue or Glue1D(), yglue or Glue1D())
|
Each Glue1D consists of a grow weight value
and two springs, left and right or top and bottom, depending on the dimension. We
refer to these type of values as wGrow.
Listing 5. Glue1D
class Glue1D:
"1-Dimensional Glue"
def __init__(self, preSpring=None, postSpring=None, wGrow=1):
self.springs = (preSpring or Spring(), postSpring or Spring())
self.wGrow = wGrow # of owning widget
|
Finally, the Spring class consists of the values shown in the following class constructor.
Listing 6. Spring
class Spring:
"One side attachment requirement"
def __init__(self, pad=0, wGrow=0):
self.pad = pad
self.wGrow = wGrow
|
To summarize, a Glue object consists of four Spring
objects that affect the margins' sizes. The (2x(1+2)=6)
wGrow values play two roles:
- Globally determining the relative extra pixel space the rows or columns receive
- For each attached child, locally determining the distribution of extra space between the child and its (2x2=4) margins
For convenience, we have the following "total wGrow"
method:
Listing 7. Glue1D.total_grow_weight
def total_grow_weight(self):
return self.wGrow + self.springs[0].wGrow + self.springs[1].wGrow
|
Then you return back and present the attach method.
(Note that you actually allow the option to conveniently specify a single column
and a single row instead of a columns and rows range.
Listing 8. WTable.attach
def attach(self, widget, leftright=(0,1), topbottom=(0,1), glue=None):
if glue == None:
glue = Glue()
# Conveniently change possible single n value to (n,n+1)
lrtb = map(lambda x: (type(x) == types.IntType) and (x, x+1) or x,
(leftright, topbottom))
child = Child(widget, lrtb[0], lrtb[1], glue)
self.gChildren.append(child)
if self.flags() & GTK.REALIZED:
widget.set_parent_window(self.window)
widget.set_parent(self)
self.queue_resize()
return child
|
The model for GTK+ geometry management consists of two phases:
- Requisition: In this phase, the container traverses its children and queries each for its desired size.
- Allocation: In this phase, the container is given some sub-rectangle of pixel space and divides it among its children.
Understand that both phases are recursive and that some children may well be containers themselves.
Several foo methods of gtk.Widget (the base class for
all PyGTK widgets) internally call the virtual methods
do_foo. Currently this is not explicitly documented,
but it should be clear from this article and examples listed in the
Resources section. Likewise, our implementation overrides
some of these virtual do_foo methods.
The requisition phase is implemented by the following method:
Listing 9. WTable.do_size_request
def do_size_request(self, oreq):
reqMgrs = (req.Manager(), req.Manager()) # (X,Y)
for child in self.gChildren:
request = child.widget.size_request() # compute!
for xyi in (X, Y):
be = child.lrtb[xyi]
glue1d = child.glue.xy[xyi]
sz = request[xyi] + glue1d.base()
rr = req.RangeSize(be, sz)
reqMgrs[xyi].addRangeReq(rr)
self.reqs = map(lambda m: m.solve(), reqMgrs) # (X,Y)
bw2 = 2 * self.border_width
oreq.width = min(sum(self.reqs[X]) + bw2, self.maxsize[0])
oreq.height = min(sum(self.reqs[Y]) + bw2, self.maxsize[1])
|
It returns values through the oreq.width and
oreq.height fields. The
reqMgrs objects collect the child size requisitions and
then they call the class req.Manager's solve() method.
It determines and returns the size to require for each of the columns and rows.
For now we are interested just in the sums that are returned via
oreq. Note that even though we get the solution of the
sizes for each of the columns and rows, the requirements may have been given in
ranges of columns and rows. Ranges may consist of more than a single
column or a single row.
In this phase you get some portion of the precious screen space. The allocation you get is a result of the previously queried requisition and the ancestor containers policies (one of which could be the desktop window manager that may perform a user-interactive resizing of a top-level window).
If the allocation fits the proposed WTable requirements exactly, then you simply distribute it among the WTable's children as solved by the class req.Manager in the previous step. Otherwise, you get extra space or, less likely, a deficit in pixels. In any case, you want to distribute the difference.
The wGrow values of the glue and the widget are now
being used as pseudo requirements; you collect and solve them similarly. This time
the units are not pixels, but rather relative weights whose relative quotients are
used for expanding the columns and rows or inversely shrinking them (see
miscellaneous notes).
Listing 10. WTable.do_size_allocate
def do_size_allocate(self, allocation):
self.allocation = allocation
allocs = (allocation.width, allocation.height)
alloc_offsets = (self.border_width, self.border_width)
self.crSizes = [None, None] # 2-lists: columns size, rows sizes.
self.offsets = [None, None] # alloc_offsets + partial sums of crSizes
for xyi in (X, Y):
a = allocs[xyi]
reqs = self.reqs[xyi]
gWeights = self._getGrowWeights(xyi)
self.crSizes[xyi] = given = self._divide(allocs[xyi], reqs, gWeights)
offsets = len(given) * [None]
offsets[0] = alloc_offsets[xyi]
for oi in range(1, len(offsets)):
offsets[oi] = offsets[oi - 1] + given[oi - 1]
self.offsets[xyi] = offsets
for child in self.gChildren:
self._allocate_child(child)
if self.flags() & GTK.REALIZED:
self.window.move_resize(*allocation)
|
For the steps in Listing 10, the do_size_allocate()
method:
- Determines the size of each segment (column or row) for each dimension. By
trivially adding partial sums of these sizes to
allocation.xandallocation.y, you get the offsets as well. - After the rows' and columns' allocation is determined, you set the allocation for each child. Again note that a child may occupy not necessarily a single "cell" but rather a rectangle made of ranges of columns and rows.
In the end, you call the window.move_resize() method
as needed. Note that the allocation parameter is
*-prefixed, utilizing the sequencing method provided by
the gtk.gdk.Rectangle class. (See the fields-element in
define-boxed Rectangle
that is used for auto-generating the C-code that defines
PyGdkRectangle_Type. This converts the
allocation into a tuple matching the
move_resize() method of class gtk.gdk.Window.)
In the previous requisition phase, the required pixel size for each column and row was determined. Now you need to divide a given total allocation. If it matches the requirements exactly, then you simply use them, otherwise you need to adjust by adding extra pixels or (less likely) subtracting them.
You should split the difference according to weights. Again, the user does not
explicitly assign weights to columns and rows but rather assigns
wGrow values to each child via a "Glue." You do this to
deduce the needed weights with the help of the req.Manager class.
Listing 11. WTable._getGrowWeights
def _getGrowWeights(self, xyi):
wMgr = req.Manager()
for child in self.gChildren:
be = child.lrtb[xyi]
glue1d = child.glue.xy[xyi]
rr = req.RangeSize(be, glue1d.total_grow_weight())
wMgr.addRangeReq(rr)
wMgr.solve()
gws = wMgr.reqs
if sum(gws) == 0:
gws = len(gws) * [1] # if zero weights then equalize
return gws
|
Given the "cake" allocation (Listing 12), you divide it according to requirements and weights for splitting the excess. Note that in case of a deficit, you transform the weights so that larger weights will suffer less. You also need to take care of the all-zeros weights case, which is considered as all-equal.
Listing 12. WTable._divide
def _divide(self, cake, requirements, growWeights):
n = len(requirements) # == len(growWeights)
given = requirements[:] # start with exact satisfaction
reqTotal = sum(requirements)
delta = cake - reqTotal
if delta < 0: # rarely, "invert" weights
growWeights = map(lambda x: max(growWeights) - x, growWeights)
if sum(growWeights) == 0:
growWeights = n * [1]; # equalize
i = 0
gwTotal = sum(growWeights)
while gwTotal > 0 and delta != 0:
add = (delta * growWeights[i] + gwTotal/2) / gwTotal
gwTotal -= growWeights[i]
given[i] += add
delta -= add
i += 1
return given
|
Now that the allocation for the columns and rows is determined and represented
by crSizes and offsets,
allocating the space for each child is relatively simple. The only consideration
you have to deal with is the difference between the supplied allocation for the
child and its requirement.
Listing 13. WTable._allocate_child
def _allocate_child(self, child):
offsetsxy = [None, None]
req = list( child.widget.get_child_requisition() ) # pre-calculated
for xyi in (X, Y):
segRange = child.lrtb[xyi]
g1d = child.glue.xy[xyi]
supply = sum( self.crSizes[xyi][segRange[0]: segRange[1]] )
(oadd, cadd) = g1d.place(req[xyi], supply)
offsetsxy[xyi] = self.offsets[xyi][ segRange[0] ] + oadd
req[xyi] += cadd
allocation = gdk.Rectangle(x=offsetsxy[0], y=offsetsxy[1],
width=req[0], height=req[1])
child.widget.size_allocate(allocation)
|
In order to determine how much to grow (or shrink) the child and the margins,
use the Glue1D.place method. For each child, this
method is called twice: once for each (X,Y) dimension. Given a dimension, you have
three wGrow values to consider: two of the sides (left
and right, or top and bottom) and the child's value itself.
Listing 14. Glue1D.place
def place(self, cneed, supply):
pads = self.base()
need = cneed + pads
delta = supply - need;
if delta >= 0:
gwTotal = self.total_grow_weight()
oadd = self.springs[0].pad
if gwTotal == 0:
cadd = delta
else:
oadd += round_div(delta * self.springs[0].wGrow, gwTotal)
cadd = round_div(delta * self.wGrow, gwTotal)
else: # rare
shrink = -delta
if pads >= shrink: # Cutting from the pads is sufficient
oadd = round_div(self.springs[0].pad * delta, pads)
cadd = 0
else:
oadd = 0
cadd = delta # reduce the child as well
return (oadd, cadd)
|
The Glue1D.place method returns a 2-tuple of
(oadd,cadd) integers.
-
oaddis to be added to the offset. Equivalently, it means how much the first (left or top) margin will grow. -
caddis to be added to the child's widget allocation (width or height).
It uses the already mentioned total_grow_weight()
method and the following trivial round_div function
(see PEP 238 in Resources):
Listing 15. round_div
def round_div(n, d):
return (n + d/2) / d
|
Each size requirement of columns we have demands that a range of columns [b,e)—that is all c such that b <= c < e —will have sizes whose sum is at least of some value. A requirement r is represented by the following formula:
Figure 7. Columns sizes requirement
I want to find "minimal" sizes (Si) that will satisfy the requirement inequalities. This is essentially a linear programming problem. This case is more special than the general problem, since:
- All factors are 0 or 1.
- All 1 factors are in a consecutive range of variables.
- We are interested in an integers-only solution.
Because of these special cases, a minimal solution is likely not unique, so I would also like the solution to be intuitively balanced.
I'll briefly describe the req.Manager class functionality that provides a reasonable solution to the problem. Because this is not the essence of this article and its solution method is suboptimal, I won't elaborate here.
This class lives in a separate req.py module, independent of GTK+. The module
has an inside test, via Python's
if __name__ == '__main__': construct.
Here are some examples of resolution testings. Each case is given a list of triads (Bi, Ei, Si), meaning that you look for an array of sizes such that each subrange of [Bi, Ei) will sum up to at least Si.
Listing 16. req.Manager solutions examples
python req.py 0 1 20 1 2 30
Solution: [20, 30]
python req.py 0 2 10 1 3 5
Solution: [5, 5, 1]
python req.py 0 2 100 1 3 50
Solution: [46, 54, 6]
python req.py 0 2 100 1 3 50 2 3 20
Solution: [49, 51, 21]
|
Adding requirements to req.Manager
Here is the constructor of the req.Manager class with the method for adding a requirement.
Listing 17. req.ManageraddRangeReq method
class Manager:
def __init__(self):
self.rangeReqs = []
self.reqs = None
def addRangeReq(self, rangeReq):
self.rangeReqs += [rangeReq]
|
In this, rangeReq is an object of the following
class:
Listing 18. req.RangeSize
class RangeSize:
def __init__(self, be=(0,1), sz=1):
self.begin = be[0]
self.end = be[1]
self.size = sz
|
Solve via an heuristic approach
An optimal solution would require linear programming techniques with much less trivial code.
Listing 19. req.Manager.solve
def solve(self):
n = self.nSegments()
m = len(self.rangeReqs)
self.reqs = n * [0]
self.rangeReqs.sort()
# Satisfy single segment requirements
dumSingle = RangeSize((n, n+1), 1)
endSingleIndex = bisect.bisect_right(self.rangeReqs, dumSingle)
for rr in self.rangeReqs[ : endSingleIndex]:
curr = self.reqs[rr.begin]
needMore = rr.size - curr
self.reqs[rr.begin] += needMore
bigRangeReqs = self.rangeReqs[endSingleIndex:] # non-single ranges
self.partialSatisfy(bigRangeReqs, 1, 2) # half
self.partialSatisfy(bigRangeReqs, 1, 2) # half
self.partialSatisfy(bigRangeReqs, 1, 1) # complete
return self.reqs
|
Listing 19 above includes a solve() method and takes a
simple heuristic approach that consists of the following steps:
- Sort the requirements.
- Satisfy single element requirements for [b,e) ranges such that b+1=e.
- For each yet unsatisfied requirement r, add half of the r's
unmet size evenly distributed among the elements of r's range. This is
done by calls to the
req.Manager.partialSatisfymethod. Do this step twice. - As in the previous step, now add the entire unmet size.
Listing 20. req.Manager.partialSatisfy
def partialSatisfy(self, bigRangeReqs, rationN, ratioD):
# Thru reqs, add to satisfy rationN/ratioD of requirement
for rr in bigRangeReqs:
curr = sum(self.reqs[rr.begin: rr.end])
needMore = rr.size - curr
if needMore > 0:
give = (rationN * (needMore + ratioD - 1)) / ratioD
q, r = divmod(give, rr.end - rr.begin)
for si in range(rr.begin, rr.end):
self.reqs[si] += q
for si in range(rr.begin, rr.begin + r):
self.reqs[si] += 1
|
It may be interesting to compare with the GTK+'s gtk_table_size_request C code that has:
Listing 21. Snippet from gtk_table_size_request
GTK_table_size_request_pass1 (table);
GTK_table_size_request_pass2 (table);
GTK_table_size_request_pass3 (table);
GTK_table_size_request_pass2 (table);
|
and the implementation of these
gtk_table_size_request_pass {1,2,3} functions.
The package presented in this article comes with a test program wtbl-test.py with the following main features:
- Create a gtk.ToggleButton and
Gluevia the GUI and use them to callWTable.attach(). - Modify and delete WTable's children and glues.
- Load definitions for gtk.ToggleButtons and
Glues from a text file; create and attach them. This was used to consistently create the examples under "Weighting on a table" given earlier in this article. - Dump current children and their glues to a text file in loadable format.
In addition to the WTable that is edited by the user, the control window of the test program itself uses two instances of WTable with default glues, thus verifying that WTable implementation is global-state free.
| Description | Name | Size | Download method |
|---|---|---|---|
| Sample files for this article | wtbl-2008-05-02-1101.tgz | 35KB | HTTP |
Information about download methods
Learn
-
See Writing a Custom Widget Using PyGTK
by Mark Mruss for additional material.
-
To implement a
scrollable
container in Python, check out this example by Johan Dahlin.
-
The online book
GTK+/Gnome Application Development
(New Riders Publishing) is an excellent source on GTK+ application
development and containers and widget layout.
-
The GTK+ fundamentals series (developerWorks,
December 2005—March 2006) contains an example
Hello World application in PyGTK (see Part 2):
- "Part 1: Why use GTK+?" introduces you to the world of GTK+ -- what it is, why you should consider using it, and the benefits it provides.
- "Part 2: How to use GTK+" starts you programming with GTK+.
- "Part 3: How to deploy GTK+" covers everything you need to get your product to the user.
-
"Changing the Division Operator (PEP 238)"
details proposed fixes for the current division operator's ambiguous meaning for
numerical arguments in Python.
-
Linear programming
is well defined in Wikipedia.
-
Yotam's
STL Links & Quick Reference
provides basic information about STL, including his
eight-page "Quick Reference to STL."
-
Yotam's
Linux education program in Hebrew
serves students at the Keshet democratic school.
-
"Sugar, the XO laptop, and One Laptop per Child"
(developerWorks, April 2007) demonstrates Sugar, a graphical interface written in
PyGTK.
- In the
developerWorks Linux zone,
find more resources for Linux developers, and scan our
most popular articles and
tutorials.
- See all
Linux tips
and
Linux tutorials
on developerWorks.
- Stay current with
developerWorks technical events and Webcasts.
Get products and technologies
- The
GTK+ project site has tons of resources for
GTK+, a highly usable, feature-rich toolkit for creating graphical user interfaces
with cross-platform compatibility and an easy-to-use API.
-
PyGTK is GTK+ for Python; it allows you to
easily create programs with a graphical user interface using the Python
programming language.
-
Order the SEK for Linux,
a two-DVD set containing the latest IBM trial software for Linux from DB2®,
Lotus®, Rational®, Tivoli®, and WebSphere®.
- With
IBM trial software,
available for download directly from developerWorks, build your next development
project on Linux.
Discuss
- Get involved in the
developerWorks community
through blogs, forums, podcasts, and community topics in our
new developerWorks spaces.

Yotam Medini has worked as software engineer for more than 20 years. He started with Pascal and soon moved to C and C++. For fast solutions and whenever possible, he uses Python. He currently works for Jungo Ltd. in Netanya, Israel. He also does volunteer work teaching young kids Python on Linux (in Hebrew) at Keshet democratic school.



