TkInter: Problem with propagation of resize events through geometry manager hierarchy?

Randy Smith rdsc-python at tigana.org
Sat Feb 7 17:50:04 EST 2009


Hi!  I'm looking for help with a Tkinter program's handling of resize.
I'm trying to do a fairly simple widget that shows a cropped part of a
larger image, and let's you navigate within the larger image through a
variety of methods.  The widget hierarchy is:

root
   ImageWidget (my class)
     Label (contains the image)
     Horizontal Scroll Bar
     Vertical scroll bar

The cropping and scrolling works fine.  But when I try to add
responding to resize events, I get into trouble.  Specifically:
* When I naively change the size of the image shown to be borderwidth
   less than the size indicated in the configure event, the size of the
   image shown grows gradually but inexorably when I start the test
   app.  (Sorta scary, actually :-})
* When I fiddle a bit to figure out what the actual difference in size
   is between the Configure event and the image that can be displayed,
   I get a vibrating, jagged display of the image.

Investigation suggests that multiple configure events are hitting the
label in response to each user resize with different sizes.  I'm
guessing that when I resize the image in response to those different
events, that creates new resize events propagating through the window
manager hierarchy, which creates new configure events, which means my
handler changes the image size, which ... you get the idea.  However,
everything seems to work fine if I leave out the scroll bars and just
have a label in a frame inside the root window; the image resizes
fine.  If the scroll bars are in place but I don't have the image
resize bound to the configure event, I get two sets of resize events
propagaing through the system on startup; without, I just get one.

Event lists and code included below.  Any help would be appreciated.
Thanks!

       	    	     	      	      	  -- Randy Smith

-- Event list on startup with scroll bars:

<receiving widget>: width height
root :  220 220
root :  1 1
iwidget :  220 220
root :  220 220
vscroll :  16 204
root :  16 204
hscroll :  204 16
root :  204 16
ilabel :  204 204
root :  204 204
vscroll :  15 205
root :  15 205
hscroll :  205 15
root :  205 15
ilabel :  205 205
root :  205 205
root :  219 219
ilabel :  205 205
root :  205 205
hscroll :  205 15
root :  205 15
vscroll :  15 205
root :  15 205
iwidget :  219 219
root :  219 219
vscroll :  15 204
root :  15 204
hscroll :  204 15
root :  204 15
ilabel :  204 204
root :  204 204

-- Event list on startup without scroll bars

root :  204 204
root :  1 1
iwidget :  204 204
root :  204 204
ilabel :  204 204
root :  204 204

-- Code, without image resize.  If you want to see the vibration,
    uncomment the line
         self.label.bind("<Configure>", self.reconfigure, "+")
    To actually run it you'll need an image "test.tiff" in the current
    directory (any image of size > 200x200 will do) and access to the
    python imaging library (PIL), but I hope the code is pretty clear
    (other than the math transforming between various coordinate
    systems, which I don't believe is relevant; focus on
    reconfigure(), refresh, and __init__).

#!/usr/bin/python

import traceback
from Tkinter import *
from PIL import Image
import ImageTk

debug = 4

def display_args(*args):
     print "Args: ", args

def display_event(event):
     print event.__dict__

def display_tag_and_size(tag, event):
     print tag, ": ", event.width, event.height

class NotYetImplemented(Exception): pass

def mapnum(x, fromrange, torange):
     assert fromrange[0] <= x < fromrange[1], (fromrange[0], x,  
fromrange[1])
     assert torange[0] < torange[1], (torange[0], torange[1])
     ## Need to force floating point
     x *= 1.0
     return (x - fromrange[0]) / (fromrange[1] - fromrange[0]) *  
(torange[1] - torange[0]) + torange[0]

class ImageWidget(Frame):
     def __init__(self, parent, gfunc, image_size,
                  starting_zoom=1,
                  starting_ul=(0,0),
                  starting_size = None):
         """Create an Image Widget which will display an image based  
on the
         function passed.  That function will be called with the  
arguments
         (zoom_factor, (xstart, xend), (ystart, yend)) and must return a
         TkInter PhotoImage object of size (xend-xstart, yend-ystart).
         IMAGE_SIZE describes the "base size" of the image being  
backed by
         gfunc.
         starting_* describes the starting window on the image."""

         ## Default starting size to whole image
         if not starting_size: starting_size = image_size

         ## Init parent
         Frame.__init__(self, parent)
         self.bind("<Configure>",
                   lambda e, t="iwidget": display_tag_and_size(t, e))
         ## Base image parameters
         self.generator_func = gfunc
         self.isize = image_size

         ## Modifier of base image size for coords currently working in
         self.zoom = starting_zoom

         ## Interval of augmented (zoomed) image currently shown
         ## Note that these must be integers; these map directly to  
pixels
         self.xint = [starting_ul[0], starting_ul[0] + starting_size[0]]
         self.yint = [starting_ul[1], starting_ul[1] + starting_size[1]]

         ## Widgets
         self.label = Label(self)
         print type(self.label["borderwidth"])
         self.label.bind("<Configure>",
                         lambda e, t="ilabel": display_tag_and_size(t,  
e))
         self.labelborderwidth = 4 	# XXX: Constant because I can't  
