Placing Pixels 1x1

   1 import tkinter
   2 
   3 root = tkinter.Tk()
   4 
   5 def pixel(image, pos, color):
   6     """Place pixel at pos=(x,y) on image, with color=(r,g,b)."""
   7     r,g,b = color
   8     x,y = pos
   9     image.put("#%02x%02x%02x" % (r,g,b), (y, x))
  10 
  11 photo = tkinter.PhotoImage(width=32, height=32)
  12 
  13 pixel(photo, (16,16), (255,0,0))  # One lone pixel in the middle...
  14 
  15 label = tkinter.Label(root, image=photo)
  16 label.grid()
  17 root.mainloop()

Fill Many Pixels at Once

   1 import tkinter
   2 
   3 root = tkinter.Tk()
   4 
   5 def fill(image, color):
   6     """Fill image with a color=(r,b,g)."""
   7     r,g,b = color
   8     width = image.width()
   9     height = image.height()
  10     hexcode = "#%02x%02x%02x" % (r,g,b)
  11     horizontal_line = "{" + " ".join([hexcode]*width) + "}"
  12     image.put(" ".join([horizontal_line]*height))
  13 
  14 photo = tkinter.PhotoImage(width=32, height=32)
  15 
  16 fill(photo, (255,0,0))  # Fill with red...
  17 
  18 label = tkinter.Label(root, image=photo)
  19 label.grid()
  20 root.mainloop()

Disappearing PhotoImage

Something that seems to bite every new Tkinter programmer at least once: you've loaded a PhotoImage, applied it to a Button, Label, or other widget, and nothing shows up - because you referenced the image only via a local variable, so it went out of scope and got deleted before the widget ever appeared.

   1 import tkinter
   2 
   3 root = tkinter.Tk()
   4 
   5 def create_button_with_scoped_image():
   6     img = tkinter.PhotoImage(file="icons.gif")  # reference PhotoImage in local variable
   7     button = tkinter.Button(root, image=img)
   8     button.grid()
   9 
  10 create_button_with_scoped_image()  # PROBLEM:  the button will be blank
  11 
  12 tkinter.mainloop()

The usual solution is to store a reference to the image as an attribute of the widget that is to display it.

   1 def create_button_with_scoped_image():
   2     img = tkinter.PhotoImage(file="icons.gif")
   3     button = tkinter.Button(root, image=img)
   4     button.img = img  # store a reference to the image as an attribute of the widget
   5     button.grid()
   6 
   7 create_button_with_scoped_image()  # WORKS:  the button will have an image on it

You might just call this a bug in Tkinter, but the reality isn't quite that simple... Tk images are not first-class objects: when you apply one to a widget, you do not in any sense "pass a reference to" the image.

   1 img = tkinter.PhotoImage(file="icons.gif")
   2 print(img)  # -> "pyimage1"
   3 button = tkinter.Button(root, image=img)  # the tkinter Button remembers "pyimage1", not img

Instead, you just pass a name, which Tk looks up in an internal table of defined images (from which images can only be deleted manually). From Tkinter, passing a PhotoImage as a parameter actually only sends the str() of the image object to the Tcl side: this is just a string, the randomly-generated name assigned when the object was created. "No reference to the image itself" means "no reference counting" means "no way for the Python side to be notified when the image is truly no longer used". If Tkinter didn't delete images when no Python reference to them existed, they would be unavoidable memory leaks.

Tkinter could automatically store a reference to the image object itself when one is passed as a parameter to a widget method, but this would actually be worse in some cases. Consider that the image could become unused without the Python side being aware of it: for example, if an image is inserted in a Text widget, but the user later deletes the text containing it. An automatically-generated reference would make the image object leak in this case.

The only workable improvement I can think of is for PhotoImage.__del__ to only immediately delete the image if Tk's image inuse command showed them to currently be unused. Otherwise, the image name would be added to a list of orphaned images; periodically (via after()?), Tkinter would scan this list for images that are no longer inuse and delete them then. Possible problem: if an image was inserted in a Text widget and was later deleted, but the deletion is still in the widget's undo stack, does the image count as inuse?

Copy a SubImage

Another problem with PhotoImages: the wrapper for their copy() method is botched. It allows no parameters, so all you can do with an image is make an exact copy of it, which isn't terribly useful. It doesn't appear that this can be fixed in a backwards-compatible way, since what functionality copy() does have is exactly backwards to Tk: Tkinter's copy is a method of the source image (automatically creating the destination image for you), while Tk's copy is a subcommand of the destination image (allowing you to create it yourself with whatever options are appropriate). Fortunately, it's pretty easy to make a direct Tcl call to do image operations that Tkinter doesn't support. For example, here's a simple rectangular subimage extractor:

   1 def subimage(src, l, t, r, b):
   2     dst = PhotoImage()
   3     dst.tk.call(dst, 'copy', src, '-from', l, t, r, b, '-to', 0, 0)
   4     return dst

PIL

If you want to do much with images beyond loading and displaying them, Tk and Tkinter can't help you very much. Consider PIL (PythonImagingLibrary): it can perform whatever image operations you need (rotate, warp, scale with better quality than Tk, etc.), then copy to a ImageTk object which can then be used just like a Tkinter image object. Unfortunately, PIL isn't as commonly included in Python distributions as Tkinter is.

See Also

tkinter: PhotoImage (last edited 2014-02-27 02:53:56 by sam)