Section 11.9. More Ways to Add GUIs to Non-GUI Code


11.9. More Ways to Add GUIs to Non-GUI Code

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 itrestructure it to initialize widgets on startup, call mainloop 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 the ShellGui section earlier in this chapter, we saw how to add windows to file packing scripts to collect inputs; we also saw how to redirect their outputs to text widgets. 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 Tkinter mainloop 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 this chapter, let's briefly explore each scheme.

11.9.1. Popping up GUI Windows on Demand

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 calling mainloop 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 that mainloop doesn't return until the GUI main window is closed by the user (or quit method calls), so you cannot retrieve user inputs from widgets after mainloop 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 11-18 shows one way to code this idea in Python.

Example 11-18. PP3E\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 tkFileDialog 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='D:\\temp') 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 11-13. Its output, printed as the GUI windows are closed, looks like this:

 popup1... C:/Python23/python.exe D:/temp/new.txt popup2... C:/Python23/dir1/_ _init_ _.py D:/temp/public_html/calendar.html ending... 

Figure 11-13. GUI window popped up by non-GUI main program


Notice how this program calls mainloop twice, to implement two modal user interactions from an otherwise non-GUI script. It's OK to call mainloop more than once, but this script takes care to re-create the GUI's widgets before each call because they are destroyed when the previous mainloop call exits (widgets are destroyed internally inside Tk, even though the corresponding Python dialog object still exists). Again, this can make for an odd user experience compared to a traditional GUI program structurewindows seem to pop up from nowherebut 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 mainloop calls to implement modal dialogs, as we did in Chapter 9. In that mode, the nested mainloop call returns when the dialog's quit method is called, but we return to the enclosing mainloop layer and remain in the realm of event-driven programming. Example 11-18 instead runs mainloop 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 is inactive and blocked while mainloop runs. You cannot, for example, apply this technique to use utilities like those in the guiStreams module we met earlier in this chapter to route user interaction from non-GUI code to GUI windows. The GuiInput and GuiOutput classes in that example assume that there is a mainloop call running somewhere (they're GUI based, after all). But once you call mainloop to pop up these windows, you can't return to your non-GUI code to interact with the user until the GUI is closed and the mainloop 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 11-18 works only because the GUI can interact with the user independently, while the mainloop call runs; the script is able to surrender control to the Tkinter mainloop 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 programscallback 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 pack and unpack 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.

11.9.2. Adding a GUI As a Separate Program: Sockets

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 the guiStreams issues of the prior section, as long as inputs and outputs are communicated to the GUI over Inter-Process Communication (IPC) mechanisms, and the widget after 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 a mainloop call.

More generally, 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. The advantage to this approach is that it provides a separation of GUI and non-GUI codethe 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-process mainloop call runs (only the GUI process would run a mainloop), and the GUI program could persist after the point at which user inputs are required by the script, leading to fewer pop-up windows.

Examples 11-19 and 11-20 provide a simplistic example of this technique in action. They represent non-GUI and GUI programs that communicate over socketsan IPC and networking device we will explore in the next part of the book. The important point to notice here is the way the programs are linked. When Example 11-19 starts, it spawns the GUI as a separate program; when it prints to standard output, the printed text is sent over a socket connection to the GUI program. Other than the startup and socket creation calls, the non-GUI program knows nothing at all about GUIs.

Example 11-19. PP3E\Gui\Tools\socket-nongui.py

 import time, sys from socket import * # GUI interface port = 50008                             # I am client: use GUI server port host = 'localhost'                       # or start me after GUI started sock = socket(AF_INET, SOCK_STREAM) sock.connect((host, port)) file = sock.makefile('w', 0)             # file interface wrapper, unbuffered sys.stdout = file                        # make prints go to sock.send # non-GUI code while 1:                                 # print data to stdout     print time.asctime( )                    # sent to GUI process     time.sleep(2.0) 