manage
         				# to get the value of
         				# self.label["borderwidth"] as a number :-?
         self.hscroll = Scrollbar(self, orient = HORIZONTAL,
                                  command = lambda *args:  
self.scmd(False, *args))
         self.hscroll.bind("<Configure>",
                         lambda e, t="hscroll":  
display_tag_and_size(t, e))

         self.vscroll = Scrollbar(self, orient = VERTICAL,
                                  command = lambda *args:  
self.scmd(True, *args))
         self.vscroll.bind("<Configure>",
                           lambda e, t="vscroll":  
display_tag_and_size(t, e))

#        self.label.bind("<Configure>", self.reconfigure, "+")

         ## Configure widgets
         self.label.grid(row = 0, column = 0, sticky=N+S+E+W)
         self.hscroll.grid(row = 1, column = 0, sticky=E+W)
         self.vscroll.grid(row = 0, column = 1, sticky=N+S)
         self.columnconfigure(0, weight=1)
         self.rowconfigure(0, weight=1)

         ## And display
         self.refresh()

     def refresh(self):
         """Bring the image in the frame and the scroll bars in line  
with the
         current values."""
         self.image = self.generator_func(self.zoom, self.xint,  
self.yint)
         self.label["image"] = self.image
         ## Map x&y interval into unit interval for scroll bars.
         scroll_settings = (
             (mapnum(self.xint[0],
                     (0, self.isize[0] * self.zoom),
                     (0, 1)),
              mapnum(self.xint[1],
                     (0, self.isize[0] * self.zoom),
                     (0, 1))),
             (mapnum(self.yint[0],
                     (0, self.isize[1] * self.zoom),
                     (0, 1)),
              mapnum(self.yint[1],
                     (0, self.isize[1] * self.zoom),
                     (0, 1))))
         if debug > 5:
             print scroll_settings
         self.hscroll.set(*scroll_settings[0])
         self.vscroll.set(*scroll_settings[1])

     def reconfigure(self, event):
         print self.label["width"], self.label["height"], event.__dict__
         self.xint[1] = min(self.xint[0]+event.width -  
self.labelborderwidth,
                            int(self.isize[0]*self.zoom))
         self.yint[1] = min(self.yint[0]+event.height -  
self.labelborderwidth,
                            int(self.isize[1]*self.zoom))
         self.refresh()

     def scmd(self, isy, type, num, what = None):
         """Takes input args, changes either xint or yint, and calls
         refresh to update the entire image."""
         ## Figure out interval to modify and base image size to work  
off
         if isy:
             interval = self.yint
             int_range = self.isize[1] * self.zoom
         else:
             interval = self.xint
             int_range = self.isize[0] * self.zoom

         ## Figure out the width
         int_width = interval[1] - interval[0]

         ## Transform input
         num = float(num)

         if type == MOVETO:
             # num Describes the location of the low end of the slider
             interval[0] = mapnum(num, (0, 1), (0, int_range))
         elif type == SCROLL:
             if what == "units":
                 interval[0] += num
             else:
                 assert what == "pages", what
                 interval[0] += num * int_width

             if interval[0] < 0: interval[0] = 0
             if interval[0] > int_range - int_width:
                 interval[0] = int_range - int_width
         interval[0] = int(interval[0])
         interval[1] = interval[0] + int_width
         assert type == MOVETO, type
         if debug > 5:
             print "yscroll" if isy else "xscroll", num, interval[0],  
interval[1]
         self.refresh()

## Room for optimization here; don't need to resize the whole image
def gfunc_for_image(image, zoom, xint, yint):
     bbox = image.getbbox()
     isize = (bbox[2] - bbox[0], bbox[3] - bbox[1])
     ssize = (isize[0] * zoom, isize[1] * zoom)

     if debug > 5:
         print zoom, xint, yint, isize, ssize
     ri = image.resize(ssize, Image.BILINEAR)
     ci = ri.crop((xint[0],yint[0],xint[1],yint[1]))
     return ImageTk.PhotoImage(ci)

def IWFromFile(parent, file, starting_size = None):
     "Return an ImageWidget object based on an image on a file."
     baseimage = Image.open(file)
     (ulx, uly, lrx, lry) = baseimage.getbbox()
     return ImageWidget(parent,
                        lambda z,xint,yint,i=baseimage:  
gfunc_for_image(i, z, xint, yint),
                        (lrx - ulx, lry - uly),
                        starting_size = starting_size)

def IWFromImage(parent, img, starting_size = None):
     "Return an imageWidget object based on a PIL image passed in."
     (ulx, uly, lrx, lry) = img.getbbox()
     return ImageWidget(parent,
                        lambda z,xint,yint,i=img: gfunc_for_image(i,  
z, xint, yint),
                        (lrx - ulx, lry - uly),
                        starting_size = starting_size)


if __name__ == "__main__":
     root = Tk()
     root.resizable(True, True)

     root.bind("<Configure>", lambda e, t="root":  
display_tag_and_size(t, e))
     iw = IWFromFile(root, "test.tiff", starting_size = (200, 200))
     print "Gridding iwidget."
     iw.grid(row=0,column=0,sticky=N+S+E+W)
     print "Configuring root row."
     root.rowconfigure(0, weight=1)
     print "Configuring root column."
     root.columnconfigure(0, weight=1)
     print "Mainlooping."
     iw.mainloop()









More information about the Python-list mailing list