Our next GUI programming
technique is all about changing a GUI while it is
running—
the ultimate in customization. The
Pythonimp.reload
function lets you
dynamically change and reload a program’s modules without stopping the
program. For instance, you can bring up a text editor window to change the
source code of selected parts of a system while it is running and see
those changes show up immediately after reloading the changed
module.
This is a powerful feature, especially for developing programs that
take a long time to restart. Programs that connect to databases or network
servers, initialize large objects, implement long-running services, or
travel through a long series of steps to retrigger a callback are prime
candidates forreload
. It can shave
substantial time from the development cycle and make systems more
flexible.
The catch for GUIs, though, is that because callback handlers are
registered as
object references
rather than module
and object names, reloads of callback handler functions are ineffective
after the callback has been registered. The Pythonimp.reload
operation works by changing a module
object’s contents in place. Because tkinter stores a pointer to the
registered handler object directly, though, it is oblivious to any reloads
of the module that the handler came from. That is, tkinter will still
reference a module’s old objects even after the module is reloaded and
changed.
This is a subtle thing, but you really only need to remember that
you must do something special to reload callback handler functions
dynamically. Not only do you need to explicitly request reloading of the
modules that you change, but you must also generally provide an
indirection layer that routes callbacks from registered objects to modules
so that reloads have impact.
For example, the script in
Example 10-14
goes the extra mile to
indirectly dispatch callbacks to functions in an explicitly reloaded
module. The callback handlers registered with tkinter are method objects
that do nothing but reload and dispatch again. Because the true callback
handler functions are fetched through a module object, reloading that
module makes the latest versions of the functions accessible.
Example 10-14. PP4E\Gui\Tools\rad.py
# reload callback handlers dynamically
from tkinter import *
import radactions # get initial callback handlers
from imp import reload # moved to a module in Python 3.X
class Hello(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
self.make_widgets()
def make_widgets(self):
Button(self, text='message1', command=self.message1).pack(side=LEFT)
Button(self, text='message2', command=self.message2).pack(side=RIGHT)
def message1(self):
reload(radactions) # need to reload actions module before calling
radactions.message1() # now new version triggered by pressing button
def message2(self):
reload(radactions) # changes to radactions.py picked up by reload
radactions.message2(self) # call the most recent version; pass self
def method1(self):
print('exposed method...') # called from radactions function
Hello().mainloop()
When run, this script makes a two-button window that triggers themessage1
andmessage2
methods.
Example 10-15
contains the actual
callback handler code. Its functions receive aself
argument that gives access back to theHello
class object, as though these
were real methods. You can change this file any number of times while therad
script’s GUI is active; each time
you do so, you’ll change the behavior of the GUI when a button press
occurs.
Example 10-15. PP4E\Gui\Tools\radactions.py
# callback handlers: reloaded each time triggered
def message1(): # change me
print('spamSpamSPAM') # or could build a dialog...
def message2(self):
print('Ni! Ni!') # change me
self.method1() # access the 'Hello' instance...
Try runningrad
and editing the
messages printed byradactions
in
another window; you should see your new messages printed in thestdout
console window each time the GUI’s
buttons are pressed. This example is deliberately simple to illustrate the
concept, but the actions reloaded like this in practice might build pop-up
dialogs, new top-level windows, and so on. Reloading the code that creates
such windows would also let us dynamically change their
appearances.
There are other ways to change a GUI while it’s running. For
instance, we saw in
Chapter 9
that
appearances can be altered at any time by calling the widgetconfig
method, and widgets can be added and
deleted from a display dynamically with methods such aspack_forget
andpack
(and theirgrid
manager relatives). Furthermore, passing a
newcommand=
action
option
setting to a widget’sconfig
method
might reset a callback handler to a new action object on the fly; with
enough support code, this may be a viable alternative to the indirection
scheme used earlier to make reloads more effective in GUIs.
Of course, not all GUIs need to be so dynamic. Imagine a game which
allows character modification, though—dynamic reloads in such a system can
greatly enhance their utility. (I’ll leave the task of extending this
example with a massively multiplayer online role-playing game server as
suggested
exercise.)
Top-level window
interfaces were introduced in
Chapter 8
. This section picks up where that
introduction left off and wraps up those interfaces in classes that
automate much of the work of building top-level windows—setting titles,
finding and displaying window icons, issuing proper close actions based on
a window’s role, intercepting window manager close button clicks, and so
on.
Example 10-16
provides
wrapper classes for the most common window types—a main application
window, a transient pop-up window, and an embedded GUI component window.
These window types vary slightly in terms of their close operations, but
most inherit common functionality related to window borders: icons,
titles, and close buttons. By creating, mixing in, or subclassing the
class for the type of window you wish to make, you’ll get all its setup
logic for free.
Example 10-16. PP4E\Gui\Tools\windows.py
"""
###############################################################################
Classes that encapsulate top-level interfaces.
Allows same GUI to be main, pop-up, or attached; content classes may inherit
from these directly, or be mixed together with them per usage mode; may also
be called directly without a subclass; designed to be mixed in after (further
to the right than) app-specific classes: else, subclass gets methods here
(destroy, okayToQuit), instead of from app-specific classes--can't redefine.
###############################################################################
"""
import os, glob
from tkinter import Tk, Toplevel, Frame, YES, BOTH, RIDGE
from tkinter.messagebox import showinfo, askyesno
class _window:
"""
mixin shared by main and pop-up windows
"""
foundicon = None # shared by all inst
iconpatt = '*.ico' # may be reset
iconmine = 'py.ico'
def configBorders(self, app, kind, iconfile):
if not iconfile: # no icon passed?
iconfile = self.findIcon() # try curr,tool dirs
title = app
if kind: title += ' - ' + kind
self.title(title) # on window border
self.iconname(app) # when minimized
if iconfile:
try:
self.iconbitmap(iconfile) # window icon image
except: # bad py or platform
pass
self.protocol('WM_DELETE_WINDOW', self.quit) # don't close silent
def findIcon(self):
if _window.foundicon: # already found one?
return _window.foundicon
iconfile = None # try curr dir first
iconshere = glob.glob(self.iconpatt) # assume just one
if iconshere: # del icon for red Tk
iconfile = iconshere[0]
else: # try tools dir icon
mymod = __import__(__name__) # import self for dir
path = __name__.split('.') # poss a package path
for mod in path[1:]: # follow path to end
mymod = getattr(mymod, mod) # only have leftmost
mydir = os.path.dirname(mymod.__file__)
myicon = os.path.join(mydir, self.iconmine) # use myicon, not tk
if os.path.exists(myicon): iconfile = myicon
_window.foundicon = iconfile # don't search again
return iconfile
class MainWindow(Tk, _window):
"""
when run in main top-level window
"""
def __init__(self, app, kind='', iconfile=None):
Tk.__init__(self)
self.__app = app
self.configBorders(app, kind, iconfile)
def quit(self):
if self.okayToQuit(): # threads running?
if askyesno(self.__app, 'Verify Quit Program?'):
self.destroy() # quit whole app
else:
showinfo(self.__app, 'Quit not allowed') # or in okayToQuit?
def destroy(self): # exit app silently
Tk.quit(self) # redef if exit ops
def okayToQuit(self): # redef me if used
return True # e.g., thread busy
class PopupWindow(Toplevel, _window):
"""
when run in secondary pop-up window
"""
def __init__(self, app, kind='', iconfile=None):
Toplevel.__init__(self)
self.__app = app
self.configBorders(app, kind, iconfile)
def quit(self): # redef me to change
if askyesno(self.__app, 'Verify Quit Window?'): # or call destroy
self.destroy() # quit this window
def destroy(self): # close win silently
Toplevel.destroy(self) # redef for close ops
class QuietPopupWindow(PopupWindow):
def quit(self):
self.destroy() # don't verify close
class ComponentWindow(Frame):
"""
when attached to another display
"""
def __init__(self, parent): # if not a frame
Frame.__init__(self, parent) # provide container
self.pack(expand=YES, fill=BOTH)
self.config(relief=RIDGE, border=2) # reconfig to change
def quit(self):
showinfo('Quit', 'Not supported in attachment mode')
# destroy from Frame: erase frame silent # redef for close ops
So why not just set an application’s icon and title by calling
protocol methods directly? For one thing, those are the sorts of details
that are easy to forget (you will probably wind up cutting and pasting
code much of the time). For another, these classes add higher-level
functionality that we might otherwise have to code redundantly. Among
other things, the classes arrange for automatic quit verification dialog
pop ups and icon file searching. For instance, the window classes always
search the current working
directory
and the directory containing this module for a window icon file, once per
process
.
By using classes that
encapsulate
—that is,
hide—such details, we inherit powerful tools without having to think about
their implementation again in the future. Moreover, by using such classes,
we’ll give our applications a standard look-and-feel and behavior. And if
we ever need to change that appearance or behavior, we have to change code
in only one place, not in every window we implement.
To test this utility module,
Example 10-17
exercises its classes
in a variety of modes—as mix-in classes, as superclasses, and as calls
from nonclass code.
Example 10-17. PP4E\Gui\Tools\windows-test.py
# must import windows to test, else __name__ is __main__ in findIcon
from tkinter import Button, mainloop
from windows import MainWindow, PopupWindow, ComponentWindow
def _selftest():
# mixin usage
class content:
"same code used as a Tk, Toplevel, and Frame"
def __init__(self):
Button(self, text='Larch', command=self.quit).pack()
Button(self, text='Sing ', command=self.destroy).pack()
class contentmix(MainWindow, content):
def __init__(self):
MainWindow.__init__(self, 'mixin', 'Main')
content.__init__(self)
contentmix()
class contentmix(PopupWindow, content):
def __init__(self):
PopupWindow.__init__(self, 'mixin', 'Popup')
content.__init__(self)
prev = contentmix()
class contentmix(ComponentWindow, content):
def __init__(self): # nested frame
ComponentWindow.__init__(self, prev) # on prior window
content.__init__(self) # Sing erases frame
contentmix()
# subclass usage
class contentsub(PopupWindow):
def __init__(self):
PopupWindow.__init__(self, 'popup', 'subclass')
Button(self, text='Pine', command=self.quit).pack()
Button(self, text='Sing', command=self.destroy).pack()
contentsub()
# non-class usage
win = PopupWindow('popup', 'attachment')
Button(win, text='Redwood', command=win.quit).pack()
Button(win, text='Sing ', command=win.destroy).pack()
mainloop()
if __name__ == '__main__':
_selftest()
When run, the test generates the window in
Figure 10-10
. All generated windows get a blue “PY”
icon automatically, and intercept and verify the window manager’s upper
right corner “X” close button, thanks to the search and configuration
logic they inherit from the window module’s classes. Some of the buttons
on the test windows close just the enclosing window, some close the entire
application, some erase an attached window, and others pop up a quit
verification dialog. Run this on your own to see what the examples’
buttons do, so you can correlate with the test code; quit actions are
tailored to make sense for the type of window being run.
Figure 10-10. windows-test display
We’ll use these window protocol wrappers in the next chapter’s
PyClock example, and then again later in
Chapter 14
where they’ll come in handy to reduce
the complexity of the PyMailGUI program. Part of the benefit of doing OOP
in Python now is that we can forget the details
later.