Technically, to make this even more flexible, PyMailGUI in
Chapter 14
will queue
bound
methods
with this module—callable objects that, as
mentioned, pair a method function with an instance that gives access
to state information and other methods. In this mode, the thread
manager module’s client code takes a form that looks more like
Example 10-21
: a revision of the
prior example’s self-test using classes and methods.
Example 10-21. PP4E\Gui\Tools\threadtools-test-classes.py
# tests thread callback queue, but uses class bound methods for action and callbacks
import time
from threadtools import threadChecker, startThread
from tkinter.scrolledtext import ScrolledText
class MyGUI:
def __init__(self, reps=3):
self.reps = reps # uses default Tk root
self.text = ScrolledText() # save widget as state
self.text.pack()
threadChecker(self.text) # start thread check loop
self.text.bind('', # 3.x need list for map, range ok
lambda event: list(map(self.onEvent, range(6))) )
def onEvent(self, i): # code that spawns thread
myname = 'thread-%s' % i
startThread(
action = self.threadaction,
args = (i, ),
context = (myname,),
onExit = self.threadexit,
onFail = self.threadfail,
onProgress = self.threadprogress)
# thread's main action
def threadaction(self, id, progress): # what the thread does
for i in range(self.reps): # access to object state here
time.sleep(1)
if progress: progress(i) # progress callback: queued
if id % 2 == 1: raise Exception # odd numbered: fail
# thread callbacks: dispatched off queue in main thread
def threadexit(self, myname):
self.text.insert('end', '%s\texit\n' % myname)
self.text.see('end')
def threadfail(self, exc_info, myname): # have access to self state
self.text.insert('end', '%s\tfail\t%s\n' % (myname, exc_info[0]))
self.text.see('end')
def threadprogress(self, count, myname):
self.text.insert('end', '%s\tprog\t%s\n' % (myname, count))
self.text.see('end')
self.text.update() # works here: run in main thread
if __name__ == '__main__': MyGUI().text.mainloop()
This code both queues bound methods as thread exit and progress
actions and runs bound methods as the thread’s main action itself. As
we learned in
Chapter 5
, because
threads all run in the same process and memory space, bound methods
reference the original in-process instance object, not a copy of it.
This allows them to update the GUI and other implementation state
directly. Furthermore, because bound methods are normal objects which
pass for callables interchangeably with simple functions, using them
both on queues and in threads this way just works. To many, this
broadly shared state of threads is one of their primary advantages
over processes.
Watch for the more realistic application of this module in
Chapter 14
’s PyMailGUI, where it will serve as
the core thread exit and progress dispatch engine. There, we’ll also
run bound methods as thread actions, too, allowing both threads and
their queued actions to access shared mutable object state of the GUI.
As we’ll see, queued action updates are automatically made thread-safe
by this module’s protocol, because they run in the main thread only.
Other state updates to shared objects performed in spawned threads,
though, may still have to be synchronized separately if they might
overlap with other threads, and are made outside the scope of the
callback queue. A direct update to a mail cache, for instance, might
lock out other operations until
finished
.
Sometimes, GUIs pop
up quite unexpectedly. Perhaps you haven’t learned GUI
programming yet; or perhaps you’re just pining for non-event-driven days
past. But for whatever reason, you may have written a program to interact
with a user in an interactive console, only to decide later that
interaction in a real GUI would be much nicer. What to do?
Probably the real answer to converting a non-GUI program is to truly
convert it—restructure it to initialize widgets on startup, callmainloop
once to start event processing and
display the main window, and move all program logic into callback
functions triggered by user actions. Your original program’s actions
become event handlers, and your original main flow of control becomes a
program that builds a main window, calls the GUI’s event loop once, and
waits.
This is the traditional way to structure a GUI program, and it makes
for a coherent user experience; windows pop up on request, instead of
showing up at seemingly random times. Until you’re ready to bite the
bullet and perform such a structural conversion, though, there are other
possibilities. For example, in theShellGui
section earlier in this chapter, we saw
how to add windows to file packing scripts to collect inputs (
Example 10-5
and beyond); later, we
also saw how to redirect such scripts’ outputs to GUIs with theGuiOutput
class (
Example 10-13
). This approach works
if the non-GUI operation we’re wrapping up in a GUI is a single operation;
for more dynamic user interaction, other techniques might be
needed.
It’s possible, for instance, to launch GUI windows from a non-GUI
main program, by calling the tkintermainloop
each time a window must be displayed.
It’s also possible to take a more grandiose approach and add a completely
separate program for the GUI portion of your application. To wrap up our
survey of GUI programming techniques, let’s briefly explore each
scheme.
If you just want to add a
simple GUI user interaction to an existing non-GUI script
(e.g., to select files to open or save), it is possible to do so by
configuring widgets and callingmainloop
from the non-GUI main program when
you need to interact with the user. This essentially makes the program
GUI-capable, but without a persistent main window. The trick is thatmainloop
doesn’t return until the GUI
main window is closed by the user (orquit
method calls), so you cannot retrieve
user inputs from the destroyed window’s widgets aftermainloop
returns. To work around this, all you
have to do is be sure to save user inputs in a Python object: the object
lives on after the GUI is destroyed.
Example 10-22
shows one way to
code this idea in Python.
Example 10-22. PP4E\Gui\Tools\mainloopdemo.py
"""
demo running two distinct mainloop calls; each returns after the main window is
closed; save user results on Python object: GUI is gone; GUIs normally configure
widgets and then run just one mainloop, and have all their logic in callbacks; this
demo uses mainloop calls to implement two modal user interactions from a non-GUI
main program; it shows one way to add a GUI component to an existing non-GUI script,
without restructuring code;
"""
from tkinter import *
from tkinter.filedialog import askopenfilename, asksaveasfilename
class Demo(Frame):
def __init__(self,parent=None):
Frame.__init__(self,parent)
self.pack()
Label(self, text ="Basic demos").pack()
Button(self, text='open', command=self.openfile).pack(fill=BOTH)
Button(self, text='save', command=self.savefile).pack(fill=BOTH)
self.open_name = self.save_name = ""
def openfile(self): # save user results
self.open_name = askopenfilename() # use dialog options here
def savefile(self):
self.save_name = asksaveasfilename(initialdir='C:\\Python31')
if __name__ == "__main__":
# display window once
print('popup1...')
mydialog = Demo() # attaches Frame to default Tk()
mydialog.mainloop() # display; returns after windows closed
print(mydialog.open_name) # names still on object, though GUI gone
print(mydialog.save_name)
# Non GUI section of the program uses mydialog here
# display window again
print('popup2...')
mydialog = Demo() # re-create widgets again
mydialog.mainloop() # window pops up again
print(mydialog.open_name) # new values on the object again
print(mydialog.save_name)
# Non GUI section of the program uses mydialog again
print('ending...')
This program twice builds and displays a simple two-button main
window that launches file selection dialogs, shown in
Figure 10-13
. Its output,
printed as the GUI windows are closed, looks like this:
C:\...\PP4E\Gui\Tools>mainloopdemo.py
popup1...
C:/Users/mark/Stuff/Books/4E/PP4E/dev/Examples/PP4E/Gui/Tools/widgets.py
C:/Python31/python.exe
popup2...
C:/Users/mark/Stuff/Books/4E/PP4E/dev/Examples/PP4E/Gui/Tools/guimixin.py
C:/Python31/Lib/tkinter/__init__.py
ending...
Figure 10-13. GUI window popped up by non-GUI main program
Notice how this program callsmainloop
twice, to implement two modal user
interactions from an otherwise non-GUI script. It’s OK to callmainloop
more than once, but this script takes
care to re-create the GUI’s widgets before each call because they are
destroyed when the previousmainloop
call exits (widgets are destroyed internally inside Tk, even though the
corresponding Python widget object still exists). Again, this can make
for an odd user experience compared to a traditional GUI program
structure—windows seem to pop up from nowhere—but it’s a quick way to
put a GUI face on a script without reworking its code.
Note that this is different from using nested (recursive)mainloop
calls to implement modal dialogs, as
we did in
Chapter 8
. In that mode,
the nestedmainloop
call returns when
the dialog’squit
method is called,
but we return to the enclosingmainloop
layer and remain in the realm of
event-driven programming.
Example 10-22
instead runsmainloop
two different times,
stepping into and out of the event-driven model twice.
Finally, note that this scheme works only if you don’t have to run
any non-GUI code while the GUI is open, because your script’s mainline
code is inactive and blocked whilemainloop
runs. You cannot, for example, apply
this technique to use utilities like those in theguiStreams
module we met earlier in this
chapter to route user interaction from non-GUI code to GUI windows. TheGuiInput
andGuiOutput
classes in that example assume that
there is amainloop
call running
somewhere (they’re GUI-based, after all). But once you callmainloop
to pop up these windows, you can’t
return to your non-GUI code to interact with the user or the GUI until
the GUI is closed and themainloop
call returns. The net effect is that these classes can be used only in
the context of a fully GUI program.
But really, this is an artificial way to use tkinter.
Example 10-22
works only because
the GUI can interact with the user independently, while themainloop
call runs; the script is able to
surrender control to the tkintermainloop
call and wait for results. That
scheme won’t work if you must run any non-GUI code while the GUI is
open. Because of such constraints, you will generally need a
main-window-plus-callbacks model in most GUI programs—callback code runs
in response to user interaction while the GUI remains open. That way,
your code can run while GUI windows are active. For an example, see
earlier in this chapter for the way the non-GUI packer and unpacker
scripts were run from a GUI so that their results appear in a GUI;
technically, these scripts are run in a GUI callback handler so that
their output can be routed to a
widget.
As mentioned
earlier, it’s also possible to spawn the GUI part of your
application as a completely separate program. This is a more advanced
technique, but it can make integration simple for some applications
because of the loose coupling it implies. It can, for instance, help
with theguiStreams
issues of the
prior section, as long as inputs and outputs are communicated to the GUI
over Inter-Process Communication (IPC)
mechanisms
, and the widgetafter
method (or similar) is used by the GUI
program to detect incoming output to be displayed. The non-GUI script
would not be blocked by amainloop
call.
For example, the GUI could be spawned by the non-GUI script as a
separate program, where user interaction results can be communicated
from the spawned GUI to the script using pipes, sockets, files, or other
IPC mechanisms we met in
Chapter 5
. The
advantage to this approach is that it provides a separation of GUI and
non-GUI code—the non-GUI script would have to be modified only to spawn
and wait for user results to appear from the separate GUI program, but
could otherwise be used as is. Moreover, the non-GUI script would not be
blocked while an in-processmainloop
call runs (only the GUI process would run amainloop
), and the GUI program could persist
after the point at which user inputs are required by the script, leading
to fewer pop-up windows.
In other scenarios, the GUI may spawn the non-GUI script instead,
and listen for its feedback on an IPC device connected to the script’s
output stream. In even more complex arrangements, the GUI and non-GUI
script may converse back and forth over bidirectional
connections.
Examples
10-23
,
10-24
, and
10-25
provide a simple example of these
techniques in action: a non-GUI script sending output to a GUI. They
represent non-GUI and GUI programs that communicate over
sockets
—the IPC and networking device we met
briefly in
Chapter 5
and will explore in
depth in the next part of the book. The important point to notice as we
study these files is the way the programs are linked: when the non-GUI
script prints to its standard output, the printed text is sent over a
socket connection to the GUI program. Other than the import and call to
the socket redirection code, the non-GUI program knows nothing at all
about GUIs or sockets, and the GUI program knows nothing about the
program whose output it displays. Because this model does not require
existing scripts to be entirely rewritten to support a GUI, it is ideal
for scripts that otherwise run on the world of shells and command
lines.
In terms of code, we first need some IPC linkage in between the
script and the GUI.
Example 10-23
encapsulates the
client-side socket connection used by non-GUI code for reuse. As is,
it’s a partial work in progress (notice the...
ellipses operator in its last few
functions—Python’s notion of “To be decided,” and equivalent to apass
in this context). Because
sockets are covered in full in
Chapter 12
,
we’ll defer other stream redirection modes until then, when we’ll also
flesh out the rest of this module. The version of this module here
implements just the client-side connection of the standard output stream
to a socket—perfect for a GUI that wants to intercept a non-GUI script’s
printed text.
Example 10-23. PP4E\Gui\Tools\socket_stream_redirect0.py
"""
[partial] Tools for connecting streams of non-GUI programs to sockets
that a GUI (or other) can use to interact with the non-GUI program;
see Chapter 12 and PP4E\Sockets\Internet for a more complete treatment
"""
import sys
from socket import *
port = 50008
host = 'localhost'
def redirectOut(port=port, host=host):
"""
connect caller's standard output stream to a socket for GUI to listen;
start caller after listener started, else connect fails before accept
"""
sock = socket(AF_INET, SOCK_STREAM)
sock.connect((host, port)) # caller operates in client mode
file = sock.makefile('w') # file interface: text, bufferred
sys.stdout = file # make prints go to sock.send
def redirectIn(port=port, host=host): ... # see Chapter 12
def redirectBothAsClient(port=port, host=host): ... # see Chapter 12
def redirectBothAsServer(port=port, host=host): ... # see Chapter 12
Next,
Example 10-24
uses
Example 10-23
to
redirect its prints to a socket on which a GUI server program may
listen; this requires just two lines of code at the top of the script,
and is done selectively based upon the value of a command-line argument
(without the argument, the script runs in fully non-GUI mode):
Example 10-24. PP4E\Gui\Tools\socket-nongui.py
# non-GUI side: connect stream to socket and proceed normally
import time, sys
if len(sys.argv) > 1: # link to gui only if requested
from socket_stream_redirect0 import * # connect my sys.stdout to socket
redirectOut() # GUI must be started first as is
# non-GUI code
while True: # print data to stdout:
print(time.asctime()) # sent to GUI process via socket
sys.stdout.flush() # must flush to send: buffered!
time.sleep(2.0) # no unbuffered mode, -u irrelevant
And finally, the GUI part of this exchange is the program in
Example 10-25
. This script
implements a GUI to display the text printed by the non-GUI program, but
it knows nothing of that other program’s logic. For the display, the GUI
program prints to the stream redirection object we met earlier in this
chapter (
Example 10-12
);
because this program runs a GUImainloop
call, this all just works.
We’re also running a timer loop here to detect incoming data on
the socket as it arrives, instead of waiting for the non-GUI program to
run to completion. Because the socket is set to be
nonblocking
, input calls don’t wait for data to
appear, and hence, do not block the GUI.
Example 10-25. PP4E\Gui\Tools\socket-gui.py
# GUI server side: read and display non-GUI script's output
import sys, os
from socket import * # including socket.error
from tkinter import Tk
from PP4E.launchmodes import PortableLauncher
from PP4E.Gui.Tools.guiStreams import GuiOutput
myport = 50008
sockobj = socket(AF_INET, SOCK_STREAM) # GUI is server, script is client
sockobj.bind(('', myport)) # config server before client
sockobj.listen(5)
print('starting')
PortableLauncher('nongui', 'socket-nongui.py -gui')() # spawn non-GUI script
print('accepting')
conn, addr = sockobj.accept() # wait for client to connect
conn.setblocking(False) # use nonblocking socket (False=0)
print('accepted')
def checkdata():
try:
message = conn.recv(1024) # don't block for input
#output.write(message + '\n') # could do sys.stdout=output too
print(message, file=output) # if ready, show text in GUI window
except error: # raises socket.error if not ready
print('no data') # print to sys.stdout
root.after(1000, checkdata) # check once per second
root = Tk()
output = GuiOutput(root) # socket text is displayed on this
checkdata()
root.mainloop()
Start
Example 10-25
’s
file to launch this example. When both the GUI and the non-GUI processes
are running, the GUI picks up a new message over the socket roughly once
every two seconds and displays it in the window shown in
Figure 10-14
. The GUI’s timer
loop checks for data once per second, but the non-GUI script sends a
message every two seconds only due to itstime.sleep
calls. The printed output in the
terminal windows is as follows—“no data” messages and lines in the GUI
alternate each second:
C:\...\PP4E\Gui\Tools>socket-gui.py
starting
nongui
accepting
accepted
no data
no data
no data
no data
...more...
Figure 10-14. Messages printed to a GUI from a non-GUI program
(socket)
Notice how we’re displayingbytes
strings in
Figure 10-14
—even though the
non-GUI script prints text, the GUI script reads it with the raw socket
interface, and sockets deal in binary byte strings in Python 3.X.
Run this example by yourself for a closer look. In high-level
terms, the GUI script spawns the non-GUI script and displays a pop-up
window that shows the text printed by the non-GUI script (the date and
time). The non-GUI script can keep running linear, procedural code to
produce data, because only the GUI script’s process runs an event-drivenmainloop
call.
Moreover, unlike our earlier stream redirection explorations which
simply connected the script’s streams to GUI objects running in the same
process, this
decoupled
two-process approach
prevents the GUI from being blocked while waiting for the script to
produce output; the GUI process remains fully and independently active,
and simply picks up new results as they appear (more on this in the next
section). This model is similar in spirit to our earlier thread queue
examples, but the actors here are separate programs linked by a socket,
not in-process function calls.
Although we aren’t going to get into enough socket details in this
chapter to fully explain this script’s code, there are a few fine points
worth underscoring here:
This example should probably be augmented to detect and handle
an end-of-file signal from the spawned program, and then terminate
its timer loop.
The non-GUI script could also start the GUI instead, but in
the socket world, the server’s end (the GUI) must be configured to
accept connections
before
the client (the
non-GUI) can connect. One way or another, the GUI has to start
before the non-GUI connects to it or the non-GUI script will be
denied a connection and
will
fail
.
Because of the buffered text nature of thesocket.makefile
objects used for streams
here, the client program is required to flush its printed output
withsys.stdout.flush
to send
data to the GUI—without this call, the GUI receives and displays
nothing. As we’ll learn in
Chapter 12
,
this isn’t required for command pipes, but it is when streams are
reset to wrapped sockets as done here. These wrappers don’t support
unbuffered modes in Python 3.X, and there is no equivalent to the-u
flag in this context (more on-u
and command pipes in the next
section).
Stay tuned for much more on this example and topic in
Chapter 12
. Its socket client/server model works
well and is a general approach to connecting GUI and non-GUI code, but
there are other coding alternatives worth exploring in the next section
before we
move on.