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