DekGenius.com
[ Team LiB ] Previous Section Next Section

28.3 A Tkinter-Based GUI Editor for Managing Form Data

Let's recap: we wrote a CGI program (feedback.py) that takes the input from a web form and stores the information on disk on our server. We then wrote a program (formletter.py) that takes some of those files and generates apologies to those deserving them. The next task is to construct a program to allow a human to look at the comments and complaints, using the Tkinter toolkit to build a GUI browser for these files.

The Tkinter toolkit is a Python-specific interface to a non-Python GUI library called Tk. Tk is the GUI toolkit most commonly chosen by Python programmers because it provides professional-looking GUIs within a fairly easy-to-use system and because the Python/Tk interface comes with most Python distributions. The interfaces it generates don't look exactly like Windows, the Mac, or any Unix toolkit, but they look very close to each of them, and the same Python program works on all those platforms, an impossible task with any platform-specific toolkit. Two other portable toolkits worth considering are wxPython (http://www.wxPython.org) and PyQt.

Tk, therefore, is what we'll use in this example. It's a toolkit developed by John Ousterhout, originally as a companion to Tcl, another scripting language. Since then, Tk has been adopted by many other scripting languages including Python and Perl.

The goals of this program are simple: to display in a window a description of each feedback instance, allowing the user to select one to examine in greater detail (e.g., seeing the contents of the text widget). Furthermore, Joe wants to be able to discard items once they have been dealt with. A screenshot of the finished program in action is shown in Figure 28-4.

Figure 28-4. A sample screen dump of the feedbackeditor.py program
figs/lpy2_2804.gif

28.3.1 The Main Program

We'll work through one possible way of coding the program to manage form data. Our entire program, called feedbackeditor.py, is:

from FormEditor import FormEditor
from feedback import FeedbackData, FormData 
from Tkinter import mainloop
FormEditor("Feedback Editor", FeedbackData, feedback.DIRECTORY)
mainloop(  )

The point of breaking these five lines out into a separate file is that we've broken out all that is specific to our form. As we'll see, the FormEditor program is completely independent of the specific CGI form. A further point made explicit by this microprogram is that it shows how to interact with Tkinter; you create widgets and windows, and then call the mainloop function, which sets the GUI running. Every change in the program that follows happens as a result of GUI actions. As for formletter.py, this program imports the class objects from the feedback module, for the same reason (unpickling). Then, an instance of the FormEditor class is created, passing to its initialization function the name of the editor, the class of the objects being unpickled, and the location of the pickled instances.

28.3.2 The Form Editor

The code for FormEditor.py is just a class definition, which we'll show all at once and then describe in parts, one method at a time:

from Tkinter import *
import os, pickle

class FormEditor:

    def __init__(self, name, dataclass, storagedir):
        self.storagedir = storagedir      # Stash away some references.
        self.dataclass = dataclass
        self.row = 0
        self.current = None

        self.root = root = Tk(  )           # Create window and size it.
        root.minsize(300,200)

        root.rowconfigure(0, weight=1)    # Define how columns and rows scale
        root.columnconfigure(0, weight=1) # when the window is resized.
        root.columnconfigure(1, weight=2)
        
        # Create the title Label.
        Label(root, text=name, font='bold').grid(columnspan=2)
        self.row = self.row + 1 
        # Create the main listbox and configure it.
        self.listbox = Listbox(root, selectmode=SINGLE)
        self.listbox.grid(columnspan=2, sticky=E+W+N+S)
        self.listbox.bind('<ButtonRelease-1>', self.select)
        self.row = self.row + 1

        # Call self.add_variable once per variable in the class's fieldnames var.
        for fieldname in dataclass.fieldnames:
            setattr(self, fieldname, self.add_variable(root, fieldname))

        # Create a couple of buttons, with assigned commands.
        self.add_button(self.root, self.row, 0, 'Delete Entry', self. delentry)
        self.add_button(self.root, self.row, 1, 'Reload', self.load_data)

        self.load_data(  )

    def add_variable(self, root, varname):
        Label(root, text=varname).grid(row=self.row, column=0, sticky=E)
        value = Label(root, text='', background='gray90',
                      relief=SUNKEN, anchor=W, justify=LEFT)
        value.grid(row=self.row, column=1, sticky=E+W)
        self.row = self.row + 1
        return value

    def add_button(self, root, row, column, text, command):
        button = Button(root, text=text, command=command)
        button.grid(row=row, column=column, sticky=E+W, padx=5, pady=5)

    def load_data(self):
        self.listbox.delete(0,END)
        self.items = [  ]
        for filename in os.listdir(self.storagedir):
            item = pickle.load(open(os.path.join(self.storagedir, filename)))
            item._filename = filename
            self.items.append(item)
            self.listbox.insert('end', repr(item))
        self.listbox.select_set(0)
        self.select(None)

    def select(self, event):
        selection = self.listbox.curselection(  )
        self.selection = self.items[int(selection[0])]
        for fieldname in self.dataclass.fieldnames:
            label = getattr(self, fieldname)               # GUI field
            labelstr = getattr(self.selection, fieldname)  # instance attribute
            labelstr = labelstr.replace('\r', '')
            label.config(text=labelstr)

    def delentry(self):
        os.remove(os.path.join(self.storagedir,self.selection._filename))
        self.load_data(  )

You'll notice that the first line uses the from . . . import * construct we warned you about earlier. In Tkinter programs, it's usually fairly safe, because Tkinter only exports variables that are fairly obviously GUI-related (Label, Widget, etc.), and they all start with uppercase letters.

Understanding the __init__ method is best done by comparing the structure of the code to the structure of the window screen dump shown in Figure 28-4. As you move down the __init__ method lines, you should be able to match many statements with their graphical consequences.

The first few lines simply stash away a few things in instance variables and assign default values to variables. The next set of lines access a Toplevel widget (basically, a window; the Tk( ) call returns the currently defined top-level widget), set its minimum size and a few properties. The row and column configuration options allow the widgets inside the window to scale if the user changes the size of the window and determines the relative width of the two columns of internal widgets.

The next call creates a Label widget, which is defined in the Tkinter module, and which, as you can see in the screen dump, is just a text label. It spans both columns of widgets, meaning that it extends from the leftmost edge of the window to the rightmost edge. Specifying the locations of graphical elements is responsible for the majority of GUI calls, due to the wide array of possible arrangements.

The Listbox widget is created next; it's a list of text lines, which can be selected by the user using arrow keys and the mouse button. This specific listbox allows only one line to be selected at a time (selectmode=SINGLE) and fills all the space available to it (the sticky option).

The for loop block, by iterating over the fieldnames attribute of the dataclass variable (the fieldnames class of the FeedbackData class), finds out which variables are in the instance data, and for each, calls the add_variable method of the FormEditor class and takes the returned value and stuffs it in an instance variable. This is equivalent in our case to:

    ...
self.name = self.add_variable(root, 'name')
self.email = self.add_variable(root, 'email')
self.address = self.add_variable(root, 'address')
self.type = self.add_variable(root, 'type')
self.text = self.add_variable(root, 'text')

The version in the code sample, however, is better, because the list of field names is already available to the program and retyping anything is usually an indicator of bad design. Furthermore, there is nothing about FormData that is specific to our forms. It can be used to browse any instance of a class that defines a variable fieldnames. Making the program generic like this makes it more likely to be reused in other contexts for other tasks.

Finishing off with the __init__ method, we see that two buttons finish the graphical layout of the window, each associated with a command that's executed when it's clicked. One is the delentry method, which deletes the current entry, and the other is a reloading function that rereads the data in the storage directory.

Finally, the data is loaded by a call to the load_data method. We'll describe it as soon as we're done with the calls that set up widgets, namely add_variable and add_button.

add_variable creates two Label widgets on the same row. The first displays the name of the field, and the second will contain the value of the corresponding field in the entry selected in the listbox:

def add_variable(self, root, varname):
    Label(root, text=varname).grid(row=self.row, column=0, sticky=E)
    value = Label(root, text='', background='gray90',
                  relief=SUNKEN, anchor=W, justify=LEFT)
    value.grid(row=self.row, column=1, sticky=E+W)
    self.row = self.row + 1
    return value

add_button is simpler, as it needs to create only one widget:

def add_button(self, root, row, column, text, command):
    button = Button(root, text=text, command=command)
    button.grid(row=row, column=column, sticky=E+W, padx=5, pady=5)

The load_data function is called when the Refresh button is selected. Before loading the data from the pickled file, it first cleans up any contents in the listbox (the graphical list of items) corresponding to possibly out-of-date data and resets the items attribute (which is a Python list that will contain references to the actual data instances). The loop that fills in the listbox and items attribute is quite similar to that used for formletter.py:

def load_data(self):
    self.listbox.delete(0,END)
    self.items = [  ]
    for filename in os.listdir(self.storagedir):
        item = pickle.load(open(os.path.join(self.storagedir, filename)))
        item._filename = filename
        self.items.append(item)
        self.listbox.insert('end', repr(item))
    self.listbox.select_set(0)
    self.select(None)

Note that:

  • The name of the file in which an instance is stored is attached as an attribute to that instance.

  • The instance is added to the items instance attribute.

  • The string representation of the item is added to the listbox.

  • The first item in the listbox is selected.

We now get to the select method. It's called in one of two circumstances. The first, as we just showed, is the last thing to happen when the data is loaded. The second is a consequence of the binding operation in the __init__ method, which we reprint here:

self.listbox.bind('<ButtonRelease-1>', self.select)

This call binds the occurrence of a specific event ('<ButtonRelease-1>') in a specific widget (self.listbox) to a call to self.select. In other words, whenever you let go of the left mouse button on an item in the listbox, the select method of your editor is called. It's called with an argument of type Event, which can let us know such things as when the button click occurred, but since we don't need to know anything about the event except that it occurred, we'll ignore it. What must happen on selection? First, the instance corresponding to the item being selected in the GUI element must be identified, and then the fields corresponding to the values of that instance must be updated. This is performed by iterating over each field name (looking back to the fieldnames class variable again), finding the value of the field in the selected instance, and configuring the appropriate label widget to display the right text:[3]

[3] The replace operation is needed because Tk treats the \r\n sequence that occurs on Windows machines as two carriage returns instead of one.

def select(self, event):
    selection = self.listbox.curselection(  )
    self.selection = self.items[int(selection[0])]
    for fieldname in self.dataclass.fieldnames:
        label = getattr(self, fieldname)               # GUI field
        labelstr = getattr(self.selection, fieldname)  # instance attribute
        labelstr = string.replace(labelstr,'\r', '')
        label.config(text=labelstr)

The reload functionality is exactly that of the load_data method, which is why that's what was passed as the command to be called when the Reload button is clicked. The deletion of an entry, however, is a tad more difficult. The first thing we do when loading an instance from disk is to give it an attribute that corresponds to the filename whence it came. We use this information to delete the file before asking for a reload; the listbox is automatically updated:

def delentry(self):
    os.remove(os.path.join(self.storagedir,self.selection._filename))
    self.load_data(  )

This program is probably the hardest to understand of any in this book, simply because it uses the complex and powerful Tkinter library extensively. There is documentation for Tkinter, as well as for Tk itself.

  • The most complete documentation is Fredrik Lundh's documentation, available on the Web at http://www.pythonware.com/library/tkinter/introduction/index.htm.

  • Several books cover Tkinter. John Grayson wrote a book devoted to Tkinter programming, Python and Tkinter Programming (Manning Publications). O'Reilly's Programming Python also has extensive coverage of Tkinter, including a 260-page section devoted to basic widgets and complete example programs. Finally, Python in a Nutshell (O'Reilly) has concise documentation covering most of Tkinter.

  • The New Mexico Institute of Mining and Technology created its own 84-page Tkinter manual. It is available in PDF and PostScript form from http://www.nmt.edu/tcc/help/lang/python/docs.html. Look under "Locally written documentation."

  • Also, see the Python web site section on Tkinter at http://www.python.org/topics/tkinter/.

28.3.3 Design Considerations

Think of the CGI script feedback.py and the GUI program FormEditor.py as two different ways of manipulating a common dataset (the pickled instances on disk). When should you use a web-based interface, and when should you use a GUI? The choice should be based on a couple of factors:

  • How easy is it to implement the needed functionality in a given framework?

  • What software can you require the user to install in order to access or modify the data?

The web frontend is well suited to cases where the complexity of the data manipulation requirements is low and where it's more important that users be able to work the program than that the program be full-featured. Building a real program on top of a GUI toolkit, on the other hand, allows maximum flexibility, at the cost of having to teach the user how to use it and/or installing specific programs. One reason for Python's success among experienced programmers is that Python allows them to design programs on such reasoned bases, as opposed to forcing them to use one type of programming framework just because it's what the language designer had in mind.

    [ Team LiB ] Previous Section Next Section