The GUI part of this exchange is the program in Example 11-20. 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; because this program runs a GUI mainloop call, this 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 11-20. PP3E\Gui\Tools\socket-gui.py

 import sys, os from socket import * from Tkinter import Tk from PP3E.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) os.startfile('socket-nongui.py')          # spawn non-GUI on Windows (os.popen) conn, addr = sockobj.accept( )             # wait for client to connect sockobj.setblocking(0)                     # use nonblocking socket def checkdata( ):     try:         print conn.recv(1024),            # if ready, show text in GUI window     except error:                         # raises socket.error if not ready         pass                              # or message to sys._ _stdout_ _     root.after(1000, checkdata)           # check once per second root = Tk( ) sys.stdout = GuiOutput(root)              # socket text is displayed on this checkdata( ) root.mainloop( ) 

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 11-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 its time.sleep calls.

Figure 11-14. Messages printed to a GUI from a non-GUI program


To run this by yourself, start the GUI scriptit spawns the non-GUI script and displays a pop-up window that shows the text printed in the socket-nongui script (the date and time). The non-GUI script can keep running linear, procedural code to produce data, because only the socket-GUI program runs an event-driven mainloop call.

Although we aren't going to get into enough socket details in this chapter to show how, 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.

The socket client/server model works well and is a general approach to connecting GUI and non-GUI code, but there are a few coding alternatives worth exploring in the next section before we move on.

11.9.3. Adding a GUI As a Separate Program: Pipes

The net effect of the two programs of the preceding section is similar to a GUI program reading the output of a shell command over a pipe file with os.popen; but as we'll see later, sockets can also link programs running on remote machines across a network. Perhaps subtler and more significant is the fact that without an after timer loop and nonblocking input sources, the GUI may become stuck and unresponsive while waiting for data from the non-GUI program and may not be able to handle more than one data stream.

For instance, consider the guiStreams call we wrote in Example 11-9 to redirect the output of a shell command spawned with os.popen to a GUI window. We could use this with simplistic code like that in Example 11-21 to capture the output of a spawned Python program and display it in a separately running GUI program's window.

Example 11-21. PP3E\Gui\Tools\pipes-gui1.py

 from PP3E.Gui.Tools.guiStreams import redirectedGuiShellCmd redirectedGuiShellCmd('python -u pipes-nongui.py') 

Notice the -u Python command-line flag used here: it forces the spawned program's standard streams to be unbuffered, so we get printed text immediately as it is produced, instead of waiting for the spawned program to completely finish. We talked about this option in Chapter 5, when discussing deadlocks and pipes. Recall that print writes to sys.stdout, which is normally buffered. If we don't use the -u flag here and the spawned program doesn't manually call sys.stdout.flush( ), we won't see any output in the GUI until the spawned program exits or until its buffers fill up. If the spawned program is a perpetual loop that does not exit, we may be waiting a long time for output to appear on the pipe, and hence, in the GUI.

This approach makes the non-GUI code in Example 11-22 much simpler: it just writes to standard output as usual, and it need not be concerned with creating a socket interface.

Example 11-22. PP3E\Gui\Tools\pipes-nongui.py

 import time while 1:                             # non-GUI code     print time.asctime( )            # sends to GUI process     time.sleep(2.0) 

Start the GUI script in Example 11-21: it launches the non-GUI program automatically. This works, but the GUI is oddwe never call mainloop ourselves, and we get a default empty top-level window. (In fact, it apparently works at all only because the Tkinter update call issued within the redirect function enters the Tk event loop momentarily to process pending events.) To do better, Example 11-23 creates an enclosing GUI and kicks off an event loop manually by the time the shell command is spawned.

Example 11-23. PP3E\Gui\Tools\pipes-gui2.py

 from Tkinter import * from PP3E.Gui.Tools.guiStreams import redirectedGuiShellCmd def launch( ):     redirectedGuiShellCmd('python -u pipes-nongui.py') window = Tk( ) Button(window, text='GO!', command=launch).pack( ) window.mainloop( ) 

The -u unbuffered flag is crucial herewithout it, you won't see the text output window. The GUI will be blocked in the initial pipe input call indefinitely because the spawned program's standard output will be queued up in an in-memory buffer.

