Chapter 11. User Interfaces

Introduction

Credit: Fredrik Lundh, SecretLabs AB, author of Python Standard Library

Back in the early days of interactive computing, most computers offered terminals that looked and behaved pretty much like clunky typewriters. The main difference from an ordinary typewriter was that the computer was in the loop. It could read what the user typed and print hard-copy output on a roll of paper.

So when you found yourself in front of a 1960s Teletype ASR-33, the only reasonable way to communicate with the computer was to type a line of text, press the send key, hope that the computer would manage to figure out what you meant, and wait for the response to appear on the paper roll. This line-oriented way of communicating with your computer is known as a command-line interface (CLI).

Some 40 years later, the paper roll has been replaced with high-resolution video displays, which can display text in multiple typefaces, color photographs, and even animated 3D graphics. The keyboard is still around, but we also have pointing devices such as the mouse, trackballs, game controls, touchpads, and other input devices.

The combination of a graphics display and the mouse made it possible to create a new kind of user interface: the graphical user interface (GUI). When done right, a GUI can give the user a better overview of what a program can do (and what it is doing), and make it easier to carry out many kinds of tasks.

However, most programming languages, including Python, make it easy to write programs using teletype-style output and input. In Python, you use the print statement to print text to the display and the input and raw_input functions to read expressions and text strings from the keyboard.

Creating GUIs takes more work. You need access to functions to draw text and graphics on the screen, select typefaces and styles, and read information from the keyboard and other input devices. You need to write code to interact with other applications (via a window manager), keep your windows updated when the user moves them around, and respond to key presses and mouse actions.

To make this a bit easier, programmers have developed graphical user interface toolkits, which provide standard solutions to these problems. A typical GUI toolkit provides a number of ready-made GUI building blocks, usually called widgets. Common standard widgets include text and image labels, buttons, and text-entry fields. Many toolkits also provide more advanced widgets, such as Tkinter’s Text widget, which is a rather competent text editor/display component.

All major toolkits are event based, which means that your program hands control over to the toolkit (usually by calling a “main loop” function or method). The toolkit then calls back into your application when certain events occur—for example, when the user clicks OK in a dialog or when a window needs to be redrawn. Most toolkits also provide ways to position widgets on the screen automatically (e.g., in tables, rows, or columns) and to modify widget behavior and appearance.

Tkinter is the de facto standard toolkit for Python and comes with most Python distributions. Tkinter provides an object-oriented layer on top of the Tcl/Tk GUI library and runs on Windows, Unix, and Macintosh systems. Tkinter is easy to use but provides a relatively small number of standard widgets. Tkinter extension libraries, such as Pmw and Tix, supply many components missing from plain Tkinter, and you can use Tkinter’s advanced Text and Canvas widgets to create custom widgets. The Widget Construction Kit, WCK, lets you write all sorts of new widgets in pure Python: see http://effbot.org/zone/wck.htm.

wxPython (http://www.wxPython.org) is another popular toolkit; it is based on the wxWidgets C++ library (http://www.wxWidgets.org). wxPython is modeled somewhat after the Windows MFC library but is available for multiple platforms. wxPython provides a rich set of widgets, and it’s relatively easy to create custom widgets.

PyGTK (http://www.pygtk.org) is an object-oriented Python interface to the Gimp toolkit (GTK), used in projects such as Gnome and the Gimp. PyGTK is a good choice for Linux applications, especially if you want them to run in the Gnome environment.

PyQt (http://www.riverbankcomputing.co.uk/pyqt/index.php) is a Python wrapper for TrollTech’s Qt library (http://www.trolltech.com), which is the basis of the popular KDE environment, as well as the Qtopia environment for hand-held computers; it also runs on Windows and Mac OS X. Qt and PyQt require license fees for commercial (software that is not free) use, but are free (licensed by the GPL) for free software development. (No GPL-licensed Qt is currently available for Windows, but one is under development—see http://kde-cygwin.sourceforge.net/qt3-win32/.)

You can also use many other toolkits from Python. Mark Hammond’s Pythonwin gives access to Windows MFC. Greg Ewing is developing a cross-platform GUI API, known as PyGUI (http://nz.cosc.canterbury.ac.nz/~greg/python_gui/), developed specifically for Python and taking advantage of Python’s unique strengths. Also available are interfaces to Motif/X11 and Mac OS X native toolboxes and many other toolkits. Cameron Laird maintains a list of toolkits at http://starbase.neosoft.com/~claird/comp.lang.python/python_GUI.html. It currently lists about 20 toolkits. A Wiki page at http://www.python.org/cgi-bin/moinmoin/GuiProgramming is actively maintained lists even more.

Finally, several projects, in various stages, are based on the idea of overlaying easy unified APIs on top of one or more other toolkits or graphical facilities. anygui (rather dormant—see http://www.anygui.org), PythonCard (pretty active—see http://pythoncard.sourceforge.net/), Wax (http://zephyrfalcon.org/labs/dope_on_wax.html), and PyUI (http://pyui.sourceforge.net/) are examples of this “higher-level” approach.

11.1. Showing a Progress Indicator on a Text Console

Credit: Larry Bates

Problem

Your program has no GUI (i.e., your program just runs on a text console), and yet you want your program to show to the user a “progress indicator bar” during lengthy operations, to communicate that work is progressing and the amount of the total work that has been completed.

Solution

We can easily code a simple little class to handle this whole task:

import sys
class progressbar(object):
    def _ _init_ _(self, finalcount, block_char='.'):
        self.finalcount = finalcount
        self.blockcount = 0
        self.block = block_char
        self.f = sys.stdout
        if not self.finalcount: return
        self.f.write('
------------------ % Progress -------------------1
')
        self.f.write('    1    2    3    4    5    6    7    8    9    0
')
        self.f.write('----0----0----0----0----0----0----0----0----0----0
')
    def progress(self, count):
        count = min(count, self.finalcount)
        if self.finalcount:
            percentcomplete = int(round(100.0*count/self.finalcount))
            if percentcomplete < 1: percentcomplete = 1
        else:
            percentcomplete=100
        blockcount = int(percentcomplete//2)
        if blockcount <= self.blockcount:
            return
        for i in range(self.blockcount, blockcount):
            self.f.write(self.block)
        self.f.flush( )
        self.blockcount = blockcount
        if percentcomplete == 100:
            self.f.write("
")

Discussion

Here is an example of the use of this progressbar class, presented, as usual, with a guard of if _ _name_ _ == '_ _main_ _‘. We can make it part of the module containing the class and have it run when the module is executed as a “main script”:

if _ _name_ _ == "_ _main_ _":
    from time import sleep
    pb = progressbar(8, "*")
    for count in range(1, 9):
        pb.progress(count)
        sleep(0.2)
    pb = progressbar(100)
    pb.progress(20)
    sleep(0.3)
    pb.progress(47)
    sleep(0.3)
    pb.progress(90)
    sleep(0.3)
    pb.progress(100)
    print "testing 1:"
    pb = progressbar(1)
    pb.progress(1)

Programs that run lengthy operations, such as FTP downloads and database insertions, should normally give visual feedback to the user regarding the progress of the task that is running. GUI toolkits generally have such facilities included as “widgets”, but if your program does not otherwise require a GUI, it’s overkill to give it one just to be able to display a progress bar. This recipe’s progress bar class provides an easy way of showing the percentage of completion that is updated periodically by the program.

The recipe operates on the basis of a totally arbitrary final count that the ongoing task is supposed to reach at the end. This makes it optimally easy for the application that makes use of the progressbar class: the application can use any handy unit of measure (such as amount of bytes downloaded for an FTP download, number of records inserted for a database insertion, etc.) to track the task’s progress and completion. As long as the same unit of measure applies to both the “final count” and the count argument that the application must periodically pass to the progress method, the progress bar’s display will be accurate.

See Also

Documentation on text-mode console I/O in Python in a Nutshell.

11.2. Avoiding lambda in Writing Callback Functions

Credit: Danny Yoo, Martin Sjogren

Problem

You need to use many callbacks without arguments, typically while writing a Tkinter-based GUI, and you’d rather avoid using lambda.

Solution

Between the classic lambda approach and a powerful general-purpose currying mechanism is a third, extremely simple way for doing callbacks that can come in handy in many practical cases:

def command(callback, *args, **kwargs):
    def do_call( ):
        return callback(*args, **kwargs)
    # 2.4 only: do_call._ _name_ _ = callback._ _name_ _
    return do_call

Discussion

I remember a utility class (to perform the same task handled by a closure in this recipe) quite a while back, but I don’t remember who to attribute it to. Perhaps I saw it in John E. Grayson, Python and Tkinter Programming (Manning).

Writing a lot of callbacks that give customized arguments can look a little awkward with lambda, so this command closure provides alternative syntax that is easier to read. For example:

import Tkinter
def hello(name):
    print "Hello", name
root = Tk( )
# the lambda way of doing it:
Button(root, text="Guido", command=lambda name="Guido": hello(name)).pack( )
# using the Command class:
Button(root, text="Guido", command=command(hello, "Guido")).pack( )

Of course, you can also use a more general currying approach, which enables you to fix some of the arguments when you bind the callback, while others may be given at call time (see Recipe 16.4). However, “doing the simplest thing that can possibly work” is a good programming principle (this wording of the principle is due, I believe, to Kent Beck). If your application needs callbacks that fix all arguments at currying time and others that leave some arguments to be determined at callback time, it’s probably simpler to use the more general currying approach for all the callbacks. But if all the callbacks you need must fix all arguments at currying time, it may be simpler to forego unneeded generality and use the simpler, less-general approach in this recipe exclusively. You can always refactor later if it turns out that you do need the generality.

See Also

Recipe 16.4; information about Tkinter can be obtained from a variety of sources, such as Fredrik Lundh, An Introduction to Tkinter (PythonWare: http://www.pythonware.com/library), New Mexico Tech’s Tkinter Reference (http://www.nmt.edu/tcc/help/lang/python/docs.html), Python in a Nutshell, and various other books.

11.3. Using Default Values and Bounds with tkSimpleDialog Functions

Credit: Mike Foord, Peter Cogolo

Problem

You need to get an input value from the user with one of Tkinter’s tkSimpleDialog dialog functions, but you want to add a default value, or to ensure that the value entered lies within certain bounds.

Solution

Each of Tkinter’s tkSimpleDialog functions (askstring, askfloat, askinteger) supports an optional default value, as well as optional validation against minimum and maximum value. However, this set of features is not clearly spelled out in the documentation. Here’s a wrapper function that you may find preferable:

import tkSimpleDialog
_dispatch = { str: tkSimpleDialog.askstring,
              int: tkSimpleDialog.askinteger,
              float: tkSimpleDialog.askfloat,
            }
def getinput(title, prompt, type=str, default=None, min=None, max=None):
    ''' gets from the user an input of type `type' (str, int or float),
        optionally with a default value, and optionally constrained to
        lie between the values `min' and `max' (included).
    '''
    f = _dispatch.get(type)
    if not f:
        raise TypeError, "Can't ask for %r input" % (type,)
    return f(title, prompt, initialvalue=default, minvalue=min, maxvalue=max)

Discussion

The built-in tkSimpleDialog module offers a few simple functions that pop up dialogs that ask the user to input a string, a float, or an integer—not a very advanced user interface but dirt-simple to use in your programs. Unfortunately, while these functions do support a few nice extras (the ability to pass in a default value, and having the result validated within certain optional minimum and maximum values), the module’s documentation (what little there is of it) does not make this feature clear. Even the pydoc-generated page http://epydoc.sourceforge.net/stdlib/public/tkSimpleDialog-module.html just says “see SimpleDialog class.” Since no such class exists, seeing it is not easy. (The relevant class is actually named _QueryDialog, and due to the leading underscore in the name, it is considered “private”. Therefore pydoc does not build a documentation web page for it.)

This recipe shows how to access this functionality that’s already part of the Python Standard Library. As a side benefit, it refactors the functionality into a single getinput function that takes as an argument the type of input desired (defaulting to str, meaning that the default type of result is a string, just as for built-in function raw_input). If you prefer the original concept of having three separate functions, it’s easy to modify the recipe according to your tastes. The recipe mostly makes the semi-hidden functionality of the original functions’ undocumented keyword arguments initialvalue, minvalue and maxvalue manifest and clearer through its optional parameters default, min, and max, which it passes right on to the underlying original function.

See Also

tkSimpleDialog module documentation is at http://epydoc.sourceforge.net/stdlib/public/tkSimpleDialog-module.html.

11.4. Adding Drag and Drop Reordering to a Tkinter Listbox

Credit: John Fouhy

Problem

You want to use a Tkinter Listbox widget, but you want to give the user the additional capability of reordering the entries by drag-and-drop.

Solution

We just need to code the relevant functionality and bind it to the Tkinter event corresponding to the “drag” mouse gesture:

import Tkinter
class DDList(Tkinter.Listbox):
    """ A Tkinter listbox with drag'n'drop reordering of entries. """
    def _ _init_ _(self, master, **kw):
        kw['selectmode'] = Tkinter.SINGLE
        Tkinter.Listbox._ _init_ _(self, master, kw)
        self.bind('<Button-1>', self.setCurrent)
        self.bind('<B1-Motion>', self.shiftSelection)
        self.curIndex = None
    def setCurrent(self, event):
        self.curIndex = self.nearest(event.y)
    def shiftSelection(self, event):
        i = self.nearest(event.y)
        if i < self.curIndex:
            x = self.get(i)
            self.delete(i)
            self.insert(i+1, x)
            self.curIndex = i
        elif i > self.curIndex:
            x = self.get(i)
            self.delete(i)
            self.insert(i-1, x)
            self.curIndex = i

Discussion

Here is an example of use of this DDList class, presented, as usual, with a guard of if _ _name_ _ == '_ _main_ _' so we can make it part of the module containing the class and have it run when the module is executed as a “main script”:

if _ _name_ _ == '_ _main_ _':
    tk = Tkinter.Tk( )
    length = 10
    dd = DDList(tk, height=length)
    dd.pack( )
    for i in xrange(length):
        dd.insert(Tkinter.END, str(i))
    def show( ):
        ''' show the current ordering every 2 seconds '''
        for x in dd.get(0, Tkinter.END):
            print x,
        print
        tk.after(2000, show)
    tk.after(2000, show)
    tk.mainloop( )

Allowing the user of a GUI program to drag the elements of a list into new positions is often useful, and this recipe shows a fairly simple way of adding this functionality to a Tkinter Listbox widget.

This recipe’s code tries to ensure that the clicked-on element stays selected by deleting and inserting on either side of it. Nevertheless, it is possible, by moving the mouse quickly enough, to start dragging an unselected element instead. While it doesn’t cause any major problems, it just looks a bit odd.

This recipe’s code is partly based on a post by Fredrik Lundh, http://mail.python.org/pipermail/python-list/1999-May/002501.html.

See Also

Information about Tkinter can be obtained from a variety of sources, such as Fredrik Lundh, An Introduction to Tkinter (PythonWare: http://www.pythonware.com/library), New Mexico Tech’s Tkinter Reference (http://www.nmt.edu/tcc/help/lang/python/docs.html), Python in a Nutshell, and various other books.

11.5. Entering Accented Characters in Tkinter Widgets

Credit: Artur de Sousa Rocha

Problem

You want your application to allow the user to easily enter accented characters into Tkinter widgets even from a U.S.-layout keyboard.

Solution

Internationalized applications should enable the user to easily enter letters with accents and diacritics (e.g., umlauts, and tildes) even from a U.S.-layout keyboard. A usefully uniform convention is the following: hitting Ctrl-accent, for any kind of accent or diacritic, acts as a dead key, ensuring that the next letter hit will be decorated by that accent or diacritic. For example, Ctrl-apostrophe, followed by a, enters an a with an acute accent (the character á). The following classes provide the keyboard and widget bindings that allow this internationalized input functionality:

from Tkinter import *
from ScrolledText import ScrolledText
from unicodedata import lookup
import os
class Diacritical(object):
    """ Mixin class that adds keyboard bindings for accented characters, plus
        other common functionality (e.g.: Control-A == 'select all' on Windows).
    """
    if os.name == "nt":
        stroke = '/'
    else:
        stroke = 'minus'
    accents = (('acute', "'"), ('grave', '`'), ('circumflex', '^'),
               ('tilde', '='), ('diaeresis', '"'), ('cedilla', ','),
               ('stroke', stroke))
    def _ _init_ _(self):
        # Fix some non-Windows-like Tk bindings, if we're on Windows
        if os.name == 'nt':
            self.bind("<Control-Key-a>", self.select_all)
            self.bind("<Control-Key-/>", lambda event: "break")
        # Diacritical bindings
        for a, k in self.accents:
            self.bind("<Control-Key-%s><Key>" % k,
                        lambda event, a=a: self.insert_accented(event.char, a))
    def insert_accented(self, c, accent):
        if c.isalpha( ):
            if c.isupper( ):
                cap = 'capital'
            else:
                cap = 'small'
            try:
                c = lookup("latin %s letter %c with %s" % (cap, c, accent))
                self.insert(INSERT, c)
                return "break"
            except KeyError, e:
                pass
class DiacriticalEntry(Entry, Diacritical):
    """ Tkinter Entry widget with some extra key bindings for
        entering typical Unicode characters - with umlauts, accents, etc. """
    def _ _init_ _(self, master=None, **kwargs):
        Entry._ _init_ _(self, master=None, **kwargs)
        Diacritical._ _init_ _(self)
    def select_all(self, event=None):
        self.selection_range(0, END)
        return "break"
class DiacriticalText(ScrolledText, Diacritical):
    """ Tkinter ScrolledText widget with some extra key bindings for
        entering typical Unicode characters - with umlauts, accents, etc. """
    def _ _init_ _(self, master=None, **kwargs):
        ScrolledText._ _init_ _(self, master=None, **kwargs)
        Diacritical._ _init_ _(self)
    def select_all(self, event=None):
        self.tag_add(SEL, "1.0", "end-1c")
        self.mark_set(INSERT, "1.0")
        self.see(INSERT)
        return "break"

Discussion

Here is an example of use of these widget classes. We present the example, as usual, with a guard of if _ _name_ _ == '_ _main_ _'; so we can make it part of the module containing the classes and have it run when the module is executed as a “main script”:

def test( ):
    frame = Frame( )
    frame.pack(fill=BOTH, expand=YES)
    if os.name == "nt":
        frame.option_add("*font", "Tahoma 8")    # Win default, Tk uses other
    # The editors
    entry = DiacriticalEntry(frame)
    entry.pack(fill=BOTH, expand=YES)
    text = DiacriticalText(frame, width=76, height=25, wrap=WORD)
    if os.name == "nt":
        text.config(font="Arial 10")
    text.pack(fill=BOTH, expand=YES)
    text.focus( )
    frame.master.title("Diacritical Editor")
    frame.mainloop( )
if _ _name_ _ == "_ _main_ _":
    test( )

You might want to remove the keyboard event settings that don’t really have much to do with accents and diacritics, (e.g., Ctrl-A, meaning “select all”) to some other, separate mixin class. I keep that functionality together with the actual handling of diacritics basically because I always need both features anyway.

Some design choices might be altered, such as my decision to have Ctrl-equal as the way to enter a tilde. I took that path because I just couldn’t find a way to make Ctrl-~ work the right way, at least on my Windows machine! Also, depending on which languages you need to support, you might have to add other accents and diacritics, such as a-ring for Swedish, German scharfes-s, Icelandic eth and thorn, and so forth.

See Also

Docs about the unicodedata module in the Library Reference and Python in a Nutshell; information about Tkinter can be obtained from a variety of sources, such as Pythonware’s An Introduction to Tkinter, by Fredrik Lundh (http://www.pythonware.com/library), New Mexico Tech’s Tkinter Reference (http://www.nmt.edu/tcc/help/lang/python/docs.html), Python in a Nutshell, and various other books.

11.6. Embedding Inline GIFs Using Tkinter

Credit: Brent Burley

Problem

You need to embed GIF images inside your source code—for use in Tkinter buttons, labels, and so on—to make toolbars and the like without worrying about installing the right icon files.

Solution

A lively Tkinter GUI can include many small images. However, you don’t want to require that a small GIF file be present for each of these images. Ensuring the presence of many small files is a bother, and if they’re missing, your GUI may be unusable. Fortunately, you can construct Tkinter PhotoImage objects with inline data. It’s easy to convert a GIF to inline form as Python source code, with a little script or snippet that you can save and run separately.

import base64
print "icon='''\
" + base64.encodestring(open("icon.gif").read( )) + "'''"

This emits to standard output a lot of strange-looking “text”, which you can capture (typically using your shell’s facilities for output redirection, or with copy and paste) and split into lines of reasonable length:

icon='''R0lGODdhFQAVAPMAAAQ2PESapISCBASCBMTCxPxmNCQiJJya/ISChGRmzPz+/PxmzDQyZ
DQyZDQyZDQyZCwAAAAAFQAVAAAElJDISau9Vh2WMD0gqHHelJwnsXVloqDd2hrMm8pYYiSHYfMMRm
53ULlQHGFFx1MZCciUiVOsPmEkKNVp3UBhJ4Ohy1UxerSgJGZMMBbcBACQlVhRiHvaUsXHgywTdyc
LdxyB gm1vcTyIZW4MeU6NgQEBXEGRcQcIlwQIAwEHoioCAgWmCZ0Iq5+hA6wIpqislgGhthEAOw==
'''

Now, you can use this Python-inlined data in Tkinter:

import Tkinter
if _ _name_ _ == '_ _main_ _':
    root = Tkinter.Tk( )
    iconImage =Tkinter.PhotoImage(master=root, data=icon)
    Tkinter.Button(image=iconImage).pack( )

Discussion

The basic technique is to encode the GIF with the standard Python module base64 and store the results as a string literal in the Python code. At runtime, the Python code passes that string object to Tkinter’s PhotoImage. The current release of PhotoImage supports GIF and PPM, but inline data is supported only for GIF. To convert between image formats, see Recipe 11.7. Of course, you can use file='filename', instead of data=string, for either GIF or PPM, if your image data is indeed in a file.

You must keep a reference to the PhotoImage object yourself; that reference is not kept by the Tkinter widget. If you pass the object to Button and forget it, you will become frustrated! Here’s an easy workaround for this minor annoyance:

def makeImageWidget(icondata, *args, **kwds):
    if args:
        klass = args.pop(0)
    else:
        klass = Tkinter.Button
    class Widget(klass):
        def _ _init_ _(self, image, *args, **kwds):
            kwds['image'] = image
            klass._ _init_ _(self, *args, **kwds)
            self._ _image = image
    return Widget(Tkinter.PhotoImage(data=icondata), *args, **kwds)

Using this handy makeImageWidget function, the equivalent of the example in the recipe becomes:

makeImageWidget(icon).pack( )

The master argument on PhotoImage is optional; it defaults to the default application window. If you create a new application window (by calling Tk again), you must create your images in that context and supply the master argument, so the makeImageWidget function has to be updated to let you optionally pass the master argument to the PhotoImage constructor. However, most applications do not require this refinement.

See Also

Information about Tkinter can be obtained from a variety of sources, such as Fredrik Lundh, An Introduction to Tkinter (PythonWare: http://www.pythonware.com/library), New Mexico Tech’s Tkinter Reference (http://www.nmt.edu/tcc/help/lang/python/docs.html), Python in a Nutshell, and various other books.

11.7. Converting Among Image Formats

Credit: Doug Blanding

Problem

Your image files are in various formats (GIF, JPG, PNG, TIF, BMP), and you need to convert among these formats.

Solution

The Python Imaging Library (PIL) can read and write all of these formats; indeed, net of user-interface concerns, image-file format conversion using PIL boils down to a one-liner:

    Image.open(infile).save(outfile)

where filenames infile and outfile have the appropriate file extensions to indicate what kind of images we’re reading and writing. We just need to wrap a small GUI around this one-liner functionality—for example:

#!/usr/bin/env python
import os, os.path, sys
from Tkinter import *
from tkFileDialog import *
import Image
openfile = '' # full pathname: dir(abs) + root + ext
indir = ''
outdir = ''
def getinfilename( ):
    global openfile, indir
    ftypes=(('Gif Images', '*.gif'),
            ('Jpeg Images', '*.jpg'),
            ('Png Images', '*.png'),
            ('Tiff Images', '*.tif'),
            ('Bitmap Images', '*.bmp'),
            ("All files", "*"))
    if indir:
        openfile = askopenfilename(initialdir=indir, filetypes=ftypes)
    else:
        openfile = askopenfilename(filetypes=ftypes)
    if openfile:
        indir = os.path.dirname(openfile)
def getoutdirname( ):
    global indir, outdir
    if openfile:
        indir = os.path.dirname(openfile)
        outfile = asksaveasfilename(initialdir=indir, initialfile='foo')
    else:
        outfile = asksaveasfilename(initialfile='foo')
    outdir = os.path.dirname(outfile)
def save(infile, outfile):
    if infile != outfile:
        try:
            Image.open(infile).save(outfile)
        except IOError:
            print "Cannot convert", infile
def convert( ):
    newext = frmt.get( )
    path, file = os.path.split(openfile)
    base, ext = os.path.splitext(file)
    if var.get( ):
        ls = os.listdir(indir)
        filelist = [  ]
        for f in ls:
            if os.path.splitext(f)[1] == ext:
                filelist.append(f)
    else:
        filelist = [file]
    for f in filelist:
        infile = os.path.join(indir, f)
        ofile = os.path.join(outdir, f)
        outfile = os.path.splitext(ofile)[0] + newext
        save(infile, outfile)
    win = Toplevel(root)
    Button(win, text='Done', command=win.destroy).pack( )
# Divide GUI into 3 frames: top, mid, bot
root = Tk( )
root.title('Image Converter')
topframe = Frame(root, borderwidth=2, relief=GROOVE)
topframe.pack(padx=2, pady=2)
Button(topframe, text='Select image to convert',
       command=getinfilename).pack(side=TOP, pady=4)
multitext = "Convert all image files
(of this format) in this folder?"
var = IntVar( )
chk = Checkbutton(topframe, text=multitext, variable=var).pack(pady=2)
Button(topframe, text='Select save location',
       command=getoutdirname).pack(side=BOTTOM, pady=4)
midframe = Frame(root, borderwidth=2, relief=GROOVE)
midframe.pack(padx=2, pady=2)
Label(midframe, text="New Format:").pack(side=LEFT)
frmt = StringVar( )
formats = ['.bmp', '.gif', '.jpg', '.png', '.tif']
for item in formats:
    Radiobutton(midframe, text=item, variable=frmt, value=item).pack(anchor=NW)
botframe = Frame(root)
botframe.pack( )
Button(botframe, text='Convert', command=convert).pack(
       side=LEFT, padx=5, pady=5)
Button(botframe, text='Quit', command=root.quit).pack(
       side=RIGHT, padx=5, pady=5)
root.mainloop( )

Needing 80 lines of GUI code to wrap a single line of real functionality may be a bit extreme, but it’s not all that far out of line in my experience with GUI coding ;-).

Discussion

I needed this tool when I was making .avi files from the CAD application program I generally use. That CAD program emits images in .bmp format, but the AVI[1]-generating program I normally use requires images in .jpg format. Now, thanks to the little script in this recipe (and to the power of Python, Tkinter, and most especially PIL), with a couple of clicks, I get a folder full of images in .jpg format ready to be assembled into an AVI file, or, just as easily, files in .gif ready to be assembled into an animated GIF image file.

I used to perform this kind of task with simple shell scripts on Unix, using ImageMagick’s convert command. But, with this script, I can do exactly the same job just as easily on all sorts of machines, be they Unix, Windows, or Macintosh.

I had to work around one annoying problem to make this script work as I wanted it to. When I’m selecting the location into which a new file is to be written, I need that dialog to give me the option to create a new directory for that purpose. However, on Windows NT, the Browse for Folder dialog doesn’t allow me to create a new folder, only to choose among existing ones! My workaround, as you’ll see by studying this recipe’s Solution, was to use instead the Save As dialog. That dialog does allow me to create a new folder. I do have to indicate the dummy file in that folder, and the file gets ignored; only the directory part is kept. This workaround is not maximally elegant, but it took just a few minutes and almost no work on my part, and I can live with the result.

See Also

Information about Tkinter can be obtained from a variety of sources, such as Fredrik Lundh, An Introduction to Tkinter, (PythonWare: http://www.pythonware.com/library), New Mexico Tech’s Tkinter Reference (http://www.nmt.edu/tcc/help/lang/python/docs.html), Python in a Nutshell, and various other books; PIL is at http://www.pythonware.com/products/pil/.

11.8. Implementing a Stopwatch in Tkinter

Credit: JØrgen Cederberg, Tobias Klausmann

Problem

You are coding an application in Tkinter and need a widget that implements a stopwatch.

Solution

Implementing a new widget is almost always best done by subclassing Frame:

from Tkinter import *
import time
class StopWatch(Frame):
    """ Implements a stop watch frame widget. """
    msec = 50
    def _ _init_ _(self, parent=None, **kw):
        Frame._ _init_ _(self, parent, kw)
        self._start = 0.0
        self._elapsedtime = 0.0
        self._running = False
        self.timestr = StringVar( )
        self.makeWidgets( )
    def makeWidgets(self):
        """ Make the time label. """
        l = Label(self, textvariable=self.timestr)
        self._setTime(self._elapsedtime)
        l.pack(fill=X, expand=NO, pady=2, padx=2)
    def _update(self):
        """ Update the label with elapsed time. """
        self._elapsedtime = time.time( ) - self._start
        self._setTime(self._elapsedtime)
        self._timer = self.after(self.msec, self._update)
    def _setTime(self, elap):
        """ Set the time string to Minutes:Seconds:Hundredths """
        minutes = int(elap/60)
        seconds = int(elap - minutes*60.0)
        hseconds = int((elap - minutes*60.0 - seconds)*100)
        self.timestr.set('%02d:%02d:%02d' % (minutes, seconds, hseconds))
    def Start(self):
        """ Start the stopwatch, ignore if already running. """
        if not self._running:
            self._start = time.time( ) - self._elapsedtime
            self._update( )
            self._running = True
    def Stop(self):
        """ Stop the stopwatch, ignore if already stopped. """
        if self._running:
            self.after_cancel(self._timer)
            self._elapsedtime = time.time( ) - self._start
            self._setTime(self._elapsedtime)
            self._running = False
    def Reset(self):
        """ Reset the stopwatch. """
        self._start = time.time( )
        self._elapsedtime = 0.0
        self._setTime(self._elapsedtime)

Discussion

Here is an example of use of this StopWatch widget, presented, as usual, with a guard of if _ _name_ _ == '_ _main_ _' so we can make it part of the module containing the class and have it run when the module is executed as a “main script”:

if _ _name_ _ == '_ _main_ _':
    def main( ):
        root = Tk( )
        sw = StopWatch(root)
        sw.pack(side=TOP)
        Button(root, text='Start', command=sw.Start).pack(side=LEFT)
        Button(root, text='Stop', command=sw.Stop).pack(side=LEFT)
        Button(root, text='Reset', command=sw.Reset).pack(side=LEFT)
        Button(root, text='Quit', command=root.quit).pack(side=LEFT)
        root.mainloop( )
    main( )

You might want to use time.clock instead of time.time if your stopwatch’s purpose is to measure the amount of CPU time that your program is taking, rather than the amount of elapsed time. I used time.time, without even bothering to make that choice easily customizable (you’ll need to edit its several appearances in the recipe’s code), because it seems the most natural choice to me by far. One aspect that you can customize easily, by subclassing and data overriding or simply by setting the msec instance attribute on a particular StopWatch instance, is how often the time display is updated onscreen; the default of 50 milliseconds, which translates to 20 updates a second, may well mean updates that are too frequent for your purposes, although they suit my own just fine.

See Also

Docs about the time module in the Library Reference and Python in a Nutshell; information about Tkinter can be obtained from a variety of sources, such as Fredrik Lundh, An Introduction to Tkinter (PythonWare: http://www.pythonware.com/library), New Mexico Tech’s Tkinter Reference (http://www.nmt.edu/tcc/help/lang/python/docs.html), Python in a Nutshell, and various other books.

11.9. Combining GUIs and Asynchronous I/Owith Threads

Credit: Jacob Hallén, Laura Creighton, Boudewijn Rempt

Problem

You need to access sockets, serial ports, or other asynchronous (but blocking) I/O sources, while running a GUI.

Solution

The solution is to handle a GUI interface on one thread and communicate to it (via Queue instances) the events on I/O channels handled by other threads. Here’s the code for the standard Tkinter GUI toolkit that comes with Python:

import Tkinter, time, threading, random, Queue
class GuiPart(object):
    def _ _init_ _(self, master, queue, endCommand):
        self.queue = queue
        # Set up the GUI
        Tkinter.Button(master, text='Done', command=endCommand).pack( )
        # Add more GUI stuff here depending on your specific needs
    def processIncoming(self):
        """ Handle all messages currently in the queue, if any. """
        while self.queue.qsize( ):
            try:msg = self.queue.get(0)
                # Check contents of message and do whatever is needed. As a
                # simple example, let's print it (in real life, you would
                # suitably update the GUI's display in a richer fashion).
                print msg
            except Queue.Empty:
                # just on general principles, although we don't expect this
                # branch to be taken in this case, ignore this exception!
                pass
class ThreadedClient(object):
    """
    Launch the "main" part of the GUI and the worker thread.  periodicCall and
    endApplication could reside in the GUI part, but putting them here
    means that you have all the thread controls in a single place.
    """
    def _ _init_ _(self, master):
        """
        Start the GUI and the asynchronous threads.  We are in the "main"
        (original) thread of the application, which will later be used by
        the GUI as well.  We spawn a new thread for the worker (I/O).
        """
        self.master = master
        # Create the queue
        self.queue = Queue.Queue( )
        # Set up the GUI part
        self.gui = GuiPart(master, self.queue, self.endApplication)
        # Set up the thread to do asynchronous I/O
        # More threads can also be created and used, if necessary
        self.running = True
        self.thread1 = threading.Thread(target=self.workerThread1)
        self.thread1.start( )
        # Start the periodic call in the GUI to check the queue
        self.periodicCall( )
    def periodicCall(self):
        """ Check every 200 ms if there is something new in the queue. """
        self.master.after(200, self.periodicCall)
        self.gui.processIncoming( )
        if not self.running:
            # This is the brutal stop of the system.  You may want to do
            # some cleanup before actually shutting it down.
            import sys
            sys.exit(1)
    def workerThread1(self):
        """
        This is where we handle the asynchronous I/O.  For example, it may be
        a 'select( )'.  One important thing to remember is that the thread has
        to yield control pretty regularly, be it by select or otherwise.
        """
        while self.running:
            # To simulate asynchronous I/O, create a random number at random
            # intervals. Replace the following two lines with the real thing.
            time.sleep(rand.random( ) * 1.5)
            msg = rand.random( )
            self.queue.put(msg)
    def endApplication(self):
        self.running = False
rand = random.Random( )
root = Tkinter.Tk( )
client = ThreadedClient(root)
root.mainloop( )

Discussion

This recipe demonstrates the easiest way of handling access to sockets, serial ports, and other asynchronous I/O ports while running a Tkinter-based GUI. The recipe’s principles generalize to other GUI toolkits, since most toolkits make it preferable to access the GUI itself from a single thread, and all offer a toolkit-dependent way to set up periodic polling as this recipe does.

Tkinter, like most other GUIs, is best used with all graphic commands in a single thread. On the other hand, it’s far more efficient to make I/O channels block, then wait for something to happen, rather than using nonblocking I/O and having to poll at regular intervals. The latter approach may not even be available in some cases, since not all data sources support nonblocking I/O. Therefore, for generality as well as for efficiency, we should handle I/O with a separate thread, or more than one. The I/O threads can communicate in a safe way with the “main”, GUI-handling thread through one or more Queues. In this recipe, the GUI thread still has to do some polling (on the Queues), to check whether something in the Queue needs to be processed. Other architectures are possible, but they are much more complex than the one in this recipe. My advice is to start with this recipe, which will handle your needs over 90% of the time, and explore the much more complex alternatives only if it turns out that this approach cannot meet your performance requirements.

This recipe lets a worker thread block in a select (simulated by random sleeps in the recipe’s example worker thread). Whenever something arrives, it is received and inserted in a Queue instance. The main (GUI) thread polls the Queue five times per second and processes all messages that have arrived since it last checked. (Polling 5 times per second is frequent enough that the end user will not notice any significant delay but infrequent enough that the computational load on the computer will be negligible.) You may want to fine-tune this feature, depending on your needs.

This recipe solves a common problem that is frequently asked about on Python mailing lists and newsgroups. Other solutions, involving synchronization between threads, help you solve such problems without polling (the self.master.after call in the recipe). Unfortunately, such solutions are generally complicated and messy, since you tend to raise and wait for semaphores throughout your code. In any case, a GUI already has several polling mechanisms built into it (the “main” event loop), so adding one more won’t make much difference, especially since it seldom runs. The code has been tested in depth only under Linux, but it should work on any platform with working threads, including Windows.

Here is a PyQt equivalent, with very minor variations:

import sys, time, threading, random, Queue, qt
class GuiPart(qt.QMainWindow):
    def _ _init_ _(self, queue, endcommand, *args):
        qt.QMainWindow._ _init_ _(self, *args)
        self.queue = queue
        # We show the result of the thread in the gui, instead of the console
        self.editor = qt.QMultiLineEdit(self)
        self.setCentralWidget(self.editor)
        self.endcommand = endcommand
    def closeEvent(self, ev):
        """ We just call the endcommand when the window is closed,
            instead of presenting a button for that purpose.  """
        self.endcommand( )
    def processIncoming(self):
        """ Handle all the messages currently in the queue (if any). """
        while self.queue.qsize( ):
            try:
                msg = self.queue.get(0)
                self.editor.insertLine(str(msg))
            except Queue.Empty:
                pass
class ThreadedClient(object):
    """
    Launch the "main" part of the GUI and the worker thread.  periodicCall and
    endApplication could reside in the GUI part, but putting them here
    means that you have all the thread controls in a single place.
    """
    def _ _init_ _(self):
        # Create the queue
        self.queue = Queue.Queue( )
        # Set up the GUI part
        self.gui = GuiPart(self.queue, self.endApplication)
        self.gui.show( )
        # A timer to periodically call periodicCall
        self.timer = qt.QTimer( )
        qt.QObject.connect(self.timer, qt.SIGNAL("timeout( )"),
                           self.periodicCall)
        # Start the timer -- this replaces the initial call to periodicCall
        self.timer.start(200)
        # Set up the thread to do asynchronous I/O
        # More can be made if necessary
        self.running = True
            self.thread1 = threading.Thread(target=self.workerThread1)
        self.thread1.start( )
    def periodicCall(self):
        """
        Check every 200 ms if there is something new in the queue.
        """
        self.gui.processIncoming( )
        if not self.running:
            root.quit( )
    def endApplication(self):
        self.running = False
    def workerThread1(self):
        """
        This is where we handle the asynchronous I/O.  For example, it may be
        a 'select( )'.  An important thing to remember is that the thread has
        to yield control once in a while.
        """
        while self.running:
            # To simulate asynchronous I/O, we create a random number at
            # random intervals.  Replace the following 2 lines with the real
            # thing.
            time.sleep(rand.random( ) * 0.3)
            msg = rand.random( )
            self.queue.put(msg)
rand = random.Random( )
root = qt.QApplication(sys.argv)
client = ThreadedClient( )
root.exec_loop( )

As you can see, this PyQt variation has a structure that’s uncannily similar to the Tkinter version, with just a few variations (and a few enhancements, such as using QApplication.quit instead of the more brutal sys.exit, and displaying the thread’s result in the GUI itself rather than on the console).

See Also

Documentation of the standard library modules threading and Queue in the Library Reference and Python in a Nutshell; information about Tkinter can be obtained from a variety of sources, such as Fredrik Lundh, An Introduction to Tkinter (Pythonware: http://www.pythonware.com/library), New Mexico Tech’s Tkinter Reference (http://www.nmt.edu/tcc/help/lang/python/docs.html), Python in a Nutshell, and various other books; information about PyQt can be found at PyQt’s own web site, http://www.riverbankcomputing.co.uk/pyqt/index.php.

11.10. Using IDLE’s Tree Widget in Tkinter

Credit: Sanghyeon Seo

Problem

You need to use a Tree widget in your Tkinter application, and you know that such a widget comes with IDLE, the Integrated Development Environment that comes with Python.

Solution

IDLE’s functionality is available in the Python Standard Library in package idlelib, so it is easy to import and use in your applications. The Tree widget is in idlelib.TreeWidget. Here, as an example, is how to use that widget to display an XML document’s DOM as a tree:

from Tkinter import Tk, Canvas
from xml.dom.minidom import parseString
from idlelib.TreeWidget import TreeItem, TreeNode
class DomTreeItem(TreeItem):
    def _ _init_ _(self, node):
        self.node = node
    def GetText(self):
        node = self.node
        if node.nodeType == node.ELEMENT_NODE:
            return node.nodeName
        elif node.nodeType == node.TEXT_NODE:
            return node.nodeValue
    def IsExpandable(self):
        node = self.node
        return node.hasChildNodes( )
    def GetSubList(self):
        parent = self.node
        children = parent.childNodes
        prelist = [DomTreeItem(node) for node in children]
        itemlist = [item for item in prelist if item.GetText( ).strip( )]
        return itemlist
if _ _name_ _ == '_ _main_ _':
    example_data = '''
    <A>
     <B>
      <C>d</C>
      <C>e</C>
     </B>
     <B>
      <C>f</C>
     </B>
    </A>
    '''
    root = Tk( )
    canvas = Canvas(root)
    canvas.config(bg='white')
    canvas.pack( )
    dom = parseString(data)
    item = DomTreeItem(dom.documentElement)
    node = TreeNode(canvas, None, item)
    node.update( )
    node.expand( )
    root.mainloop( )

Discussion

My applications needed Tree widgets, and Tkinter does not have such a widget among its built-in ones. So I started looking around the Internet to see the Tree widgets that had been implemented for Tkinter. After a while, I was pleasantly surprised to learn that quite a useful one was already installed and working on my computer! Specifically, I had IDLE, the free Integrated DeveLopment Environment that comes with Python, and therefore I had idlelib, the package within the standard Python library that contains just about all of the functionality of IDLE. A Tree widget is among the widgets that IDLE uses for its own GUI, so idlelib.TreeWidget is just sitting there in the standard Python library, quite usable and useful.

The only problem with idlelib is that it is not well documented as a part of the Python Standard Library documentation, nor elsewhere. The best documentation I could find is the pydoc-generated one at http://pydoc.org/2.3/idlelib.html. TreeWidget is one of the modules documented there. I suggest reading the sources on your disk, which include the docstrings that pydoc is using to build the useful documentation site. Between sources and pydoc, it is quite possible to reuse some of the rich functionality that’s included in idlelib, although having real docs about it would definitely not hurt. Python is known as the language that comes “with batteries included.” When you consider, not just the hundreds of library modules that are fully documented in Python’s official docs, but also the many additional library modules that aren’t (such as those in idlelib), it’s hard to deny this characterization.

This recipe shows how to implement a simple GUI Tree: define a node-item class by subclassing idlelib.TreeWidget.TreeItem, and override some methods. You may want to override ten methods (http://pydoc.org/2.3/idlelib.TreeWidget.html#TreeItem has the complete list), and this recipe only needs three: GetText to define how the item is displayed (textually), IsExpandable to tell the Tree whether to put a clickable + character next to the node to allow expansion, GetSubList to return a list of children items in case expansion is required. Other optional methods, which this recipe does not need, allow iconic rather than textual display, double-clicking on nodes, and even editing of Tree items.

See Also

idlelib docs at http://pydoc.org/2.3/idlelib.html.

11.11. Supporting Multiple Values per Row in a Tkinter Listbox

Credit: Brent Burley, Pedro Werneck, Eric Rose

Problem

You need a Tkinter widget that works just like a normal Listbox but with multiple values per row.

Solution

When you find a functional limitation in Tkinter, most often the best solution is to build your own widget as a Python class, subclassing an appropriate existing Tkinter widget (often Frame, so you can easily aggregate several native Tkinter widgets into your own compound widget) and extending and tweaking the widget’s functionality as necessary. Rather than solving a problem for just one application, this approach gives you a component that you can reuse in many applications. For example, here’s a way to make a multicolumn equivalent of a Tkinter Listbox:

from Tkinter import *
class MultiListbox(Frame):
    def _ _init_ _(self, master, lists):
        Frame._ _init_ _(self, master)
        self.lists = [  ]
        for l, w in lists:
            frame = Frame(self)
            frame.pack(side=LEFT, expand=YES, fill=BOTH)
            Label(frame, text=l, borderwidth=1, relief=RAISED).pack(fill=X)
            lb = Listbox(frame, width=w, borderwidth=0, selectborderwidth=0,
                         relief=FLAT, exportselection=FALSE)
            lb.pack(expand=YES, fill=BOTH)
            self.lists.append(lb)
            lb.bind('<B1-Motion>', lambda e, s=self: s._select(e.y))
            lb.bind('<Button-1>', lambda e, s=self: s._select(e.y))
            lb.bind('<Leave>', lambda e: 'break')
            lb.bind('<B2-Motion>', lambda e, s=self: s._b2motion(e.x, e.y))
            lb.bind('<Button-2>', lambda e, s=self: s._button2(e.x, e.y))
        frame = Frame(self)
        frame.pack(side=LEFT, fill=Y)
        Label(frame, borderwidth=1, relief=RAISED).pack(fill=X)
        sb = Scrollbar(frame, orient=VERTICAL, command=self._scroll)
        sb.pack(expand=YES, fill=Y)
        self.lists[0]['yscrollcommand'] = sb.set
    def _select(self, y):
        row = self.lists[0].nearest(y)
        self.selection_clear(0, END)
        self.selection_set(row)
        return 'break'
    def _button2(self, x, y):
        for l in self.lists:
            l.scan_mark(x, y)
        return 'break'
    def _b2motion(self, x, y):
        for l in self.lists
            l.scan_dragto(x, y)
        return 'break'
    def _scroll(self, *args):
        for l in self.lists:
            apply(l.yview, args)
        return 'break'
    def curselection(self):
        return self.lists[0].curselection( )
    def delete(self, first, last=None):
        for l in self.lists:
            l.delete(first, last)
    def get(self, first, last=None):
        result = [  ]
        for l in self.lists:
            result.append(l.get(first,last))
        if last: return apply(map, [None] + result)
        return result
    def index(self, index):
        self.lists[0].index(index)
    def insert(self, index, *elements):
        for e in elements:
            i = 0
            for l in self.lists:
                l.insert(index, e[i])
                i = i + 1
    def size(self):
        return self.lists[0].size( )
    def see(self, index):
        for l in self.lists:
            l.see(index)
    def selection_anchor(self, index):
        for l in self.lists:
            l.selection_anchor(index)
    def selection_clear(self, first, last=None):
        for l in self.lists:
            l.selection_clear(first, last)
    def selection_includes(self, index):
        return self.lists[0].selection_includes(index)
    def selection_set(self, first, last=None):
        for l in self.lists:
            l.selection_set(first, last)
if _ _name_ _ == '_ _main_ _':
    tk = Tk( )
    Label(tk, text='MultiListbox').pack( )
    mlb = MultiListbox(tk, (('Subject', 40), ('Sender', 20), ('Date', 10)))
    for i in range(1000):
      mlb.insert(END, 
          ('Important Message: %d' % i, 'John Doe', '10/10/%04d' % (1900+i)))
    mlb.pack(expand=YES, fill=BOTH)
    tk.mainloop( )

Discussion

This recipe shows a compound widget that gangs multiple Tk Listbox widgets to a single scrollbar to achieve a simple multicolumn scrolled listbox. Most of the Listbox API is mirrored, to make the widget act like normal Listbox, but with multiple values per row. The resulting widget is lightweight, fast, and easy to use. The main drawback is that only text is supported, which is a fundamental limitation of the underlying Listbox widget.

In this recipe’s implementation, only single selection is allowed, but the same idea could be extended to multiple selection. User-resizable columns and auto-sorting by clicking on the column label should also be possible. Auto-scrolling while dragging Button-1 was disabled because it broke the synchronization between the lists. However, scrolling with Button-2 works fine. Mice with scroll wheels appear to behave in different ways depending on the platform. For example, while things appear to work fine with the preceding code on some platforms (such as Windows/XP), on other platforms using X11 (such as Linux), I’ve observed that mouse scroll wheel events correspond to Button-4 and Button-5, so you could deal with them just by adding at the end of the for loop in method _ _init_ _ the following two statements:

    lb.bind('<Button-4>', lambda e, s=self: s._scroll(SCROLL, -1, UNITS))
    lb.bind('<Button-5>', lambda e, s=self: s._scroll(SCROLL, +1, UNITS))

This addition should be innocuous on platforms such as Windows/XP. You should check this issue on all platforms on which you need to support mouse scroll wheels.

If you need to support sorting by column-header clicking, you can obtain the hook needed for that functionality with a fairly modest change to this recipe’s code. Specifically, within the for loop in method _ _init_ _, you can change the current start:

        for l, w in lists:
            frame = Frame(self)
            frame.pack(side=LEFT, expand=YES, fill=BOTH)
            Label(frame, text=l, borderwidth=1, relief=RAISED).pack(fill=X)

to the following richer code:

        for l, w, sort_command in lists:
            frame = Frame(self)
            frame.pack(side=LEFT, expand=YES, fill=BOTH)
            Button(frame, text=l, borderwidth=1, relief=RAISED,
                   command=sort_command).pack(fill=X)

To take advantage of this hook, you then need to pass as the lists' argument, rather than one tuple of pairs, a list of three tuples, the third item of each tuple being an object callable with no arguments to perform the appropriate kind of sorting. In my applications, I’ve generally found this specific refinement to be more trouble than it’s worth, but I’m presenting it anyway (although not in the actual “Solution” of this recipe!) just in case your applications differ in this respect. Maybe sorting by column header clicking is something that’s absolutely invaluable to you.

One note about the implementation: in the MultiListbox._ _init_ _ method, several lambda forms are used as the callable second arguments (callbacks) of the bind method calls on the contained Listbox widgets. This approach is traditional, but if you share the widespread dislike for lambda, you should know that lambda is never truly necessary. In this case, the easiest way to avoid the lambdas is to redefine all the relevant methods (_select, _button2, etc.) as taking two formal arguments (self, e) and extract the data they need from argument e. Then in the bind calls, you can simply pass the bound self._select method, and so on.

See Also

Information about Tkinter can be obtained from a variety of sources, such as Pythonware’s An Introduction to Tkinter, by Fredrik Lundh (http://www.pythonware.com/library), New Mexico Tech’s Tkinter Reference (http://www.nmt.edu/tcc/help/lang/python/docs.html), Python in a Nutshell, and various other books.

11.12. Copying Geometry Methods and Options Between Tkinter Widgets

Credit: Pedro Werneck

Problem

You want to create new Tkinter compound widgets, not by inheriting from Frame and packing other widgets inside, but rather by setting geometry methods and options from other widget to another.

Solution

Here is an example of a compound widget built by this approach:

from Tkinter import *
class LabeledEntry(Entry):
    """ An Entry widget with an attached Label """
    def _ _init_ _(self, master=None, **kw):
        ekw = {  }                               # Entry options dictionary
        fkw = {  }                               # Frame options dictionary
        lkw = {'name':'label'}                 # Label options dictionary
        skw = {'padx':0, 'pady':0, 'fill':'x', # Geometry manager opts dict
                'side':'left'}
        fmove = ('name',)                      # Opts to move to the Frame dict
        lmove = ('text', 'textvariable',
                 'anchor','bitmap', 'image')   # Opts to move to the Label dict
        smove = ('side', 'padx', 'pady',       # Opts to move to the Geometry
                 'fill')                       # manager dictionary
        # dispatch each option towards the appropriate component
        for k, v in kw:
            if k in fmove: fkw[k] = v
            elif k in lmove: lkw[k] = v
            elif k in smove: skw[k] = v
            else: ekw[k] = v
        # make all components with the accumulated options
        self.body = Frame(master, **fkw)
        self.label = Label(self.body, **lkw)
        self.label.pack(side='left', fill=skw['fill'],
                        padx=skw['padx'], pady=skw['pady'])
        Entry._ _init_ _(self, self.body, **ekw)
        self.pack(side=skw['side'], fill=skw['fill'],
                  padx=skw['padx'], pady=skw['pady'])
        methods = (Pack._ _dict_ _.keys( ) +  # Set Frame geometry methods to self
                   Grid._ _dict_ _.keys( ) +
                   Place._ _dict_ _.keys( ))
        for m in methods:
            if m[0] != '_' and m != 'config' and m != 'configure':
                setattr(self, m, getattr(self.body, m))

Discussion

Here is an example of use of this LabeledEntry widget, presented, as usual, with a guard of if _ _name_ _ == '_ _main_ _' so we can make it part of the module containing the class and have it run when the module is executed as a “main script”:

if _ _name_ _ == '_ _main_ _':
    root = Tk( )
    le1 = LabeledEntry(root, name='label1', text='Label 1: ',
                       width=5, relief=SUNKEN, bg='white', padx=3)
    le2 = LabeledEntry(root, name='label2', text='Label 2: ',
                       relief=SUNKEN, bg='red', padx=3)
    le3 = LabeledEntry(root, name='label3', text='Label 3: ',
                       width=40, relief=SUNKEN, bg='yellow', padx=3)
    le1.pack(expand=1, fill=X)
    le2.pack(expand=1, fill=X)
    le3.pack(expand=1, fill=X)
    root.mainloop( )

The usual approach to defining new compound Tkinter widgets is to inherit from Frame and pack your component widgets inside. While simple and habitual, that approach has a few problems. In particular, you need to invent, design, document, and implement additional methods or options to access the component widgets’ attributes from outside of the compound widget class. Using another alternative (which I’ve often seen done, but it’s still a practice that is not advisable at all!), you can violate encapsulation and Demeter’s Law by having other code access the component widgets directly. If you do violate encapsulation, you’ll pay for it in the not-so-long run, when you find a need to tweak your compound widget and discover that you can’t do it without breaking lots of code that depends on the compound widget’s internal structure. Those consequences are bad enough when you own all of the code in question, but it’s worse if you have “published” your widget and other people’s code depends on it.

This recipe shows it doesn’t have to be that bad, by elaborating upon an idea I first saw used in the ScrolledText widget, which deserves to be more widely exposed. Instead of inheriting from Frame, you inherit from the “main” widget of your new compound widget. Then, you create a Frame widget to be used as a body, pretty much like in the more usual approach. Then, and here comes the interesting novelty, you create dicts for each component widget you contain and move to those dictionaries the respective options that pertain to component widgets.

The novelty continues after you’ve packed the “main” widget: at that point, you can reset said widget’s geometry methods to the base Frame attributes (meaning, in this case, methods), so that accessing the object methods will in fact access the inner base Frame geometry methods. This transparent, seamless delegation by juggling bound methods is uniquely Pythonic and is part of what makes this recipe so novel and interesting!

The main advantage of this recipe’s approach is that you can create your widget with options to all slave widgets inside it in a single line, just like any other widget, instead of doing any further w.configure or w['option'] calls or accesses to set all details exactly the way you want them. To be honest, there is a potential disadvantage, too: in this recipe’s approach, it’s hard to handle options with the same name on different component widgets. However, sometimes you can handle them by renaming options: if two separate widgets need a 'foo' option that’s also of interest to the “main” widget, for example, use, 'upper_foo' and 'lower_foo' variants and rename them appropriately (with yet another auxiliary dictionary) at the same time you’re dispatching them to the appropriate dictionary of component-widget options. You can’t sensibly keep doing that “forever”, as the number of component widgets competing for the same option grows without bounds: if that happens, revert to the good old tried-and-true approach. But for nine out of ten compound widgets you find yourself programming, you’ll find this recipe’s approach to be an interesting alternative to the usual, traditional approach to compound-widget programming.

See Also

Information about Tkinter can be obtained from a variety of sources, such as Fredrik Lundh, An Introduction to Tkinter (PythonWare: http://www.pythonware.com/library), New Mexico Tech’s Tkinter Reference (http://www.nmt.edu/tcc/help/lang/python/docs.html), Python in a Nutshell, and various other books.

11.13. Implementing a Tabbed Notebook for Tkinter

Credit: Iuri Wickert

Problem

You have some Tkinter applications, each with a single top-level window, and want to organize them as panels in a tabbed notebook with minimal changes to your original applications’ source code.

Solution

A simple widget class can implement a notebook with all the features we need, including all possible orientations and the ability to add and switch frames (panels) at will:

from Tkinter import *
class notebook(object):
    def _ _init_ _(self, master, side=LEFT):
        self.active_fr = None
        self.count = 0
        self.choice = IntVar(0)
        if side in (TOP, BOTTOM): self.side = LEFT
        else: self.side = TOP
        self.rb_fr = Frame(master, borderwidth=2, relief=RIDGE)
        self.rb_fr.pack(side=side, fill=BOTH)
        self.screen_fr = Frame(master, borderwidth=2, relief=RIDGE)
        self.screen_fr.pack(fill=BOTH)
    def _ _call_ _(self):
        return self.screen_fr
    def add_screen(self, fr, title):
        b = Radiobutton(self.rb_fr, text=title, indicatoron=0,
                        variable=self.choice,
                        value=self.count, command=lambda: self.display(fr))
        b.pack(fill=BOTH, side=self.side)
        if not self.active_fr:
            fr.pack(fill=BOTH, expand=1)
            self.active_fr = fr
        self.count += 1
    def display(self, fr):
        self.active_fr.forget( )
        fr.pack(fill=BOTH, expand=1)
        self.active_fr = fr

Just save this code as a notebook.py module, somewhere on your Python sys.path, and you can import and use it in your apps.

Discussion

The simplest way to show how this notebook class works is with a simple demonstration program:

from Tkinter import *
from notebook import *
# make a toplevel with a notebook in it, with tabs on the left:
root = Tk( )
nb = notebook(root, LEFT)
# make a few diverse frames (panels), each using the NB as 'master':
f1 = Frame(nb( ))
b1 = Button(f1, text="Button 1")
e1 = Entry(f1)
# pack your widgets in the frame before adding the frame to the
# notebook, do NOT pack the frame itself!
b1.pack(fill=BOTH, expand=1)
e1.pack(fill=BOTH, expand=1)
f2 = Frame(nb( ))
b2 = Button(f2, text='Button 2')
b3 = Button(f2, text='Beep 2', command=Tk.bell)
b2.pack(fill=BOTH, expand=1)
b3.pack(fill=BOTH, expand=1)
f3 = Frame(nb( ))
# add the frames as notebook 'screens' and run this GUI app
nb.add_screen(f1, "Screen 1")
nb.add_screen(f2, "Screen 2")
nb.add_screen(f3, "dummy")
root.mainloop( )

Tkinter is a simple GUI toolkit, easy to use but notoriously feature-poor when compared to more advanced toolkits. And yet, sometimes advanced features are not all that difficult to add! I wondered how I could use a tabbed appearance, also known as a notebook, to organize various pages of an application, or various related applications, simply and elegantly. I discovered that simulating a notebook widget by using standard Tkinter frames and radio buttons was not only possible, but also quite simple and effective.

Tk has some “odd”, and somewhat unknown, corners, which make the whole task a snap. The indicatoron option on a radio button reverts the radio button default appearance back to the normal button look—a rectangle, which may not be a perfect-looking tab but is plenty good enough for me. Each Tkinter frame has a forget method, which allows easy and fast swapping of “screens” (notebook panels, application frames) within the single “screen frame” of the notebook object.

To convert any existing Tkinter app, based on a single top-level window, to run inside a notebook panel, all you need to do is to change the application master frame’s root, which is generally a top-level widget (an instance of Tkinter’s Tk class), to the one provided by the notebook object when you call it. (The three occurrences of nb( ) in the example code show how to go about it.)

The notebook implementations in other toolkits often have advanced features such as the ability to exclude (remove) some frames as well as adding others. I have not found this kind of thing to be necessary, and so I have taken no trouble in this recipe to make it possible: all references to the external frames are kept implicitly in lambda closures, without any obvious way to remove them. If you think you need the ability to remove frames, you might consider an alternative architecture: keep the frames’ references in a list, indexed by the binding variable of the radio buttons (i.e., the choice attribute of each radio button). Doing so lets you destroy a “frame” and its associated radio button in a reasonably clean way.

See Also

Information about Tkinter can be obtained from a variety of sources, such as Fredrik Lundh, An Introduction to Tkinter (PythonWare: http://www.pythonware.com/library), New Mexico Tech’s Tkinter Reference (http://www.nmt.edu/tcc/help/lang/python/docs.html), Python in a Nutshell, and various other books.

11.14. Using a wxPython Notebook with Panels

Credit: Mark Nenadov

Problem

You want to design a wxPython GUI comprised of multiple panels—each driven by a separate Python script running in the background—that let the user switch back and forth (i.e., a wxPython Notebook).

Solution

Notebooks are an effective GUI approach, as they let the user select the desired view from several options at any time with an instinctive button click. wxPython supports this feature by supplying a wxNotebook widget. Here is a “frame” class that holds a notebook and adds to it three panes, each driven by a different Python module (not shown) through a function in each module named runPanel:

from wxPython.wx import *
class MainFrame(wxFrame):
    #
    # snipped: mainframe class attributes
    #
    def _ _init_ _(self, parent, id, title):
        #
        # snipped: frame-specific initialization
        #
        # Create the notebook object
        self.nb = wxNotebook(self, -1,
            wxPoint(0,0), wxSize(0,0), wxNB_FIXEDWIDTH)
        # Populate the notebook with pages (panels), each driven by a
        # separate Python module which gets imported for the purpose:
        panel_names = "First Panel", "Second Panel", "The Third One"
        panel_scripts = "panel1", "panel2", "panel3"
        for name, script in zip(panel_names, panel_scripts):
            # Make panel named 'name' (driven by script 'script'.py)
            self.module = _ _import_ _(script, globals( ))
            self.window = self.module.runPanel(self, self.nb)
            if self.window: self.nb.AddPage(self.window, name)
        #
        # snipped: rest of frame initialization
        #

Discussion

wxPython provides a powerful notebook user-interface object, with multiple panels, each of which can be built and driven by a separate Python script (actually a module, not a “main script”). Each panel’s script runs in the background, even when the panel is not selected, and maintains state as the user switches back and forth.

This recipe isn’t a fully functional wxPython application, but it adequately demonstrates how to use notebooks and panels (which it loads by importing files). This recipe assumes that you have files named panel1.py, panel2.py, and panel3.py, each of which contains a runPanel function that takes two arguments (a wxFrame and a wxNotebook in the frame) and returns a wxPanel object.

The notebook-specific functionality is easy: the notebook object is created by the wxNotebook function, and an instance of this recipe’s MainFrame class saves its notebook object as the self.nb instance attribute. Then, each page (a wxPanel object), obtained by calling the separate script’s runPanel functions, is added to the notebook by calling the notebook’s AddPage method, with the page object as the first argument and a name string as the second. Your code only needs to make the notebook and its panels usable; the wxWidgets framework, as wrapped by the wxPython package, handles all the rest on your behalf.

See Also

wxPython, and the wxWidgets toolkit it depends on, are described in detail at http://www.wxPython.org and http://www.wxWidgets.org.

11.15. Implementing an ImageJ Plug-in in Jython

Credit: Ferdinand Jamitzky, Edoardo “Dado” Marcora

Problem

You perform image processing using the excellent free program ImageJ and need to extend it with your own plug-ins, but you want to code those plug-ins in Jython rather than in Java.

Solution

Jython can do all that Java can, but with Python’s elegance and high productivity. For example, here is an ImageJ plug-in that implements a simple image inverter:

import ij
class Inverter_py(ij.plugin.filter.PlugInFilter):
    def setup(self, arg, imp):
        """@sig public int setup(String arg, ij.ImagePlus imp)"""
        return ij.plugin.filter.PlugInFilter.DOES_8G
    def run(self,ip):
        """@sig public void run(ij.process.ImageProcessor ip)"""
        pixels = ip.getPixels( )
        width = ip.getWidth( )
        r = ip.getRoi( )
        for y in range(r.y, r.y+r.height):
            for x in range(r.x, r.x+r.width):
                i = y*width + x
                pixels[i] = 255-pixels[i]

Discussion

To make this plug-in usable from ImageJ, all you need to do is compile it into a Java bytecode class using the jythonc command with the appropriate command-line option switches. For example, I use IBM’s open source Java compiler, jikes, and I have placed it into the C:ImageJ directory, which also holds the plugins and jre subdirectories. So, in my case, the command line to use is:

# jythonc -w C:ImageJpluginsJython -C C:ImageJjikes
  -J "-bootclasspath C:ImageJjrelib
t.jar -nowarn"

If you use Sun’s Java SDK, or other Java implementations, you just change the -C argument, which indicates the path of your Java compiler and the -J argument, which specifies the options to pass to your Java compiler.

11.16. Viewing an Image from a URL with Swing and Jython

Credit: Joel Lawhead, Chuck Parker

Problem

You want to make a simple Swing image viewer, accepting the URL to an image and displaying the image in a Swing window.

Solution

Jython makes this task very easy:

from pawt import swing
from java import net
def view(url):
    frame = swing.JFrame("Image: " + url, visible=1)
    frame.getContentPane( ).add(swing.JLabel(swing.ImageIcon(net.URL(url))))
    frame.setSize(400,250)
    frame.show( )
if _ _name_ _ == '_ _main_ _':
    view("http://www.python.org/pics/pythonHi.gif")

Discussion

Swing’s JLabel and ImageIcon widgets can be easily combined in Jython to make a simple image viewer. The need to pass a URL to the view function is not at all a limitation, because you can always use the file: protocol in your URL if you want to display an image that lives on your filesystem rather than out there on the Web. Remember that the U in URL stands for Universal!

11.17. Getting User Input on Mac OS

Credit: Matteo Rattotti

Problem

You’re writing a simple application to run on Mac OS and want to get an input value from the user without frightening the user by opening a scary terminal window.

Solution

Many Mac OS users are frightened by the terminal, so Python scripts that require simple input from the user shouldn’t rely on normal textual input but rather should use the EasyDialogs module from the Python Standard Library. Here is an example, a simple image converter and resizer application:

import os, sys, EasyDialogs, Image
# instead of relying on sys.argv, ask the user via a simple dialog:
rotater = ('Rotate right', 'Rotate image by 90 degrees clockwise')
rotatel = ('Rotate left', 'Rotate image by 90 degrees anti-clockwise')
scale = ('Makethumb', 'Make a 100x100 thumbnail')
str = ['Format JPG', 'Format PNG']
cmd = [rotater, rotatel, scale]
optlist = EasyDialogs.GetArgv(str, cmd,
              addoldfile=False, addnewfile=False, addfolder=True)
# now we can parse the arguments and options (we could use getopt, too):
dirs = [  ]
format = "JPEG"
rotationr = False
rotationl = False
resize = False
for arg in optlist:
    if arg == "--Format JPG":
        format = "JPEG"
    if arg == "--Format PNG":
        format = "PNG"
    if arg == "Rotate right":
        rotationr = True
    if arg == "Rotate left":
        rotationl = True
    if arg == "Makethumb":
        resize = True
    if os.path.isdir(arg):
        dirs.append(arg)
if len(dirs) == 0:
    EasyDialogs.Message("No directories specified")
    sys.exit(0)
# Now, another, simpler dialog, uses the system's folder-chooser dialog:
path = EasyDialogs.AskFolder("Choose destination directory")
if not path:
    sys.exit(0)
if not os.path.isdir(path) :
    EasyDialogs.Message("Destination directory not found")
    sys.exit(0)
# and now a progress bar:
tot_numfiles = sum([ len(os.listdir(d)) for d in dirs ])
bar = EasyDialogs.ProgressBar("Processing", tot_numfiles)
for d in dirs:
    for item in os.listdir(d):
        bar.inc( )
        try:
            objpict = Image.open(d + "/" + item)
            if resize: objpict.thumbnail((100, 100, 1))
            if rotationr: objpict = objpict.rotate(-90)
            if rotationl: objpict = objpict.rotate(90)
            objpict.save(path + "/" + item + "." + format, format)
        except:
            print item + " is not an image"
# and one last dialog...:
score = EasyDialogs.AskYesNoCancel("Do you like this program?")
if score == 1:
    EasyDialogs.Message("Wwowowowow, EasyDialog roolz, ;-)")
elif score == 0:
    EasyDialogs.Message("Sigh, sorry, will do better next time!-(")
elif score == -1:
    EasyDialogs.Message("Hey, you didn't answer?!")

Discussion

This recipe’s program is quite trivial, mostly meant to show how to use a few of the dialogs in the EasyDialogs standard library module for the Mac. You could add quite a few more features, or do a better job of implementing some of those in this recipe, for example, by using getopt from the Python Standard Library to parse the arguments and options, rather than the roll-your-own approach we’ve taken.

Since EasyDialogs is in the Python Standard Library for the Mac, you can count on finding that module, as well as Python itself, in any Mac that runs Mac OS X 10.3 Panther—and that’s well over ten million Macs, according to Apple. Just build your script into an application with bundlebuilder or, even better, with py2app and distutils. Doing so will enable you to distribute your Python application so that users can park it in the Dock, use drag-and-drop from the Finder to give it arguments, and so on. Documentation for both bundlebuilder and py2app can be found on the Wiki at http://www.pythonmac.org/wiki.

The EasyDialogs module in the Python Standard Library works only on the Mac, but if you like the concept, you can try out Jimmy Retzlaff’s port of that module to Windows, available for download at http://www.averdevelopment.com/python/EasyDialogs.html.

See Also

Library Reference documentation on EasyDialogs; http://www.pythonmac.org/wiki for more information on Python for Mac resources; py2app is at http://undefined.org/python/; http://www.averdevelopment.com/python/EasyDialogs.html for a port of EasyDialogs to Microsoft Windows.

11.18. Building a Python Cocoa GUI Programmatically

Credit: Dethe Elza

Problem

You are developing a Python application using Mac OS X’s Aqua interface (through Apple’s Cocoa toolkit and the PyObjC, Python/Objective-C bridge). You want to build the application’s user interface within the program itself (as is normally done in most other Python GUI toolkits), rather than via Apple’s Interface Builder (IB) and resulting .nib files (as is usually done with Cocoa for Aqua applications).

Solution

Anything that you can do via Interface Builder and .nib files, you can also do directly in your program. Here is a simple demo:

from math import sin, cos, pi
from Foundation import *
from AppKit import *
class DemoView(NSView):
    n = 10
    def X(self, t):
        return (sin(t) + 1) * self.width * 0.5
    def Y(self, t):
        return (cos(t) + 1) * self.height * 0.5
    def drawRect_(self, rect):
        self.width = self.bounds( )[1][0]
        self.height = self.bounds( )[1][1]
        NSColor.whiteColor( ).set( )
        NSRectFill(self.bounds( ))
        NSColor.blackColor( ).set( )
        step = 2 * pi/self.n
        loop = [i * step for i in range(self.n)]
        for f in loop:
            p1 = NSMakePoint(self.X(f), self.Y(f))
            for g in loop:
                p2 = NSMakePoint(self.X(g), self.Y(g))
                NSBezierPath.strokeLineFromPoint_toPoint_(p1, p2)
class AppDelegate(NSObject):
    def windowWillClose_(self, notification):
        app.terminate_(self)
def main( ):
    global app
    app = NSApplication.sharedApplication( )
    graphicsRect = NSMakeRect(100.0, 350.0, 450.0, 400.0)
    myWindow = NSWindow.alloc( ).initWithContentRect_styleMask_backing_defer_(
        graphicsRect,
        NSTitledWindowMask
        | NSClosableWindowMask
        | NSResizableWindowMask
        | NSMiniaturizableWindowMask,
        NSBackingStoreBuffered,
        False)
    myWindow.setTitle_('Tiny Application Window')
    myView = DemoView.alloc( ).initWithFrame_(graphicsRect)
    myWindow.setContentView_(myView)
    myDelegate = AppDelegate.alloc( ).init( )
    myWindow.setDelegate_(myDelegate)
    myWindow.display( )
    myWindow.orderFrontRegardless( )
    app.run( )
    print 'Done'
if _ _name_ _ == '_ _main_ _':
    main( )

Discussion

Most programmers prefer to lay out their programs’ user interfaces graphically, and Apple’s Interface Builder application, which comes with Apple’s free Developer Tools (also known as XCode), is a particularly nice tool for this task (when you’re using Apple’s Cocoa toolkit to develop a GUI for Mac OS X’s Aqua interface). (The PyObjC extension makes using Cocoa from Python an obvious choice, if you’re developing applications for the Macintosh.)

Sometimes it is more convenient to keep all the GUI building within the very program I’m developing, at least at first. During the early iterations of developing a new program, I often need to refactor everything drastically as I rethink the problem space. When that happens, trying to find all the connections that have to be modified or renamed is a chore in Interface Builder or in any other such interactive GUI-painting application.

Some popular GUI toolkits, such as Tkinter, are based on the idea that the program builds its own GUI at startup by defining the needed objects and appropriately calling functions and methods. It may not be entirely clear to users of other toolkits, such as Cocoa, that just about every toolkit is capable of operating in a similar manner, allowing “programmatic” GUI construction. This applies even to those toolkits that are most often used by means of interactive GUI-painting applications. By delaying the use of IB until your program is more functional and stable, it’s more likely that you’ll be able to design an appropriate interface. This recipe can help get you started in that direction.

This recipe’s code is a straight port of tiny.m, from Simson Garfinkel and Michael Mahoney, Building Cocoa Applications: A Step-by-Step Guide (O’Reilly), showing how to build a Cocoa application without using Interface Builder nor loading .nib files. This recipe was my first PyObjC project, and it is indebted both to the Cocoa book and to PyObjC’s “Hello World” example code. Starting from this simple, almost toy-level recipe, I was able to use Python’s file handling to easily build a graphical quote viewer and ramp up from there to building rich, full-fledged GUIs.

See Also

Garfinkel and Mahoney’s Building Cocoa Applications: A Step-by-Step Guide (O’Reilly); PyObjC is at http://pyobjc.sourceforge.net/.

11.19. Implementing Fade-in Windows with IronPython

Credit: Brian Quinlan

Problem

You’re developing an application with IronPython (using Windows Forms on Microsoft .NET), and you want to use fade-in windows to display temporary data.

Solution

Fading in can best be accomplished using the Form.Opacity property and a Timer. Fade-in windows, being a form of pop-up window, should also set the topmost window style:

from System.Windows.Forms import *
from System.Drawing import *
from System.Drawing.Imaging import *
form = Form(Text="Window Fade-ins with IronPython",
         HelpButton=False, MinimizeBox=True, MaximizeBox=True,
         WindowState=FormWindowState.Maximized,
         FormBorderStyle=FormBorderStyle.Sizable,
         StartPosition=FormStartPosition.CenterScreen,
         Opacity = 0)
# create a checker background pattern image
box_size = 25
image = Bitmap(box_size * 2, box_size * 2)
graphics = Graphics.FromImage(image)
graphics.FillRectangle(Brushes.Black, 0, 0, box_size, box_size)
graphics.FillRectangle(Brushes.White, box_size, 0, box_size, 50)
graphics.FillRectangle(Brushes.White, 0, box_size, box_size, box_size)
graphics.FillRectangle(Brushes.Black, box_size, box_size, box_size, box_size)
form.BackgroundImage = image
# create a control to allow the opacity to be adjusted
opacity_tracker = TrackBar(Text="Transparency",
                           Height = 20, Dock = DockStyle.Bottom,
                           Minimum = 0, Maximum = 100, Value = 0,
                           TickFrequency = 10, Enabled = False)
def track_opacity_change(sender, event):
    form.Opacity = opacity_tracker.Value / 100.0
opacity_tracker.ValueChanged += track_opacity_change
form.Controls.Add(opacity_tracker)
# create a timer to animate the initial appearance of the window
timer = Timer( )
timer.Interval = 15
def tick(sender, event):
    val = opacity_tracker.Value + 1
    if val >= opacity_tracker.Maximum:
        # ok, we're done, set the opacity to maximum, stop the
        # animation, and let the user play with the opacity manually
        opacity_tracker.Value = opacity_tracker.Maximum
        opacity_tracker.Minimum = 20    # don't let the window disappear
        opacity_tracker.Enabled = True
        timer.Stop( )
    else:
        opacity_tracker.Value = val
timer.Tick += tick
timer.Start( )
form.ShowDialog( )

Discussion

While IronPython, at the time of this writing, is not yet entirely mature, and it therefore cannot be recommended for use to develop Windows Forms applications intended for production deployment, any .NET (or Mono) developer should already download IronPython and start playing with it; when it matures, it promises to provide a nonpareil high-productivity avenue for .NET application development.

This recipe shows that IronPython can already do, with elegance and ease, a number of interesting things with Windows Forms. Specifically, the recipe demonstrates several techniques of Windows Forms programming:

  • How to create a form.

  • How to draw in an off-screen image.

  • How to create a control, add it to a form, and manage its events.

  • How to create a timer and add a delegate to get periodic events.

More specifically, this recipe shows how to create a fade-in window using IronPython. Several applications use fade-in windows for temporary data; look, for example, at Microsoft’s new Outlook XP. It displays mail messages through a fade-in/fade-out pop-up window. It looks cool, it’s also quite useful, and IronPython makes it a snap!

See Also

IronPython is at http://ironpython.com/.



[1] AVI (Advanced Visual Interface)

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.232.189