Loading this file’s data into our GUI makes the dimensions of the
grid change accordingly—the class simply reruns its widget construction
logic after erasing all the old entry widgets with thegrid_forget
method. Thegrid_forget
method unmaps gridded widgets and
so effectively erases them from the display. Also watch for thepack_forget
widget and windowwithdraw
methods used in theafter
event “alarm” examples of the next
section for other ways to erase and redraw GUI components.
Once the GUI is erased and redrawn for the new data,
Figure 9-39
captures the scene
after the file Load and a new Sum have been requested by the user in the
GUI.
Figure 9-39. Data file loaded, displayed, and summed
The
grid5-data2.txt
datafile has the same
dimensions but contains expressions in two of its columns, not just
simple numbers. Because this script converts input field values with the
Pythoneval
built-in function, any
Python syntax will work in this table’s fields, as long as it can be
parsed and evaluated within the scope of theonSum
method:
C:\...\PP4E\Gui\Tour\Grid>type grid5-data2.txt
1 2 3 2*2 5 6
1 3-1 3 2<<1 5 6
1 5%3 3 pow(2,2) 5 6
1 2 3 2**2 5 6
1 2 3 [4,3][0] 5 6
1 {'a':2}['a'] 3 len('abcd') 5 6
1 abs(-2) 3 eval('2+2') 5 6
Summing these fields runs the Python code they contain, as seen in
Figure 9-40
. This can be
a powerful feature; imagine a full-blown spreadsheet grid, for
instance—field values could be Python code “snippets” that compute
values on the fly, call functions in modules, and even download current
stock quotes over the Internet with tools we’ll meet in the next part of
this book.
Figure 9-40. Python expressions in the data and table
It’s also a potentially
dangerous
tool—a
field might just contain an expression that erases your hard
drive!
[
36
]
If you’re not sure what expressions may do, either don’t
useeval
(convert with more limited
built-in functions likeint
andfloat
instead) or make sure your
Python is running in a process with restricted access permissions for
system components you don’t want to expose to the code you run.
Of course, this still is nowhere near a true spreadsheet program.
There are fixed column sums and file loads, for instance, but individual
cells cannot contain formulas based upon other cells. In the interest of
space, further mutations toward that goal are left as exercises.
I should also point out that there is more to gridding than we
have time to present fully here. For instance, by creating subframes
that have grids of their own, we can build up more sophisticated layouts
as component hierarchies in much the same way as nested frames arranged
with the packer. For now, let’s move on to one last widget survey
topic.
[
36
]
I debated showing this, but since understanding a danger is a
big part of avoiding it—if the Python process had permission to
delete files, passing the code string__import__('os').system('rm –rf *')
toeval
on Unix would delete all
files at and below the current directory by running a shell command
(and'rmdir /S /Q .'
would have a
similar effect on Windows). Don’t do this! To see how this works in
a less devious and potentially useful way, type__import__('math').pi
into one of the GUI
table’s cells—on Sum, the cell evaluates to pi (3.14159). Passing"__import__('os').system('dir')"
toeval
interactively proves the
point safely as well. All of this also applies to theexec
built-in—eval
runs expression strings andexec
statements, but expressions are
statements (though not vice versa). A typical user of most GUIs is
unlikely to type this kind of code accidentally, of course,
especially if that user is always you, but be careful out
there!
The last stop on our widget tour is perhaps the most unique. tkinter
also comes with a handful of tools that have to do with the event-driven
programming model, not graphics displayed on a computer screen.
Some GUI applications need to perform background activities
periodically. For example, to “blink” a widget’s appearance, we’d like to
register a callback handler to be invoked at regular time intervals.
Similarly, it’s not a good idea to let a long-running file operation block
other activity in a GUI; if the event loop could be forced to update
periodically, the GUI could remain responsive. tkinter comes with tools
for both scheduling such delayed actions and forcing screen
updates:
widget.after(
milliseconds,
function, *args
)
This tool
schedules the function to be called once by the GUI’s
event processing system after a number of milliseconds. This form of
the call does not pause the program—the callback function is
scheduled to be run later from the normal tkinter event loop, but
the calling program continues normally, and the GUI remains active
while the function call is pending. As also discussed in
Chapter 5
, unlike thethreading
module’sTimer
object,widget.after
events are dispatched in the
main GUI thread and so can freely update the GUI.
Thefunction
event handler argument
can be any callable Python object: a function, bound method, lambda
and so on. Themilliseconds
timer
duration argument is an integer which can be used to specify both
fractions and multiples of a second; its value divided by 1,000
gives equivalent seconds. Anyargs
arguments are passed by position tofunction
when it is later
called.
In practice, alambda
can
be used in place of individually-listed arguments to make the
association of arguments to function explicit, but that is not
required. When the function is a method, object state information
(attributes) might also provide its data instead of listed
arguments. Theafter
method
returns an ID that can be passed toafter_cancel
to cancel the callback. Since
this method is so commonly used, I’ll say more about it by example
in a moment.
widget.after(
milliseconds
)
This tool pauses the calling program for a number of
milliseconds—for example, an argument of 5,000 pauses the caller for
5 seconds. This is essentially equivalent to Python’s library
functiontime.sleep(
seconds
)
, and both calls can be used to add a
delay in time-sensitive displays (e.g., animation programs such as
PyDraw and the simpler examples ahead).
widget.after_idle(
function,
*args
)
This tool
schedules the function to be called once when there
are no more pending events to process. That is,function
becomes an idle handler, which is
invoked when the GUI isn’t busy doing anything else.
widget.after_cancel(
id
)
This tool cancels a
pendingafter
callback event before it occurs;id
is
the return value of anafter
event scheduling call.
widget.update()
This tool forces tkinter
to process all pending events in the event queue,
including geometry resizing and widget updates and redraws. You can
call this periodically from a long-running callback handler to
refresh the screen and perform any updates to it that your handler
has already requested. If you don’t, your updates may not appear
on-screen until your callback handler exits. In fact, your display
may hang completely during long-running handlers if not manually
updated (and handlers are not run in threads, as described in the
next section); the window won’t even redraw itself until the handler
returns if covered and uncovered by another.
For instance, programs that animate by repeatedly moving an
object and pausing must call for an update before the end of the
animation or only the final object position will appear on-screen;
worse, the GUI will be completely inactive until the animation
callback returns (see the simple animation examples later in this
chapter, as well as PyDraw in
Chapter 11
).
widget.update_idletasks()
This tool
processes any pending idle events. This may sometimes
be safer thanafter
, which has
the potential to set up race (looping) conditions in some scenarios.
Tk widgets use idle events to display themselves.
_tkinter.createfilehandler(
file,
mask, function
)
This tool
schedules the function to be called when a file’s
status changes. The function may be invoked when the file has data
for reading, is available for writing, or triggers an exception. Thefile
argument is a Python file or
socket object (technically, anything with afileno()
method) or an integer file
descriptor;mask
istkinter.READABLE
ortkinter.WRITABLE
to specify the mode; and
the callbackfunction
takes two arguments—the file
ready to converse and a mask. File handlers are often used to
process pipes or sockets, since normal input/output requests can
block the caller.
Because this call is not available on Windows, it won’t be
used in this book. Since it’s currently a Unix-only alternative,
portable GUIs may be better off usingafter
timer loops to poll for data and
spawning threads to read data and place it on queues if needed—see
Chapter 10
for more details. Threads
are a much more general solution to nonblocking data
transfers.
widget.wait_variable(var)
widget.wait_window(win)
widget.wait_visibility(win)
These tools
pause the caller until a tkinter variable changes its
value, a window is destroyed, or a window becomes visible. All of
these enter a local event loop, such that the application’smainloop
continues to handle events. Note
thatvar
is a tkinter variable
object (discussed earlier), not a simple Python variable. To use for
modal dialogs, first callwidget.focus()
(to
set input focus) andwidget.grab()
(to make a window be the
only one active).
Although we’ll put some of these to work in examples, we won’t go
into exhaustive details on all of these tools here; see other Tk and
tkinter documentation for more information.
Keep in mind
that for many programs, Python’s thread support that we
discussed in
Chapter 5
can serve some of
the same roles as the tkinter tools listed in the preceding section and
can even make use of them. For instance, to avoid blocking a GUI (and
its users) during a long-running file or socket transfer, the transfer
can simply be run in a spawned thread, while the rest of the program
continues to run normally. Similarly, GUIs that must watch for inputs on
pipes or sockets can do so in spawned threads orafter
callbacks, or some combination thereof,
without blocking the GUI itself.
If you do use threads in tkinter programs, however, you need to
remember that only the main thread (the one that built the GUI and
started themainloop
) should
generally make GUI calls. At the least, multiple threads should not
attempt to update the GUI at the same time. For example, theupdate
method described in the preceding
section has historically caused problems in threaded GUIs—if a spawned
thread calls this method (or calls a method that does), it can sometimes
trigger very strange and even spectacular program crashes.
In fact, for a simple and more vivid example of the lack of thread
safety in tkinter GUIs, see and run the following files in the book
examples distribution package:
...\PP4E\Gui\Tour\threads-demoAll-frm.py |
...\PP4E\Gui\Tour threads-demoAll-win.py |
These scripts are takeoffs of the prior chapter’s Examples
8-32
and
8-33
, which run the construction of four
GUI demo components in parallel threads. They also both crash
horrifically on Windows and require forced shutdown of the program.
While some GUI operations appear to be safe to perform in parallel
threads (e.g., see the canvas moves in
Example 9-32
), thread safety is
not guaranteed by tkinter in general. (For further proof of tkinter’s
lack of thread safety, see the discussion of threaded update loops in
the next chapter, just after
Example 10-28
; a thread there that
attempts to pop up a new window also makes the GUI fail
resoundingly.)
This GUI thread story is prone to change over time, but it imposes
a few structural constraints on programs. For example, because spawned
threads cannot usually perform GUI processing, they must generally
communicate with the main thread using global variables or shared
mutable objects such as queues, as required by the application. A
spawned thread which watches a socket for data, for instance, might
simply set global variables or append to shared queues, which in turn
triggers GUI changes in the main thread’s periodicafter
event callbacks. The main thread’s timer
events process the spawned thread’s results.
Although some GUI operations or toolkits may support multiple
threads better than others, GUI programs are generally best structured
as a main GUI thread and non-GUI “worker” threads this way, both to
avoid potential collisions and to finesse the thread safety issue
altogether. The PyMailGUI example later in the book, for instance, will
collect and dispatch callbacks produced by threads and stored on a
queue.
Also remember that irrespective of thread safety of the GUI
itself, threaded GUI programs must follow the same principles of
threaded programs in general—as we learned in
Chapter 5
, such programs must still synchronize
access to mutable state shared between threads, if it may be changed by
threads running in parallel. Although a
producer
/consumer thread model based upon
queues can alleviate many thread issues for the GUI itself, a program
that spawns non-GUI threads to update shared information used by the GUI
thread may still need to use thread locks to avoid concurrent update
issues.
We’ll explore GUI threading in more detail in
Chapter 10
, and we’ll meet more realistic
threaded GUI programs in
Part IV
,
especially in
Chapter 14
’s PyMailGUI. The
latter, for instance, runs long-running tasks in threads to avoid
blocking the GUI, but both restricts GUI updates to the main thread and
uses locks to prevent overlap of operations that may change shared
caches.
Of all the event tools
in the preceding list, theafter
method may be the most interesting. It
allows scripts to schedule a callback handler to be run at some time in
the future. Though a simple device, we’ll use this often in later
examples in this book. For instance, in
Chapter 11
, we’ll meet a clock program that usesafter
to wake up 10 times per second
and check for a new time, and we’ll see an image slideshow program that
usesafter
to schedule the next photo
display (see PyClock and PyView). To illustrate the basics of scheduled
callbacks,
Example 9-27
does something a bit different.
Example 9-27. PP4E\Gui\Tour\alarm.py
# flash and beep every second using after() callback loop
from tkinter import *
class Alarm(Frame):
def __init__(self, msecs=1000): # default = 1 second
Frame.__init__(self)
self.msecs = msecs
self.pack()
stopper = Button(self, text='Stop the beeps!', command=self.quit)
stopper.pack()
stopper.config(bg='navy', fg='white', bd=8)
self.stopper = stopper
self.repeater()
def repeater(self): # on every N millisecs
self.bell() # beep now
self.stopper.flash() # flash button now
self.after(self.msecs, self.repeater) # reschedule handler
if __name__ == '__main__': Alarm(msecs=1000).mainloop()
This script builds the window in
Figure 9-41
and periodically calls both the
button widget’sflash
method to make
the button flash momentarily (it alternates colors quickly) and the
tkinterbell
method to call
your system’s sound interface. Therepeater
method
beeps and flashes once and schedules a callback to be
invoked after a specific amount of time with theafter
method.
Figure 9-41. Stop the beeps!
Butafter
doesn’t pause the
caller: callbacks are scheduled to occur in the background, while the
program performs other processing—technically, as soon as the Tk event
loop is able to notice the time rollover. To make this work,repeater
callsafter
each time through, to reschedule the
callback. Delayed events are one-shot callbacks; to repeat the event as
a loop, we need to reschedule it anew.
The net effect is that when this script runs, it starts beeping
and flashing once its one-button window pops up. And it keeps beeping
and flashing. And beeping. And flashing. Other activities and GUI
operations don’t affect it. Even if the window is iconified, the beeping
continues, because tkinter timer events fire in the background. You need
to kill the window or press the button to stop the alarm. By changing
themsecs
delay, you can make this
beep as fast or as slow as your system allows (some platforms can’t beep
as fast as others…). This may or may not be the best demo to launch in a
crowded office, but at least you’ve been warned.
The buttonflash
method
flashes the widget, but it’s easy to dynamically change
other appearance options of widgets, such as buttons, labels, and
text, with the widgetconfig
method. For instance, you can
also achieve a flash-like effect by manually reversing foreground and
background colors with the widgetconfig
method in scheduledafter
callbacks. Largely for fun,
Example 9-28
specializes the
alarm to go a step further.
Example 9-28. PP4E\Gui\Tour\alarm-hide.py
# customize to erase or show button on after() timer callbacks
from tkinter import *
import alarm
class Alarm(alarm.Alarm): # change alarm callback
def __init__(self, msecs=1000): # default = 1 second
self.shown = False
alarm.Alarm.__init__(self, msecs)
def repeater(self): # on every N millisecs
self.bell() # beep now
if self.shown:
self.stopper.pack_forget() # hide or erase button now
else: # or reverse colors, flash...
self.stopper.pack()
self.shown = not self.shown # toggle state for next time
self.after(self.msecs, self.repeater) # reschedule handler
if __name__ == '__main__': Alarm(msecs=500).mainloop()
When this script is run, the same window appears, but the button
is erased or redrawn on alternating timer events. The widgetpack_forget
method erases (unmaps) a drawn widget andpack
makes it show up again;grid_forget
andgrid
similarly hide and show widgets in a
grid. Thepack_forget
method is
useful for dynamically drawing and changing a running GUI. For
instance, you can be selective about which components are displayed,
and you can build widgets ahead of time and show them only as needed.
Here, it just means that users must press the button while it’s
displayed, or else the noise keeps going.
Example 9-29
goes
even further. There are a handful of methods for hiding and unhiding
entire top-level windows:
To hide and unhide the entire window instead of just one
widget within it, use the
top-level window widgetwithdraw
anddeiconify
methods. Thewithdraw
method, demonstrated in
Example 9-29
, completely
erases the window and its icon (useiconify
if you
want the window’s icon to appear during a hide).
Thelift
method
raises a window above all its siblings or relative
to another you pass in. This method is also known astkraise
, but notraise
—its name in Tk—becauseraise
is a reserved word in
Python.
Thestate
method
returns or changes the window’s current state—it
acceptsnormal
,iconic
,zoomed
(full screen), orwithdrawn
.
Experiment with these methods on your own to see how they
differ. They are also useful to pop up prebuilt dialog windows
dynamically, but are perhaps less practical here.
Example 9-29. PP4E\Gui\Tour\alarm-withdraw.py
# same, but hide or show entire window on after() timer callbacks
from tkinter import *
import alarm
class Alarm(alarm.Alarm):
def repeater(self): # on every N millisecs
self.bell() # beep now
if self.master.state() == 'normal': # is window displayed?
self.master.withdraw() # hide entire window, no icon
else: # iconify shrinks to an icon
self.master.deiconify() # else redraw entire window
self.master.lift() # and raise above others
self.after(self.msecs, self.repeater) # reschedule handler
if __name__ == '__main__': Alarm().mainloop() # master = default Tk root
This works the same, but the entire window appears or disappears
on beeps—you have to press it when it’s shown. You could add lots of
other effects to the alarm, and their timer-based callbacks technique
is widely applicable. Whether your buttons and windows should flash
and disappear, though, probably depends less on tkinter technology
than on your
users’ patience.