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
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
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
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
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
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
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
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:
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.