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. We’d design our widgets interactively, press a button, and fill in the callback handler blanks.
For now, a programming-based approach can 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 11-2 interprets data structure representations of menus and toolbars and builds all the widgets automatically.
Example 11-2. PP3EGuiToolsguimaker.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 types import * # type constants class GuiMaker(Frame): menuBar = [] # class defaults toolBar = [] # change per instance in subclasses helpButton = 1 # 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) == ListType: # list: disabled item list for num in item: menu.entryconfig(num, state=DISABLED) elif type(item[2]) != ListType: 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 """ 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 """ from tkMessageBox import showinfo showinfo('Help', 'Sorry, no help for ' + self._ _class_ _._ _name_ _) def start(self): pass # override me in subclass ############################################################################### # 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
10. If you are, though, it’s straightforward—the GuiMaker
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 a handler
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 widget pack
method (it accepts dictionaries,
but we could also transform the dictionary into keyword
arguments by passing it as a third argument to apply
or by using the newer func(*pargs, **kargs)
syntax).
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 set menuBar
and toolBar
attributes somewhere in the
inheritance chain by the time the start
method has finished.
The start
method can
be overridden to construct menu and toolbar templates
dynamically (since self
is
then available); start
is
also where general initializations should be
performed—GuiMixin
’s
_ _init_ _
constructor must
be run, not overridden.
The makeWidgets
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 specialized makeWidgets
method, clients may
attach their middle portion’s widgets to any side of self
(a Frame
) since the menu and toolbars
have already claimed the container’s top and bottom by the
time makeWidgets
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 nested Frame
that is itself packed within
the self
parent. (Remember
that each container level may use grid
or pack
, not both, and that self
is a Frame
with already packed bars by
the time makeWidgets
is
called.) Because the GuiMaker
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 gridded
Frame
to use it in this
context.
In return for conforming to GuiMaker
protocols and templates, client
subclasses get a Frame
that knows
how to automatically build up its own menus and toolbars from
template data structures. If you read the last 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 10:
GuiMakerWindowMenu
Implements Tk 8.0-style top-level window menus, useful for menus associated with standalone programs and pop ups.
GuiMakerFrameMenu
Implements alternative Frame/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 (a Menu
with Menu
cascades rather than a Frame
with Menubuttons
).
Like GuiMixin
, when we run
Example 11-2 as a
top-level program, we trigger the self-test logic at the bottom; Figure 11-2 shows the windows
we get. Three windows come up, representing each of the self-test
code’s TestApp
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.
Because of the superclass relationships coded, two of the
three windows get their help
callback handler from GuiMixin
;
TestAppWindowMenuBasic
gets
GuiMaker
’s instead. Notice that
the order in which these two classes are mixed can be important:
because both GuiMixin
and
Frame
define a quit
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
select GuiMixin
’s methods, it
should usually be listed before a superclass derived from real
widgets.
We’ll put GuiMaker
to more
practical use in instances such as the PyEdit example in Chapter 12. The next module shows
another way to use GuiMaker
’s
templates to build up a sophisticated interface.
Let’s look at a program that makes better use of the
two automation classes we just wrote. In the module in Example 11-3, the Hello
class inherits from both GuiMixin
and GuiMaker
. GuiMaker
provides the link to the Frame
widget, plus the menu/toolbar
construction logic. GuiMixin
provides extra common-behavior methods. Really, Hello
is another kind of extended Frame
widget because it is derived from
GuiMaker
. To get a menu and
toolbar for free, it simply follows the protocols defined by
GuiMaker
—it sets the menuBar
and toolBar
attributes in its start
method, and overrides makeWidgets
to put a label in the
middle.
Example 11-3. PP3EGuiToolsBigGuiig_gui.py
#!/usr/bin/python ######################################################### # GUI implementation - combines maker, mixin, and this ######################################################### import sys, os from Tkinter import * # widget classes from PP3E.Gui.Tools.guimixin import * # mix-in methods from PP3E.Gui.Tools.guimaker import * # frame, plus menu/toolbar builder from find_demo_dir import findDemoDir # Python demos search class Hello(GuiMixin, GuiMakerWindowMenu): # or GuiMakerFrameMenu def start(self): self.hellos = 0 self.master.title("GuiMaker Demo") self.master.iconname("GuiMaker") self.menuBar = [ # a tree: 3 pull downs ('File', 0, # (pull-down) [('New...', 0, self.notdone), # [menu items list] ('Open...', 0, self.fileOpen), ('Quit', 0, self.quit)] # label,underline,action ), ('Edit', 0, [('Cut', -1, self.notdone), # no underline|action ('Paste',-1, self.notdone), # lambda:0 works too 'separator', # add a separator ('Stuff', -1, [('Clone', -1, self.clone), # cascaded submenu ('More', -1, self.more)] ), ('Delete', -1, lambda:0), [5]] # disable 'delete' ), ('Play', 0, [('Hello', 0, self.greeting), ('Popup...', 0, self.dialog), ('Demos', 0, [('Hanoi', 0, lambda: self.spawn(findDemoDir( ) + 'guidohanoi.py', wait=0)), ('Pong', 0, lambda: self.spawn(findDemoDir( ) + 'mattpong-demo-1.py', wait=0)), ('Other...', -1, self.pickDemo)] )] )] self.toolBar = [ ('Quit', self.quit, {'side': RIGHT}), # add 3 buttons ('Hello', self.greeting, {'side': LEFT}), ('Popup', self.dialog, {'side': LEFT, 'expand':YES}) ] def makeWidgets(self): # override default middle = Label(self, text='Hello maker world!', width=40, height=10, cursor='pencil', bg='white', relief=SUNKEN) middle.pack(expand=YES, fill=BOTH) def greeting(self): self.hellos += 1 if self.hellos % 3: print "hi" else: self.infobox("Three", 'HELLO!') # on every third press def dialog(self): button = self.question('OOPS!', 'You typed "rm*" ... continue?', 'questhead', ('yes', 'no', 'help')) [lambda:0, self.quit, self.help][button]( ) def fileOpen(self): pick = self.selectOpenFile(file='big_gui.py') if pick: self.browser(pick) # browse my source file, or other def more(self): new = Toplevel( ) Label(new, text='A new non-modal window').pack( ) Button(new, text='Quit', command=self.quit).pack(side=LEFT) Button(new, text='More', command=self.more).pack(side=RIGHT) def pickDemo(self): pick = self.selectOpenFile(dir=findDemoDir( )+'guido') if pick: self.spawn(pick, wait=0) # spawn any Python program if _ _name_ _ == '_ _main_ _': Hello().mainloop( ) # make one, run one
This script lays out a fairly large menu and toolbar structure
that we’ll see in a moment. It also adds callback methods of its own
that print stdout
messages, pop
up text file browsers and new windows, and run other programs. Many
of the callbacks don’t do much more than run the notDone
method inherited from GuiMixin
, though; this code is intended
mostly as a GuiMaker
and GuiMixin
demo.
The big_gui
script is
almost a complete program, but not quite: it relies on a utility
module to search for canned demo programs that come packaged with
the Python full source distribution. (These demos are not part of
this book’s examples collection.) The Python source distribution
might be unpacked anywhere on the host machine.
Because of that, it’s impossible to know where the demo
directory is located (if it is present at all). But instead of
expecting beginners to change the source code of this script to
hardcode a path, the guessLocation
tool in the Launcher
module we met at the end of Chapter 6 is used to hunt for the
demo directory (see Example
11-4). Flip back if you’ve forgotten how this works (though
the beauty of code reuse is that it’s often OK to forget).
Example 11-4. PP3EGuiToolsBigGuifind_demo_dir.py
######################################################### # search for demos shipped in Python source distribution; # PATH and PP3EHOME won't help here, because these demos # are not part of the standard install or the book's tree ######################################################### import os, PP3E.Launcher demoDir = None myTryDir = '' #sourceDir = r'C:StuffEtcPython-ddj-cddistributions' #myTryDir = sourceDir + r'Python-1.5.2Demo kinter' def findDemoDir( ): global demoDir if not demoDir: # only searches on first call if os.path.exists(myTryDir): # use hardcoded dir, or search demoDir = myTryDir # save in global for next call else: print 'Searching for standard demos on your machine...' path = PP3E.Launcher.guessLocation('hanoi.py') if path: demoDir = os.sep.join(path.split(os.sep)[:-2]) print 'Using demo dir:', demoDir assert demoDir, 'Where is your demo directory?' return demoDir
When big_gui
is run as a
top-level program, it creates a window with four menu pull downs on
top, and a three-button toolbar on the bottom, shown in Figure 11-3 along with some of
the pop-up windows its callbacks create. The menus have separators,
disabled entries, and cascading submenus, all as defined by the
menuBar
template.
Figure 11-4 shows this script’s window again, after its Play pull down has been used to launch two independently running instances of the hanoi.py demo script that is shipped in the Python source distribution and was coded by Python creator, Guido van Rossum. This demo shows a simple animation of solutions to the “Towers of Hanoi” puzzle—a classic recursive problem popular on computer science quizzes (if you never heard of it, I’ll spare you the gory details here).
To find this demo, the script searches directory trees on your machine rooted at common places; it was found on mine only by a last-resort traversal of my entire C: hard drive:
C:...PP3EGuiToolsBigGui>python big_gui.py
Searching for standard demos on your machine...
Searching for hanoi.py in C:Program FilesPython
Searching for hanoi.py in C:PP3rdEdexamplesPP3EGuiToolsBigGui
Searching for hanoi.py in C:Program Files
Searching for hanoi.py in C:
Using demo dir: C:PP3rdEdcdromPython1.5.2SourceDistributionUnpackedPython-
1.5.2Demo kinter
C:PP3rdEdcdromPython1.5.2SourceDistributionUnpackedPython-1.5.2Demo kint
erguidohanoi.py
This search takes about 20 seconds on my 650 MHz Windows
laptop, but is done only the first time you select one of these
demos—after a successful search, the find_demo_dir
module caches away the
directory name in a global variable for immediate retrieval the next
time you start a demo. If you want to run demos from other
directories (e.g., one of the book demos in the
PP3E tree), select the Play menu’s Other option
to pop up a standard file selection dialog instead and navigate to
the desired program’s file.
Finally, I should note that GuiMaker
can be redesigned to use trees of
embedded class instances that know how to apply themselves to the
Tkinter widget tree being constructed, instead of branching on the
types of items in template data structures. In the interest of
space, though, we’ll banish that extension to the land of suggested
exercises in this edition.
18.221.163.13