GuiMaker: Automating Menus and Toolbars






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 Figure interprets data structure representations of menus and toolbars and builds all the widgets automatically.

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

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.

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

GuiMaker Self-Test

Like GuiMixin, when we run Figure as a top-level program, we trigger the self-test logic at the bottom; Figure 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.

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.

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 Figure, 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.

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 Figure). Flip back if you've forgotten how this works (though the beauty of code reuse is that it's often OK to forget).

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

big_gui with various pop ups


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

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.



 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows