Section 11.4. ShellGui: GUIs for Command-Line Tools


11.4. ShellGui: GUIs for Command-Line Tools

To better show how things like the GuiMixin class can be of practical use, we need a more realistic application. Here's one: in Chapter 6, we saw simple scripts for packing and unpacking text files. The packapp.py script we met there, you'll recall, concatenates multiple text files into a single file, and unpackapp.py extracts the original files from the combined file.

We ran these scripts in that chapter with manually typed command lines that weren't the most complex ever devised, but were complicated enough to be easily forgotten. Instead of requiring users of such tools to type cryptic commands at a shell, why not also provide an easy-to-use Tkinter GUI interface for running such programs? While we're at it, why not generalize the whole notion of running command-line tools from a GUI, to make it easy to support future tools too?

11.4.1. A Generic Shell-Tools Display

Examples 11-5 through 11-8 comprise one concrete implementation of these artificially rhetorical musings. Because I wanted this to be a general-purpose tool that could run any command-line program, its design is factored into modules that become more application-specific as we go lower in the software hierarchy. At the top, things are about as generic as they can be, as shown in Example 11-5.

Example 11-5. PP3E\Gui\ShellGui\shellgui.py.py

 #!/usr/local/bin/python ################################################################################ # tools launcher; uses guimaker templates, guimixin std quit dialog; # I am just a class library: run mytools script to display the GUI; ################################################################################ from Tkinter import *                               # get widgets from PP3E.Gui.Tools.guimixin import GuiMixin        # get quit, not done from PP3E.Gui.Tools.guimaker import *               # menu/toolbar builder class ShellGui(GuiMixin, GuiMakerWindowMenu):       # a frame + maker + mixins     def start(self):                                # use GuiMaker if component         self.setMenuBar( )         self.setToolBar( )         self.master.title("Shell Tools Listbox")         self.master.iconname("Shell Tools")     def handleList(self, event):                    # on listbox double-click         label = self.listbox.get(ACTIVE)            # fetch selection text         self.runCommand(label)                      # and call action here     def makeWidgets(self):                          # add listbox in middle         sbar = Scrollbar(self)                      # cross link sbar, list         list = Listbox(self, bg='white')            # or use Tour.ScrolledList         sbar.config(command=list.yview)         list.config(yscrollcommand=sbar.set)         sbar.pack(side=RIGHT, fill=Y)                     # pack 1st=clip last         list.pack(side=LEFT, expand=YES, fill=BOTH)       # list clipped first         for (label, action) in self.fetchCommands( ):         # add to listbox             list.insert(END, label)                       # and menu/toolbars         list.bind('<Double-1>', self.handleList)          # set event handler         self.listbox = list     def forToolBar(self, label):                          # put on toolbar?         return 1                                          # default = all     def setToolBar(self):         self.toolBar = []         for (label, action) in self.fetchCommands( ):             if self.forToolBar(label):                 self.toolBar.append((label, action, {'side': LEFT}))         self.toolBar.append(('Quit', self.quit, {'side': RIGHT}))     def setMenuBar(self):         toolEntries  = []         self.menuBar = [             ('File',  0, [('Quit', -1, self.quit)]),    # pull-down name             ('Tools', 0, toolEntries)                   # menu items list             ]                                           # label,underline,action         for (label, action) in self.fetchCommands( ):             toolEntries.append((label, -1, action))     # add app items to menu ################################################################################ # delegate to template type-specific subclasses # which delegate to app tool-set-specific subclasses ################################################################################ class ListMenuGui(ShellGui):     def fetchCommands(self):             # subclass: set 'myMenu'         return self.myMenu               # list of (label, callback)     def runCommand(self, cmd):         for (label, action) in self.myMenu:             if label == cmd: action( ) class DictMenuGui(ShellGui):     def fetchCommands(self):   return self.myMenu.items( )     def runCommand(self, cmd): self.myMenu[cmd]( ) 

The ShellGui class in this module knows how to use the GuiMaker and GuiMixin interfaces to construct a selection window that displays tool names in menus, a scrolled list, and a toolbar. It also provides a forToolBar method that you can override and that allows subclasses to specify which tools should and should not be added to the window's toolbar (the toolbar can become crowded in a hurry). However, it is deliberately ignorant about both the names of tools that should be displayed in those places and about the actions to be run when tool names are selected.

Instead, ShellGui relies on the ListMenuGui and DictMenuGui subclasses in this file to provide a list of tool names from a fetchCommands method and dispatch actions by name in a runCommand method. These two subclasses really just serve to interface to application-specific tool sets laid out as lists or dictionaries, though; they are still naïve about what tool names really go up on the GUI. That's by design toobecause the tool sets displayed are defined by lower subclasses, we can use ShellGui to display a variety of different tool sets.

11.4.2. Application-Specific Tool Set Classes

To get to the actual tool sets, we need to go one level down. The module in Example 11-6 defines subclasses of the two type-specific ShellGui classes, to provide sets of available tools in both list and dictionary formats (you would normally need only one, but this module is meant for illustration). This is also the module that is actually run to kick off the GUIthe shellgui module is a class library only.

Example 11-6. PP3E\Gui\ShellGui\mytools.py

 #!/usr/local/bin/python from shellgui import *                 # type-specific shell interfaces from packdlg  import runPackDialog     # dialogs for data entry from unpkdlg  import runUnpackDialog   # they both run app classes class TextPak1(ListMenuGui):     def _ _init_ _(self):         self.myMenu = [('Pack',    runPackDialog),                        ('Unpack',  runUnpackDialog),    # simple functions                        ('Mtool',   self.notdone)]       # method from guimixin         ListMenuGui._ _init_ _(self)     def forToolBar(self, label):         return label in ['Pack', 'Unpack'] class TextPak2(DictMenuGui):     def _ _init_ _(self):         self.myMenu = {'Pack':    runPackDialog,        # or use input here...                        'Unpack':  runUnpackDialog,      # instead of in dialogs                        'Mtool':   self.notdone}         DictMenuGui._ _init_ _(self) if _ _name_ _ == '_ _main_ _':                           # self-test code...     from sys import argv                               # 'menugui.py list|^'     if len(argv) > 1 and argv[1] == 'list':         print 'list test'         TextPak1().mainloop( )     else:         print 'dict test'         TextPak2().mainloop( ) 

The classes in this module are specific to a particular tool set; to display a different set of tool names, simply code and run a new subclass. By separating out application logic into distinct subclasses and modules like this, software can become widely reusable.

Figure 11-5 shows the main ShellGui window created when the mytools script is run with its dictionary-based menu layout class on Windows, along with menu tear-offs so that you can see what they contain. This window's menu and toolbar are built by GuiMaker, and its Quit and Help buttons and menu selections trigger quit and help methods inherited from GuiMixin tHRough the ShellGui module's superclasses. Are you starting to see why this book preaches code reuse so often?

Figure 11-5. mytools items in a ShellGui window


11.4.3. Adding GUI Frontends to Command Lines

The callback actions named within the prior module's classes, though, should normally do something GUI-oriented. Because the original file packing and unpacking scripts live in the world of text-based streams, we need to code wrappers around them that accept input parameters from more GUI-minded users.

The module in Example 11-7 uses the custom modal dialog techniques we studied in Chapter 9 to pop up an input display to collect pack script parameters. Its runPackDialog function is the actual callback handler invoked when tool names are selected in the main ShellGui window.

Example 11-7. PP3E\Gui\ShellGui\packdlg.py

 # added file select dialogs, empties test; could use grids from glob import glob                                   # filename expansion from Tkinter import *                                   # GUI widget stuff from tkFileDialog import *                              # file selector dialog from PP3E.System.App.Clients.packapp import PackApp     # use pack class def runPackDialog( ):     s1, s2 = StringVar(), StringVar( )          # run class like a function     PackDialog(s1, s2)                          # pop-up dialog: sets s1/s2     output, patterns = s1.get(), s2.get( )      # whether 'ok' or wm-destroy     if output != "" and patterns != "":         patterns = patterns.split( )         filenames = []         for sublist in map(glob, patterns):    # do expansion manually             filenames = filenames + sublist    # Unix does auto on command line         print 'PackApp:', output, filenames         app = PackApp(ofile=output)            # run with redirected output         app.args = filenames                   # reset cmdline args list         app.main( )                                # should show msgs in GUI too class PackDialog(Toplevel):     def _ _init_ _(self, target1, target2):         Toplevel._ _init_ _(self)                  # a new top-level window         self.title('Enter Pack Parameters')         # 2 frames plus a button         f1 = Frame(self)         l1 = Label(f1,  text='Output file?', relief=RIDGE, width=15)         e1 = Entry(f1,  relief=SUNKEN)         b1 = Button(f1, text='browse...')         f1.pack(fill=X)         l1.pack(side=LEFT)         e1.pack(side=LEFT, expand=YES, fill=X)         b1.pack(side=RIGHT)         b1.config(command= (lambda: target1.set(askopenfilename( ))) )         f2 = Frame(self)         l2 = Label(f2,  text='Files to pack?', relief=RIDGE, width=15)         e2 = Entry(f2,  relief=SUNKEN)         b2 = Button(f2, text='browse...')         f2.pack(fill=X)         l2.pack(side=LEFT)         e2.pack(side=LEFT, expand=YES, fill=X)         b2.pack(side=RIGHT)         b2.config(command=                  (lambda: target2.set(target2.get() +' '+ askopenfilename( ))) )         Button(self, text='OK', command=self.destroy).pack( )         e1.config(textvariable=target1)         e2.config(textvariable=target2)         self.grab_set( )         # make myself modal:         self.focus_set( )        # mouse grab, keyboard focus, wait...         self.wait_window( )      # till destroy; else returns to caller now if _ _name_ _ == '_ _main_ _':     root = Tk( )     Button(root, text='pop', command=runPackDialog).pack(fill=X)     Button(root, text='bye', command=root.quit).pack(fill=X)     root.mainloop( ) 

