All of this works as advertised—by making command-line tools
available in graphical form like this, they become much more
attractive to users accustomed to the GUI way of life. We’ve
effectively added a simple GUI front-end to command-line tools. Still,
two aspects of this design seem prime for improvement.
First, both of the input dialogs use common code to build the
rows of their input forms, but it’s tailored to this specific use
case; we might be able to simplify the dialogs further by importing a
more generic form-builder module instead. We met general form builder
code in Chapters
8
and
9
, and we’ll meet more later—see theform.py
module in
Chapter 12
for pointers on further genericizing
form construction.
Second, at the point where the user submits input data in either
form dialog, we’ve lost the GUI trail—the 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 blatant:packer
andunpacker
messages still show up in thestdout
console window, not in the
GUI (all the filenames here include full directory paths if you select
them with the GUI’s Browse buttons, courtesy of the standard Open
dialog):
C:\...\PP4E\Gui\ShellGui\temp>python ..\mytools.py list
PP4E scrolledtext
list test
Packer: packed.all ['spam.txt', 'ham.txt', 'eggs.txt']
packing: spam.txt
packing: ham.txt
packing: eggs.txt
Unpacker: packed.all
creating: spam.txt
creating: ham.txt
creating: eggs.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
redirectingstdout
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.
On to our next GUI
coding technique: in response to the challenge posed at the
end of the last section, the script in
Example 10-12
arranges to map input
and output sources to pop-up windows in a GUI application, much as we did
with strings in the stream redirection topics in
Chapter 3
. Although this module is really just
a first-cut prototype and needs improvement itself (e.g., each input line
request pops up a new input
dialog—
not exactly award winning
ergonomics!), it demonstrates the concepts in
general
.
Example 10-12
’sGuiOutput
andGuiInput
objects define methods that allow them
to masquerade as files in any interface that expects a real file. As we
learned earlier in
Chapter 3
, this
includes both theprint
andinput
built-in functions for accessing standard
streams, as well as explicit calls to theread
andwrite
methods of file objects. The two top-level
interfaces in this module handle common use cases:
TheredirectedGuiFunc
function uses this plug-and-play file compatibility to run a function
with its standard input and output streams mapped completely to pop-up
windows rather than to the console window (or wherever streams would
otherwise be mapped in the system shell).
TheredirectedGuiShellCmd
function similarly routes the output of a spawned shell command line
to a pop-up window. It can be used to display the output of any
program in a GUI—including that printed by a Python program.
The module’sGuiInput
andGuiOutput
classes can also be used or
customized directly by clients that need to match a more direct file
method interface or need more fine-grained control over the
process.
Example 10-12. PP4E\Gui\Tools\guiStreams.py
"""
###############################################################################
first-cut implementation of file-like classes that can be used to redirect
input and output streams to GUI displays; as is, input comes from a common
dialog pop-up (a single output+input interface or a persistent Entry field
for input would be better); this also does not properly span lines for read
requests with a byte count > len(line); could also add __iter__/__next__ to
GuiInput to support line iteration like files but would be too many popups;
###############################################################################
"""
from tkinter import *
from tkinter.simpledialog import askstring
from tkinter.scrolledtext import ScrolledText # or PP4E.Gui.Tour.scrolledtext
class GuiOutput:
font = ('courier', 9, 'normal') # in class for all, self for one
def __init__(self, parent=None):
self.text = None
if parent: self.popupnow(parent) # pop up now or on first write
def popupnow(self, parent=None): # in parent now, Toplevel later
if self.text: return
self.text = ScrolledText(parent or Toplevel())
self.text.config(font=self.font)
self.text.pack()
def write(self, text):
self.popupnow()
self.text.insert(END, str(text))
self.text.see(END)
self.text.update() # update gui after each line
def writelines(self, lines): # lines already have '\n'
for line in lines: self.write(line) # or map(self.write, lines)
class GuiInput:
def __init__(self):
self.buff = ''
def inputLine(self):
line = askstring('GuiInput', 'Enter input line +(cancel=eof)')
if line == None:
return '' # pop-up dialog for each line
else: # cancel button means eof
return line + '\n' # else add end-line marker
def read(self, bytes=None):
if not self.buff:
self.buff = self.inputLine()
if bytes: # read by byte count
text = self.buff[:bytes] # doesn't span lines
self.buff = self.buff[bytes:]
else:
text = '' # read all till eof
line = self.buff
while line:
text = text + line
line = self.inputLine() # until cancel=eof=''
return text
def readline(self):
text = self.buff or self.inputLine() # emulate file read methods
self.buff = ''
return text
def readlines(self):
lines = [] # read all lines
while True:
next = self.readline()
if not next: break
lines.append(next)
return lines
def redirectedGuiFunc(func, *pargs, **kargs):
import sys
saveStreams = sys.stdin, sys.stdout # map func streams to pop ups
sys.stdin = GuiInput() # pops up dialog as needed
sys.stdout = GuiOutput() # new output window per call
sys.stderr = sys.stdout
result = func(*pargs, **kargs) # this is a blocking call
sys.stdin, sys.stdout = saveStreams
return result
def redirectedGuiShellCmd(command):
import os
input = os.popen(command, 'r')
output = GuiOutput()
def reader(input, output): # show a shell command's
while True: # standard output in a new
line = input.readline() # pop-up text box widget;
if not line: break # the readline call may block
output.write(line)
reader(input, output)
if __name__ == '__main__': # self test when run
def makeUpper(): # use standard streams
while True:
try:
line = input('Line? ')
except:
break
print(line.upper())
print('end of file')
def makeLower(input, output): # use explicit files
while True:
line = input.readline()
if not line: break
output.write(line.lower())
print('end of file')
root = Tk()
Button(root, text='test streams',
command=lambda: redirectedGuiFunc(makeUpper)).pack(fill=X)
Button(root, text='test files ',
command=lambda: makeLower(GuiInput(), GuiOutput()) ).pack(fill=X)
Button(root, text='test popen ',
command=lambda: redirectedGuiShellCmd('dir *')).pack(fill=X)
root.mainloop()
As coded here,GuiOutput
attaches
aScrolledText
(Python’s standard
library flavor) to either a passed-in parent container or a new top-level
window popped up to serve as the container on the first write call.GuiInput
pops up a new standard input
dialog every time a read request requires a new line of input. Neither one
of these policies is ideal for all scenarios (input would be better mapped
to a more long-lived widget), but they prove the general point
intended.
Figure 10-8
shows
the scene generated by this script’s self-test code, after capturing the
output of a Windows shelldir
listing
command (on the left) and two interactive loop tests (the one with “Line?”
prompts and uppercase letters represents themakeUpper
streams redirection test). An input
dialog has just popped up for a newmakeLower
files interface test.
Figure 10-8. guiStreams routing streams to pop-up windows
This scene may not be spectacular to look at, but it reflects file
and stream input and output operations being automatically mapped to GUI
devices—as we’ll see in a moment, this accomplishes most of the solution
to the prior section’s closing challenge.
Before we move on, we should note that this module’s calls to a
redirected function as well as its loop that reads from a spawned shell
command are potentially
blocking
—they won’t return to
the GUI’s event loop until the function or shell command exits. AlthoughGuiOutput
takes care to call tkinter’supdate
method to update the display
after each line is written, this module has no control in general over the
duration of functions or shell commands it runs.
InredirectedGuiShellCmd
, for
example, the call toinput.readline
will pause until an output line is received from the spawned program,
rendering the GUI unresponsive. Because the output object runs an update
call, the display is still updated during the program’s execution (an
update call enters the Tk event loop momentarily), but only as often as
lines are received from the spawned program. In addition, because of this
function’s loop, the GUI is committed to the shell command in general
until it exits.
Calls to a redirected function inredirectedGuiFunc
are similarly blocking in
general; moreover, during the call’s duration the display is updated only
as often as the function issues output requests. In other words, this
blocking model is simplistic and might be an issue in a larger GUI. We’ll
revisit this later in the chapter when we meet threads. For now, the code
suits our present
purpose.
Now, finally, to
use such redirection tools to map command-line script
output back to a GUI, we simply run calls and command lines with the two
redirected functions in this module.
Example 10-13
shows one way to
wrap the packing operation dialog of the shell GUI section’s
Example 10-10
to force its printed
output to appear in a pop-up window when generated, instead of in the
console.
Example 10-13. PP4E\Gui\ShellGui\packdlg-redirect.py
# wrap command-line script in GUI redirection tool to pop up its output
from tkinter import *
from packdlg import runPackDialog
from PP4E.Gui.Tools.guiStreams import redirectedGuiFunc
def runPackDialog_Wrapped(): # callback to run in mytools.py
redirectedGuiFunc(runPackDialog) # wrap entire callback handler
if __name__ == '__main__':
root = Tk()
Button(root, text='pop', command=runPackDialog_Wrapped).pack(fill=X)
root.mainloop()
You can run this script directly to test its effect, without
bringing up theShellGui
window.
Figure 10-9
shows the
resultingstdout
window after the
pack input dialog is dismissed. This window pops up as soon as script
output is generated, and it is a bit more GUI user friendly than hunting
for messages in a console. You can similarly code the unpack parameters
dialog to route its output to a pop-up. Simply change
mytools.py
in
Example 10-6
to register code
like the function wrapper here as its callback handlers.
In fact, you can use this technique to route the output of any
function call or command line to a pop-up window; as usual, the notion
of compatible object interfaces is at the heart of much of Python code’s
flexibility.
Figure 10-9. Routing script outputs to GUI pop ups