We're back after a server migration that caused effbot.org to fall over a bit harder than expected. Expect some glitches.

Tkinter Tricks: Using ImageWin to Display Images on Windows

Fredrik Lundh | September 2004 | Originally posted to online.effbot.org

Tkinter provides a PhotoImage class that you can use to display images in a Tkinter widget. The standard implementation can read GIF and PGM/PPM images; if you need more formats, you can use the corresponding class in PIL’s ImageTk module instead.

Here’s a small display widget, using a Label to display a PIL image object:

from Tkinter import *
from ImageTk import PhotoImage

class ImageView(Frame):

    def __init__(self, master, **options):
        Frame.__init__(self, master, **options)
        self.view = Label(self)
        self.view.place(relwidth=1, relheight=1)

    def setimage(self, image):
        photo = PhotoImage(image)
        self.view.config(image=photo)
        self.view.photo = photo # keep a reference!

To use this widget class, make sure you pass in an appropriate width and height when you create the widget, and call the setimage with a PIL Image object to update the image. Note that since the label is placed, not packed, the outer widget will not resize itself (use config if you need to change the size).

On my machine, converting a 512x512 RGB image to a PhotoImage object, and displaying it, takes about 30 milliseconds (using a stock 2.3.3 interpreter). In other words, this machine can display just over 30 images a second.

On Windows, PIL also provides a ImageWin module, which can convert a PIL image to a Windows bitmap (a “DIB”), and copy that bitmap to the screen. With a little trickery, we can use this class also under Tkinter. Here’s an implementation:

from Tkinter import *
from ImageWin import Dib, HWND

class ImageView(Frame):

    def __init__(self, master, **options):
        Frame.__init__(self, master, **options)
        self.dib = None
        self.bind("<Expose>", self._expose)

    def setimage(self, image):
        self.config(bg="") # don't update the background
        self.dib = Dib(image)
        self.event_generate("<Expose>")

    def _expose(self, event):
        if self.dib:
            self.dib.expose(HWND(self.winfo_id()))

Here, the Frame widget itself is used to display the image. When setimage is called, the frame background is set to an empty string, to prevent Tkinter from drawing the background. We then create the bitmap, and generate an explicit <Expose> event to force an update. When Tkinter gets around to process that event, the _expose method is called, and it simply copies the image to the screen.

With this code, the display time drops to under 8 milliseconds per image, or some 130 frames per second.

To use this in portable code, you can do something like:

try:
    import ImageWin
    # make sure we can create DIBs
    dib = ImageWin.Dib("L", (1, 1))
except (ImportError, AttributeError):
    ImageWin = None

...

if ImageWin:
    class ImageView(Frame):
        ... windows version
else:
    class ImageView(Frame):
        ... generic tkinter version

...

view = ImageView(...)