Either way we code this, however, when the GUIs are run they become unresponsive for two seconds at a time while they read data from the os.popen pipewindow moves, resizes, redraws, raises, and so on, are delayed for up to two seconds, until the non-GUI program sends data to the GUI to make the pipe read call return. Worse, if you press the "GO!" button twice in the second version of the GUI, only one window updates itself every two seconds, because the GUI is stuck in the second button press callbackit never exits the loop that reads from the pipe until the spawned non-GUI program exits.

Because of such constraints, a separately spawned GUI must generally read a portion of the data at a time to avoid blocking. For instance, in the socket-based scripts of the prior section (Example 11-20), the after timer loop allows the GUI to poll for data instead of waiting and display it as it arrives.

Of course, the real issue here is that the guiStreams utility is too simplistic; issuing a read call within a GUI is generally prone to blocking. We could try to run the redirect call in a threadfor example, by changing the launch function in Example 11-23 as follows:

 def launch( ):     import thread     thread.start_new(redirectedGuiShellCmd, ('python -u pipes-nongui.py',)) 

But then we would be updating the GUI from a spawned thread, which, as we've learned, is a generally bad idea. With this change, the GUI hangs on Windows on the first "GO!" button press occasionally, and always hangs eventually if you press the button enough times (in fact, the process must be forcibly killed after it hangs). When it does run, it doesn't helpthe text window created in the child thread is still stuck in a read call.

Alternatively, we could try to use the Python select.select call (described in Chapter 13) to implement polling for data on the input pipe; unfortunately, select works only on sockets in Windows today (it also works on pipes and other file descriptors in Unix).

In other contexts, a separately spawned GUI might also use signals to inform the non-GUI program when points of interaction arise, and vice versa (the Python signal module and os.kill call were introduced in Chapter 5). The downside with this approach is that it still requires changes to the non-GUI program to handle the signals.

Named pipes (the fifo files introduced in Chapter 5) are sometimes an alternative to the socket calls of the original Examples 11-19 and 11-20, but sockets work on Windows, and fifos do not (os.mkfifo is not available in Windows XP in 2.4, though it is in Cygwin Python). Even where they do work, we would still need an after timer loop in the GUI to avoid blocking.

We might also use Tkinter's createfilehandler to register a callback to be run when input shows up on the input pipe:

 def callback(file, mask):     ...read from file here... import _tkinter, Tkinter _tkinter.createfilehandler(file, Tkinter.READABLE, callback) 

The file handler creation call is also available within Tkinter.tkinter and as a method of a Tk instance object. Unfortunately again, as noted at the end of Chapter 10, this call is not available on Windows and is a Unix-only alternative.

More generally, the GUI process might spawn a thread that reads the socket or pipe and places the data on a queue so that more than one data stream or long-running activity can overlap in time. In fact, the thread techniques we met earlier in this chapter could be used directly in such a role.

Example 11-24 shows how. The main trick this script employs is to split up the input and output parts of the original redirectedGuiShellCmd of the guiStreams module we met earlier in Example 11-9. By so doing, the input portion can be spawned off in a parallel thread and not block the GUI. The main GUI thread uses an after timer loop as usual, to watch for data to be added by the reader thread to a shared queue. Because the main thread doesn't read program output itself, it does not get stuck in wait states.

Example 11-24. PP3E\Gui\Tools\pipes_gui3.py

 import thread, Queue, os from Tkinter import Tk from PP3E.Gui.Tools.guiStreams import GuiOutput stdoutQueue = Queue.Queue( )                        # infinite size def producer(input):     while True:         line = input.readline( )                    # OK to block: child thread         stdoutQueue.put(line)                       # empty at end-of-file         if not line: break def consumer(output, root, term='<end>'):     try:         line = stdoutQueue.get(block=False)        # main thread: check queue     except Queue.Empty:                            # 4 times/sec, OK if empty         pass     else:         if not line:                               # stop loop at end-of-file             output.write(term)                     # else display next line             return         output.write(line)     root.after(250, lambda: consumer(output, root, term)) def redirectedGuiShellCmd(command, root):     input  = os.popen(command, 'r')                # start non-GUI program     output = GuiOutput(root)     thread.start_new_thread(producer, (input,))    # start reader thread     consumer(output, root) if _ _name_ _ == '_ _main_ _':     win = Tk( )     redirectedGuiShellCmd('python -u pipes-nongui.py  ', win)     win.mainloop( ) 

