Now that we’ve built a handful of similar demo launcher programs, let’s write a few top-level scripts to combine them. Because the demos were coded as both reusable classes and scripts, they can be deployed as attached frame components, run in their own top-level windows, and launched as standalone programs. All three options illustrate code reuse in action.
To illustrate hierarchical GUI composition on a grander scale than we’ve seen so far, Example 9-32 arranges to show all four of the dialog launcher bar scripts of this chapter in a single frame. It reuses Examples 9-9, 9-22, 9-25, and 9-30.
Example 9-32. PP3EGuiTourdemoAll-frm.py
##################################################### # 4 demo class components (subframes) on one window; # there are 5 Quitter buttons on this one window too; # GUIs can be reused as frames, windows, processes; ##################################################### from Tkinter import * from quitter import Quitter demoModules = ['demoDlg', 'demoCheck', 'demoRadio', 'demoScale'] parts = [] def addComponents(root): for demo in demoModules: module = _ _import_ _(demo) # import by name string part = module.Demo(root) # attach an instance part.config(bd=2, relief=GROOVE) part.pack(side=LEFT, fill=BOTH) parts.append(part) # change list in-place def dumpState( ): for part in parts: # run demo report if any print part._ _module_ _ + ':', if hasattr(part, 'report'): part.report( ) else: print 'none' root = Tk( ) # default toplevel window Label(root, text='Multiple Frame demo', bg='white').pack( ) Button(root, text='States', command=dumpState).pack(fill=X) Quitter(root).pack(expand=YES, fill=X) addComponents(root) mainloop( )
Because all four demo launcher bars are coded to attach
themselves to parent container widgets, this is easier than you
might think: simply pass the same parent widget (here, the root
window) to all four demo constructor
calls, and pack and configure the demo objects as desired. Figure 9-32 shows this
script’s graphical result—a single window embedding instances of all
four of the dialog demo launcher demos we saw earlier.
Naturally, this example is artificial, but it illustrates the power of composition when applied to building larger GUI displays. If you pretend that each of the four attached demo objects was something more useful, like a text editor, calculator, or clock, you’ll better appreciate the point of this example.
Besides demo object frames, this composite window also
contains no fewer than five instances of the Quitter button we wrote
earlier (any one of which can end the GUI), and a States button to
dump the current values of all the embedded demo objects at once (it
calls each object’s report
method, if it has one). Here is a sample of the sort of output that
shows up in the stdout
stream
after interacting with widgets on this display; States output is in
bold:
C:...PP3EGuiTour>python demoAll_frm.py
in onMove 0 in onMove 0demoDlg: none
demoCheck: 0 0 0 0 0
demoRadio:
demoScale: 0
you pressed Input result: 1.234demoDlg: none
demoCheck: 1 0 1 1 0
demoRadio: Input
demoScale: 0
you pressed Query result: yes in onMove 1 in onMove 2 You picked 2 C:/PP2ndEd/examples/PP3E/Gui/Tour/demoAll_frm.pydemoDlg: none
demoCheck: 1 0 1 1 0
demoRadio: Query
demoScale: 2
The only substantially tricky part of this script is its use
of Python’s built-in _ _import_ _
function to import a module by a name string. Look at the following
two lines from the script’s addComponents
function:
module = _ _import_ _(demo) # import module by name string part = module.Demo(root) # attach an instance of its Demo
This is equivalent to saying something like this:
import 'demoDlg' part = 'demoDlg'.Demo(root)
However, the preceding code is not legal Python syntax; the
module name in import statements must be a Python variable, not a
string. To be generic, addComponents
steps through a list of name
strings and relies on _ _import_
_
to import and return the module identified by each
string. It’s as though all of these statements were run:
import demoDlg, demoRadio, demoCheck, demoScale part = demoDlg.Demo(root) part = demoRadio.Demo(root) part = demoCheck.Demo(root) part = demoScale.Demo(root)
But because the script uses a list of name strings, it’s easier to change the set of demos embedded—simply change the list, not the lines of executable code. Moreover, such data-driven code tends to be more compact, less redundant, and easier to debug and maintain. Incidentally, modules can also be imported from name strings by dynamically constructing and running import statements, like this:
for demo in demoModules: exec 'from %s import Demo' % demo # make and run a from part = Demo(root) # or eval('Demo')(window)
The exec
statement compiles
and runs a Python statement string (here, a from
to load a module’s Demo
class); it works here as if the
statement string were pasted into the source code where the exec
statement appears. Because it
supports any sort of Python statement, this technique is more
general than the _ _import_ _
call, but it can also be slower, since it must parse code strings
before running them.[*] However, that slowness may not matter in a GUI; users
tend to be slower than parsers.
As we saw in Chapter 8, attaching nested frames like this is really just one way to reuse GUI code structured as classes. It’s just as easy to customize such interfaces by subclassing rather than embedding. Here, though, we’re more interested in deploying an existing widget package than changing it; the next two sections show two other ways to present such packages to users.
Once you have a set of component classes, any parent
will work—both frames, and brand-new, top-level windows. Example 9-33 attaches instances
of all four demo bar objects to their own Toplevel
windows, not to the same Frame
.
Example 9-33. PP3EGuiTourdemoAll-win.py
#################################################### # 4 demo classes in independent top-level windows; # not processes: when one is quit all others go away # because all windows run in the same process here #################################################### from Tkinter import * demoModules = ['demoDlg', 'demoRadio', 'demoCheck', 'demoScale'] demoObjects = [] for demo in demoModules: module = _ _import_ _(demo) # import by name string window = Toplevel( ) # make a new window demo = module.Demo(window) # parent is the new window demoObjects.append(demo) def allstates( ): for obj in demoObjects: if hasattr(obj, 'report'): print obj._ _module_ _, obj.report( ) Label(text='Multiple Toplevel window demo', bg='white').pack( ) Button(text='States', command=allstates).pack(fill=X) mainloop( )
We met the Toplevel
class
earlier; every instance generates a new window on your screen. The
net result is captured in Figure 9-33. Each demo runs in
an independent window of its own instead of being packed together in
a single display.
The main root window of this program appears in the lower left
of this screenshot; it provides a States button that runs the
report
method of each demo
object, producing this sort of stdout
text:
C:...PP3EGuiTour>python demoAll_win.py
in onMove 0 in onMove 0 in onMove 1 you pressed Open result: C:/PP2ndEd/examples/PP3E/Gui/Tour/demoAll_win.txtdemoRadio Open
demoCheck 1 1 0 0 0
demoScale 1
Finally, as we learned earlier in this chapter,
Toplevel
windows function
independently, but they are not really independent programs.
Quitting any of the windows created in Example 9-33 quits them all,
because all run in the same program process. That’s OK in some
applications, but not all.
To go truly independent, Example 9-34 spawns each of the
four demo launchers as independent programs, using the launchmodes
module we wrote at the end of
Chapter 5. This works only
because the demos were written as both importable classes and
runnable scripts. Launching them here makes all their names _ _main_ _
when run.
Example 9-34. PP3EGuiTourdemoAll-prg.py
####################################################### # 4 demo classes run as independent program processes; # if one window is quit now, the others will live on; # there is no simple way to run all report calls here, # and some launch schemes drop child program stdout; ####################################################### from Tkinter import * demoModules = ['demoDlg', 'demoRadio', 'demoCheck', 'demoScale'] from PP3E.launchmodes import PortableLauncher for demo in demoModules: # see Parallel System Tools PortableLauncher(demo, demo+'.py')( ) # start as top-level programs Label(text='Multiple program demo', bg='white').pack( ) mainloop( )
As Figure 9-34 shows, the display generated by this script is similar to the prior one; all four demos come up in windows of their own. This time, though, these are truly independent programs: if any one of the five windows here is quit, the others live on.
Spawning GUIs as programs is the ultimate in code
independence, but it makes the lines of communication between
components more complex. For instance, because the demos run as
programs here, there is no easy way to run all their report
methods from the launching
script’s window pictured in the middle of Figure 9-34. In fact, the
States button is gone this time, and we only get PortableLauncher
messages in stdout
as the demos start up:
C:...PP3EGuiTour>python demoAll_prg.py
demoDlg
demoRadio
demoCheck
demoScale
On some platforms, messages printed by the demo programs
(including their own State buttons) may show up in the original
console window where this script is launched; on Windows, the
os.spawnv
call used to start
programs in launchmodes
completely disconnects the child program’s stdout
stream from its parent.
Regardless, there is no way to call all demos’ report
methods at once; they are spawned
programs in distinct address spaces, not imported modules.
Of course, we could trigger report methods in the spawned programs with some of the Inter-Process Communication (IPC) mechanisms we met in Chapter 5. For instance:
The demos could be instrumented to catch a user signal,
and could run their report
in response.
They could also watch for request strings sent by the
launching program to show up in pipes or fifos; the demoAll
launching program would
essentially act as a client, and the demo GUIs as
servers.
Independent programs can also converse this way over sockets, a tool we’ll meet in depth in Part IV.
Given their event-driven nature, GUI-based programs may need
to be augmented with threads, timer-event callbacks, nonblocking
input calls, or some combination of such techniques to
periodically check for such incoming messages on pipes, fifos, or
sockets, and to avoid becoming stuck in wait states (e.g., see the
after
method call described
near the end of the next chapter). We’ll explore some of these
options in Chapter 11. But
since this is well beyond the scope of the current chapter’s
simple demo programs, I’ll leave such cross-program extensions up
to more parallel-minded readers for now.
A postscript: I coded all the demo launcher bars deployed by the last three examples to demonstrate all the different ways that their widgets can be used. They were not developed with general-purpose reusability in mind; in fact, they’re not really useful outside the context of introducing widgets in this book.
That was by design; most Tkinter widgets are easy to use
once you learn their interfaces, and Tkinter already provides lots
of configuration flexibility by itself. But if I had it in mind to
code checkbutton
and radiobutton
classes to be reused as
general library components, they would have to be structured
differently:
They would not display anything but radio buttons and check buttons. As is, the demos each embed State and Quit buttons for illustration, but there really should be just one Quit per top-level window.
They would allow for different button arrangements and would not pack (or grid) themselves at all. In a true general-purpose reuse scenario, it’s often better to leave a component’s geometry management up to its caller.
They would either have to export complex interfaces to support all possible Tkinter configuration options and modes, or make some limiting decisions that support one common use only. For instance, these buttons can either run callbacks at press time or provide their state later in the application.
Example 9-35 shows one way to code check button and radio button bars as library components. It encapsulates the notion of associating Tkinter variables and imposes a common usage mode on callers—state fetches rather than press callbacks—to keep the interface simple.
Example 9-35. PP3EGuiTouruttonbars.py
# check and radio button bar classes for apps that fetch state later; # pass a list of options, call state( ), variable details automated from Tkinter import * class Checkbar(Frame): def _ _init_ _(self, parent=None, picks=[], side=LEFT, anchor=W): Frame._ _init_ _(self, parent) self.vars = [] for pick in picks: var = IntVar( ) chk = Checkbutton(self, text=pick, variable=var) chk.pack(side=side, anchor=anchor, expand=YES) self.vars.append(var) def state(self): return [var.get( ) for var in self.vars] # or map(lambda, self.vars) class Radiobar(Frame): def _ _init_ _(self, parent=None, picks=[], side=LEFT, anchor=W): Frame._ _init_ _(self, parent) self.var = StringVar( ) for pick in picks: rad = Radiobutton(self, text=pick, value=pick, variable=self.var) rad.pack(side=side, anchor=anchor, expand=YES) def state(self): return self.var.get( ) if _ _name_ _ == '_ _main_ _': root = Tk( ) lng = Checkbar(root, ['Python', 'C#', 'Java', 'C++']) gui = Radiobar(root, ['win', 'x11', 'mac'], side=TOP, anchor=NW) tgl = Checkbar(root, ['All']) gui.pack(side=LEFT, fill=Y) lng.pack(side=TOP, fill=X) tgl.pack(side=LEFT) lng.config(relief=GROOVE, bd=2) gui.config(relief=RIDGE, bd=2) from quitter import Quitter def allstates(): print gui.state(), lng.state(), tgl.state( ) Quitter(root).pack(side=RIGHT) Button(root, text='Peek', command=allstates).pack(side=RIGHT) root.mainloop( )
To reuse these classes in your scripts, import and call them
with a list of the options that you want to appear in a bar of
check buttons or radio buttons. This module’s self-test code at
the bottom of the file gives further usage details. It generates
Figure 9-35—a
top-level window that embeds two Checkbars
, one Radiobar
, a Quitter
button to exit, and a Peek
button to show bar states—when this file is run as a program
instead of being imported.
Here’s the stdout
text
you get after pressing Peek—the results of these classes’ state
methods:
x11 [1, 0, 1, 1] [0] win [1, 0, 0, 1] [1]
The two classes in this module demonstrate how easy it is to
wrap Tkinter interfaces to make them easier to use; they
completely abstract away many of the tricky parts of radio button
and check button bars. For instance, you can forget about linked
variable details completely if you use such higher-level classes
instead; simply make objects with option lists and call their
state
methods later. If you
follow this path to its conclusion, you might just wind up with a
higher-level widget library on the order of the Pmw package
mentioned in Chapter
8.
On the other hand, these classes are still not universally applicable; if you need to run actions when these buttons are pressed, for instance, you’ll need to use other high-level interfaces. Luckily, Python/Tkinter already provides plenty. Later in this book, we’ll again use the widget combination and reuse techniques introduced in this section to construct larger GUIs. For now, this first chapter in the widget tour is about to make one last stop—the photo shop.
[*] As we’ll see later, exec
can also be dangerous if it is
running code strings fetched from users or network connections.
That’s not an issue for the hardcoded strings in this
example.
3.147.27.131