The last section’s
mixin class makes common tasks simpler, but it still doesn’t
address the complexity of linking up widgets such as menus and toolbars.
Of course, if we had access to a GUI layout tool that generates Python
code, this would not be an issue, at least for some of the more static
interfaces we may require. We’d design our widgets interactively, press a
button, and fill in the callback handler blanks.
Especially for a relatively simple toolkit like tkinter, though, a
programming-based approach can often work just as well. We’d like to be
able to inherit something that does all the grunt work of construction for
us, given a template for the menus and toolbars in a window. Here’s one
way it can be done—using trees of simple objects. The class in
Example 10-3
interprets data
structure representations of menus and toolbars and builds all the widgets
automatically.
Example 10-3. PP4E\Gui\Tools\guimaker.py
"""
###############################################################################
An extended Frame that makes window menus and toolbars automatically.
Use GuiMakerFrameMenu for embedded components (makes frame-based menus).
Use GuiMakerWindowMenu for top-level windows (makes Tk8.0 window menus).
See the self-test code (and PyEdit) for an example layout tree format.
###############################################################################
"""
import sys
from tkinter import * # widget classes
from tkinter.messagebox import showinfo
class GuiMaker(Frame):
menuBar = [] # class defaults
toolBar = [] # change per instance in subclasses
helpButton = True # set these in start() if need self
def __init__(self, parent=None):
Frame.__init__(self, parent)
self.pack(expand=YES, fill=BOTH) # make frame stretchable
self.start() # for subclass: set menu/toolBar
self.makeMenuBar() # done here: build menu bar
self.makeToolBar() # done here: build toolbar
self.makeWidgets() # for subclass: add middle part
def makeMenuBar(self):
"""
make menu bar at the top (Tk8.0 menus below)
expand=no, fill=x so same width on resize
"""
menubar = Frame(self, relief=RAISED, bd=2)
menubar.pack(side=TOP, fill=X)
for (name, key, items) in self.menuBar:
mbutton = Menubutton(menubar, text=name, underline=key)
mbutton.pack(side=LEFT)
pulldown = Menu(mbutton)
self.addMenuItems(pulldown, items)
mbutton.config(menu=pulldown)
if self.helpButton:
Button(menubar, text = 'Help',
cursor = 'gumby',
relief = FLAT,
command = self.help).pack(side=RIGHT)
def addMenuItems(self, menu, items):
for item in items: # scan nested items list
if item == 'separator': # string: add separator
menu.add_separator({})
elif type(item) == list: # list: disabled item list
for num in item:
menu.entryconfig(num, state=DISABLED)
elif type(item[2]) != list:
menu.add_command(label = item[0], # command:
underline = item[1], # add command
command = item[2]) # cmd=callable
else:
pullover = Menu(menu)
self.addMenuItems(pullover, item[2]) # sublist:
menu.add_cascade(label = item[0], # make submenu
underline = item[1], # add cascade
menu = pullover)
def makeToolBar(self):
"""
make button bar at bottom, if any
expand=no, fill=x so same width on resize
this could support images too: see Chapter 9,
would need prebuilt gifs or PIL for thumbnails
"""
if self.toolBar:
toolbar = Frame(self, cursor='hand2', relief=SUNKEN, bd=2)
toolbar.pack(side=BOTTOM, fill=X)
for (name, action, where) in self.toolBar:
Button(toolbar, text=name, command=action).pack(where)
def makeWidgets(self):
"""
make 'middle' part last, so menu/toolbar
is always on top/bottom and clipped last;
override this default, pack middle any side;
for grid: grid middle part in a packed frame
"""
name = Label(self,
width=40, height=10,
relief=SUNKEN, bg='white',
text = self.__class__.__name__,
cursor = 'crosshair')
name.pack(expand=YES, fill=BOTH, side=TOP)
def help(self):
"override me in subclass"
showinfo('Help', 'Sorry, no help for ' + self.__class__.__name__)
def start(self):
"override me in subclass: set menu/toolbar with self"
pass
###############################################################################
# Customize for Tk 8.0 main window menu bar, instead of a frame
###############################################################################
GuiMakerFrameMenu = GuiMaker # use this for embedded component menus
class GuiMakerWindowMenu(GuiMaker): # use this for top-level window menus
def makeMenuBar(self):
menubar = Menu(self.master)
self.master.config(menu=menubar)
for (name, key, items) in self.menuBar:
pulldown = Menu(menubar)
self.addMenuItems(pulldown, items)
menubar.add_cascade(label=name, underline=key, menu=pulldown)
if self.helpButton:
if sys.platform[:3] == 'win':
menubar.add_command(label='Help', command=self.help)
else:
pulldown = Menu(menubar) # Linux needs real pull down
pulldown.add_command(label='About', command=self.help)
menubar.add_cascade(label='Help', menu=pulldown)
###############################################################################
# Self-test when file run standalone: 'python guimaker.py'
###############################################################################
if __name__ == '__main__':
from guimixin import GuiMixin # mix in a help method
menuBar = [
('File', 0,
[('Open', 0, lambda:0), # lambda:0 is a no-op
('Quit', 0, sys.exit)]), # use sys, no self here
('Edit', 0,
[('Cut', 0, lambda:0),
('Paste', 0, lambda:0)]) ]
toolBar = [('Quit', sys.exit, {'side': LEFT})]
class TestAppFrameMenu(GuiMixin, GuiMakerFrameMenu):
def start(self):
self.menuBar = menuBar
self.toolBar = toolBar
class TestAppWindowMenu(GuiMixin, GuiMakerWindowMenu):
def start(self):
self.menuBar = menuBar
self.toolBar = toolBar
class TestAppWindowMenuBasic(GuiMakerWindowMenu):
def start(self):
self.menuBar = menuBar
self.toolBar = toolBar # guimaker help, not guimixin
root = Tk()
TestAppFrameMenu(Toplevel())
TestAppWindowMenu(Toplevel())
TestAppWindowMenuBasic(root)
root.mainloop()
To make sense of this module, you have to be familiar with the menu
fundamentals introduced in
Chapter 9
.
If you are, though, it’s straightforward—theGuiMaker
class simply traverses the menu and
toolbar structures and builds menu and toolbar widgets along the way. This
module’s self-test code includes a simple example of the data structures
used to lay out menus and toolbars:
Lists and nested sublists of(
label
,
underline
,
handler
)
triples. If ahandler
is a sublist rather than a
function or method, it is assumed to be a cascading submenu.
List of(
label
,
handler
,
pack-options
)
triples.pack-options
is coded as a dictionary of
options passed on to the widgetpack
method; we can code these as{'k':v}
literals, or with thedict(k=v)
call’s keyword syntax.pack
accepts a dictionary argument, but we
could also transform the dictionary into individual keyword
arguments
by using Python’sfunc(**kargs)
call syntax. As is, labels
are assumed to be text, but images could be supported too (see the
note under
BigGui: A Client Demo Program
).
For variety, the mouse cursor changes based upon its location: a
hand in the toolbar, crosshairs in the default middle part, and something
else over Help buttons of frame-based menus (customize
as desired).
In addition to menu
and toolbar layouts, clients of this class can also tap
into and customize the method and geometry protocols the class
implements:
Clients of this class are expected to setmenuBar
andtoolBar
attributes somewhere in the
inheritance chain by the time thestart
method has finished.
Thestart
method can be
overridden to construct menu and toolbar templates dynamically,
sinceself
is available when it
is called;start
is also where
general initializations should be performed—GuiMixin
’s__init__
constructor must be run, not
overridden.
ThemakeWidgets
method
can be redefined to construct the middle part of the window—the
application portion between the menu bar and the toolbar. By
default,makeWidgets
adds a
label in the middle with the name of the most specific class, but
this method is expected to be specialized.
In a specializedmakeWidgets
method, clients may attach
their middle portion’s widgets to any side ofself
(aFrame
) since the menu and toolbars have
already claimed the container’s top and bottom by the timemakeWidgets
is run. The middle
part does not need to be a nested frame if its parts are packed.
The menu and toolbars are also automatically packed first so that
they are clipped last if the window shrinks.
The middle part can contain a grid layout, as long as it is
gridded in a nestedFrame
that
is itself packed within theself
parent. (Remember that each
container level may usegrid
orpack
, not both, and thatself
is aFrame
with already packed bars by the
timemakeWidgets
is called.)
Because theGuiMaker Frame
packs itself within its parent, it is not directly embeddable in a
container with widgets arranged in a grid, for similar reasons;
add an intermediate griddedFrame
to use it in this context.
In return for conforming toGuiMaker
protocols and
templates, client subclasses get aFrame
that knows how to automatically build up
its own menus and toolbars from template data structures. If you read
the preceding chapter’s menu examples, you probably know that this is a
big win in terms of reduced coding requirements.GuiMaker
is also clever enough to export
interfaces for both menu styles that we met in
Chapter 9
:
GuiMakerWindowMenu
Implements Tk 8.0-style top-level window menus, useful for
menus associated with standalone programs and pop ups.
GuiMakerFrameMenu
Implements alternativeFrame/Menubutton
-based menus, useful for
menus on objects embedded as components of a larger GUI.
Both classes build toolbars, export the same protocols, and expect
to find the same template structures; they differ only in the way they
process menu templates. In fact, one is simply a subclass of the other
with a specialized menu maker method—only top-level menu processing
differs between the two styles (aMenu
withMenu
cascades rather than aFrame
withMenubuttons
).
LikeGuiMixin
, when
we run
Example 10-3
as a top-level
program, we trigger the self-test logic at the bottom of its file;
Figure 10-2
shows the windows we get. Three
windows come up, representing each of the self-test code’sTestApp
classes. All three have a menu and
toolbar with the options specified in the template data structures
created in the self-test code: File and Edit menu pull downs, plus a
Quit toolbar button and a standard Help menu button. In the screenshot,
one window’s File menu has been torn off and the Edit menu of another is
being pulled down; the lower window was resized for effect.
Figure 10-2. GuiMaker self-test at work
GuiMaker
can be mixed in with
other superclasses, but it’s primarily intended to serve the same
extending and embedding roles as a tkinterFrame
widget class (which makes sense, given
that it’s really just a customizedFrame
with extra construction protocols). In
fact, its self-test combines aGuiMaker
frame with the prior section’sGuiMixin
tools package class.
Because of the superclass relationships coded, two of the three
windows get theirhelp
callback
handler fromGuiMixin
;TestAppWindowMenuBasic
getsGuiMaker
’s instead. Notice that the order in
which these two classes are mixed can be important: because bothGuiMixin
andFrame
define aquit
method, we need to list the class from
which we want to get it first in the mixed class’s header line due to
the left-to-right search rule of multiple inheritance. To selectGuiMixin
’s methods, it should usually
be listed before a superclass derived from real widgets.
We’ll putGuiMaker
to more
practical use in instances such as the PyEdit example in
Chapter 11
. The next section shows another way
to useGuiMaker
’s templates to build
up a sophisticated interface, and serves as another test of its
functionality.