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.
Credit: Larry Bates
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.
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(" ")
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.
Documentation on text-mode console I/O in Python in a Nutshell.
Credit: Danny Yoo, Martin Sjogren
You need to use many callbacks without arguments,
typically while writing a Tkinter-based GUI, and you’d rather avoid
using lambda
.
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
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.
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.
Credit: Mike Foord, Peter Cogolo
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.
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)
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.
tkSimpleDialog
module
documentation is at http://epydoc.sourceforge.net/stdlib/public/tkSimpleDialog-module.html.
Credit: John Fouhy
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.
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
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.
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.
You want your application to allow the user to easily enter accented characters into Tkinter widgets even from a U.S.-layout keyboard.
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"
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.
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.
Credit: Brent Burley
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.
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( )
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.
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.
Your image files are in various formats (GIF, JPG, PNG, TIF, BMP), and you need to convert among these formats.
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 ;-).
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.
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/.
Credit: JØrgen Cederberg, Tobias Klausmann
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)
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.
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.
Credit: Jacob Hallén, Laura Creighton, Boudewijn Rempt
You need to access sockets, serial ports, or other asynchronous (but blocking) I/O sources, while running a GUI.
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( )
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 Queue
s. In this recipe,
the GUI thread still has to do some polling (on the Queue
s), 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).
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.
Credit: Sanghyeon Seo
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.
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( )
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.
idlelib
docs at http://pydoc.org/2.3/idlelib.html.
Credit: Brent Burley, Pedro Werneck, Eric Rose
You need a Tkinter widget that works just like a normal
Listbox
but with multiple values
per row.
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( )
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 lambda
s 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.
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.
Credit: Pedro Werneck
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.
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))
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 dict
s 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.
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.
Credit: Iuri Wickert
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.
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.
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.
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.
Credit: Mark Nenadov
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
).
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 #
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.
wxPython, and the wxWidgets toolkit it depends on, are described in detail at http://www.wxPython.org and http://www.wxWidgets.org.
Credit: Ferdinand Jamitzky, Edoardo “Dado” Marcora
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.
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]
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.
ImageJ is at http://rsb.info.nih.gov/ij/; Jython is at http://www.jython.org; jikes is at http://www-124.ibm.com/developerworks/oss/jikes/; for more on using Jython with Imagej, http://marcora.caltech.edu/jython_imagej_howto.htm.
Credit: Joel Lawhead, Chuck Parker
You want to make a simple Swing image viewer, accepting the URL to an image and displaying the image in a Swing window.
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")
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!
Swing docs are at http://java.sun.com/docs/books/tutorial/uiswing/; Jython is at http://www.jython.org.
Credit: Matteo Rattotti
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.
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?!")
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.
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.
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).
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( )
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.
Garfinkel and Mahoney’s Building Cocoa Applications: A Step-by-Step Guide (O’Reilly); PyObjC is at http://pyobjc.sourceforge.net/.
Credit: Brian Quinlan
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.
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( )
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!
IronPython is at http://ironpython.com/.