Section 11.3. GuiMaker: Automating Menus and Toolbars


11.3. GuiMaker: Automating Menus and Toolbars

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 doneusing 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. PP3E\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 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 straightforwardthe 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:


Menu bar templates

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.


Toolbar templates

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).

11.3.1. Subclass Protocols

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:


Template attributes

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.


Initialization

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 performedGuiMixin's _ _init_ _ constructor must be run, not overridden.


Adding widgets

The makeWidgets method can be redefined to construct the middle part of the windowthe 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.


Packing protocol

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.


Gridding protocol

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.

11.3.2. GuiMaker Classes

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 methodonly top-level menu processing differs between the two styles (a Menu with Menu cascades rather than a Frame with Menubuttons).

11.3.3. GuiMaker Self-Test

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.

Figure 11-2. GuiMaker self-test at work


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.

11.3.4. BigGui: A Client Demo Program

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 GuiMakerit sets the menuBar and toolBar attributes in its start method, and overrides makeWidgets to put a label in the middle.

Example 11-3. PP3E\Gui\Tools\BigGui\big_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( ) + '\guido\hanoi.py', wait=0)),                    ('Pong',  0,                        lambda:                        self.spawn(findDemoDir( ) + '\matt\pong-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. PP3E\Gui\Tools\BigGui\find_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:\Stuff\Etc\Python-ddj-cd\distributions' #myTryDir  = sourceDir + r'\Python-1.5.2\Demo\tkinter' 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-3. big_gui with various pop ups


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" puzzlea classic recursive problem popular on computer science quizzes (if you never heard of it, I'll spare you the gory details here).

Figure 11-4. big_gui with spawned hanoi demos on the move


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:\...\PP3E\Gui\Tools\BigGui>python big_gui.py Searching for standard demos on your machine... Searching for hanoi.py in C:\Program Files\Python Searching for hanoi.py in C:\PP3rdEd\examples\PP3E\Gui\Tools\BigGui Searching for hanoi.py in C:\Program Files Searching for hanoi.py in C:\ Using demo dir: C:\PP3rdEd\cdrom\Python1.5.2\SourceDistribution\Unpacked\Python- 1.5.2\Demo\tkinter C:\PP3rdEd\cdrom\Python1.5.2\SourceDistribution\Unpacked\Python-1.5.2\Demo\tkint er\guido\hanoi.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 demosafter 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.




Programming Python
Programming Python
ISBN: 0596009259
EAN: 2147483647
Year: 2004
Pages: 270
Authors: Mark Lutz

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net