As usual, we use a queue here to avoid updating the GUI except in the main thread. Note that we didn't need a thread or queue in the prior section's socket example, just because we're able to poll a socket to see whether it has data without blocking; an after timer loop was enough. For a shell-command pipe, though, a thread is an easy way to avoid blocking.

When run, this program's self-test code creates a ScrolledText window that displays the current date and time sent from the pipes-nongui.py script in Example 11-22 (its window is identical to Figure 11-14). The window is updated with a new line every two seconds because that's how often the spawned pipes-nongui script prints a message to stdout.

Note how the producer thread calls readline( ) to load just one line at a time. We can't use input calls that consume the entire stream all at once (e.g., read( ), readlines( )), because such calls would not return until the program exits and sends end-of-file. The read(N) call would work to grab one piece of the output as well, but we assume that the output stream is text here. Also notice that the -u unbuffered stream flag is used here again, to get output as it is produced; without it, output won't show up in the GUI at all because it is buffered in the spawned program (try it yourself).

This is similar in spirit to what we did in Example 11-23. Due to the way its code is structured, though, Example 11-24 has two major advantages:

  • Because input calls are spawned off in a thread this time, the GUI is completely responsive. Window moves, resizes, and so forth, happen immediately because the GUI is not blocked while waiting for output from the non-GUI program. Although it is more complex and requires thread support, its lack of blocking makes this redirectedGuiShellCmd much more generally useful than the original version we coded earlier.

  • Moreover, because this GUI reads the spawned program's standard output, no changes are required in the non-GUI program. Unlike the socket-based example in the prior section, the non-GUI program here needs no knowledge of the GUI that will display its results.

The only downside to this approach compared to the sockets of the prior section is that it does not directly support running the GUI and non-GUI programs on remote machines. As we'll see later, sockets allow data to be passed between programs running on the same machine or across networks. Furthermore, if the GUI must do more than display another program's output, sockets become a more general solutionas we'll learn later, because sockets are bidirectional data streams, they allow data to be passed back and forth between two programs in more arbitrary ways.

Here's another use case: the following code imports the new GUI redirection function as a library component and uses it to create a window that displays four lines of successively longer strings, followed by a final line containing <end>, reflecting the spawned program's exit:

 >>> print open('spams.py').read( ) import time for i in range(1, 5):     time.sleep(2)     print 'spam' * i >>> from Tkinter import * >>> from pipes_gui3 import redirectedGuiShellCmd >>> root = Tk( ) >>> redirectedGuiShellCmd('python -u spams.py', root) 

If the spawned program exits, Example 11-24 detects end-of-file on the pipe and puts the final empty line in the queue; the consumer thread displays an <end> line in the GUI by default when it detects this condition. Here again, the sleep call in the spawned program simulates a long-running task, and we really need the -u unbuffered streams flagwithout it, no output appears in the GUI for eight seconds, until the spawned program is completely finished. With it, the GUI receives and displays each line as it is printed, one every two seconds.

This is also, finally, the sort of code you could use to display the output of a non-GUI program in a GUI, without sockets, changes in the original program, or blocking the GUI. Of course, in many cases, if you have to work this hard to add a GUI anyhow, you might as well just make your script a traditional GUI program with a main window and event loop. Furthermore, the GUIs we've coded in this section are limited to displaying another program's output; sometimes the GUI may have to do more. For many programs, though, the general separation of display and program logic provided by the spawned GUI model can be an advantageit's easier to understand both parts if they are not mixed together.

We'll learn all about sockets in the next part of the book, so you should consider parts of this discussion something of a preview. As we'll see, things start to become more and more interesting when we start combining GUIs, threads, and network sockets. Before we do, though, the next chapter closes out the purely GUI part of this book by applying the widgets and techniques we've learned in more realistically scaled programs.




Programming Python
Programming Python
ISBN: 0596009259
EAN: 2147483647
Year: 2004
Pages: 270
Authors: Mark Lutz

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net