When run, this script makes the input form shown in Figure 11-6. Users may either type input and output filenames into the entry fields or press the "browse" buttons to pop up standard file selection dialogs. They can also enter filename patternsthe manual glob.glob call in this script expands filename patterns to match names and filters out nonexistent input filenames. The Unix command line does this pattern expansion automatically when running PackApp from a shell, but Windows does not (see Chapter 4 for more details).

Figure 11-6. The packdlg input form


When the form is filled in and submitted with its OK button, parameters are finally passed to an instance of the PackApp class we wrote in Chapter 6 to do file concatenations. The GUI interface to the unpacking script is simpler because there is only one input fieldthe name of the packed file to scan. The script in Example 11-8 generates the input form window shown in Figure 11-7.

Figure 11-7. The unpkdlg input form


Example 11-8. PP3E\Gui\ShellGui\unpkdlg.py

 # added file select dialog, handles cancel better from Tkinter import *                                     # widget classes from tkFileDialog import *                                # file open dialog from PP3E.System.App.Clients.unpackapp import UnpackApp   # use unpack class def runUnpackDialog( ):     input = UnpackDialog( ).input                  # get input from GUI     if input != '':                                 # do non-GUI file stuff         print 'UnpackApp:', input         app = UnpackApp(ifile=input)               # run with input from file         app.main( )                                # execute app class class UnpackDialog(Toplevel):     def _ _init_ _(self):                           # a function would work too         Toplevel._ _init_ _(self)                   # resizable root box         self.input = ''                               # a label and an entry         self.title('Enter Unpack Parameters')         Label(self, text='input file?', relief=RIDGE, width=11).pack(side=LEFT)         e = Entry(self, relief=SUNKEN)         b = Button(self, text='browse...')         e.bind('<Key-Return>', self.gotit)         b.config(command=(lambda: e.insert(0, askopenfilename( ))))         b.pack(side=RIGHT)         e.pack(side=LEFT, expand=YES, fill=X)         self.entry = e         self.grab_set( )                   # make myself modal         self.focus_set( )         self.wait_window( )                # till I'm destroyed on return->gotit     def gotit(self, event):                # on return key: event.widget==Entry         self.input = self.entry.get( )     # fetch text, save in self         self.destroy( )                    # kill window, but instance lives on if _ _name_ _ == "_ _main_ _":     Button(None, text='pop', command=runUnpackDialog).pack( )     mainloop( ) 

The "browse" button in Figure 11-7 pops up a file selection dialog just as the packdlg form did. Instead of an OK button, this dialog binds the enter key-press event to kill the window and end the modal wait state pause; on submission, the name of the file is passed to an instance of the UnpackApp class shown in Chapter 6 to perform the actual file scan process.

All of this works as advertisedby making command-line tools available in graphical form like this, they become much more attractive to users accustomed to the GUI way of life. Still, two aspects of this design seem prime for improvement.

First, both of the input dialogs use custom code to render a unique appearance, but we could probably simplify them substantially by importing a common form-builder module instead. We met generalized form builder code in Chapters 9 and 10, and we'll meet more later; see the form.py module in Chapter 13 for pointers on genericizing form construction too.

Second, at the point where the user submits input data in either form dialog, we've lost the GUI trailthe GUI is blocked, and messages are routed back to the console. The GUI is technically blocked and will not update itself while the pack and unpack utilities run; although these operations are fast enough for my files as to be negligible, we would probably want to spawn these calls off in threads for very large files to keep the main GUI thread active (more on threads later in this chapter). The console issue is more apparent: PackApp and UnpackApp messages still show up in the stdout console window, not in the GUI:

 C:\...\PP3E\Gui\ShellGui\test>python ..\mytools.py dict test PackApp: packed.all ['spam.txt', 'eggs.txt', 'ham.txt'] packing: spam.txt packing: eggs.txt packing: ham.txt UnpackApp: packed.all creating: spam.txt creating: eggs.txt creating: ham.txt 

This may be less than ideal for a GUI's users; they may not expect (or even be able to find) the command-line console. We can do better here, by redirecting stdout to an object that throws text up in a GUI window as it is received. You'll have to read the next section to see how.




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