TkInter: Problem with propagation of resize events through geometrymanager hierarchy?

Discussion in 'Python' started by Randy Smith, Feb 7, 2009.

  1. Randy Smith

    Randy Smith Guest

    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()
    Randy Smith, Feb 7, 2009
    #1
    1. Advertising

  2. Randy Smith

    James Stroud Guest

    Randy Smith wrote:
    > 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.


    I can't test your code because I don't have the test image and for some
    reason it does not recognize a tiff of my own. But, just glancing at
    your code, it looks like a quick-fix would be to set self.zoom to a
    sentinel at the end of refresh() and return 'break' at the top of the
    methods that use self.zoom if it is said sentinel value (e.g. "if
    self.zoom == WHATEVER_SENTINEL: return 'break'). You may also want to
    return 'break' for event responders that should terminate the event
    chain. This is a general technique to stop a lot of unwanted event
    propagation.

    James


    --
    James Stroud
    UCLA-DOE Institute for Genomics and Proteomics
    Box 951570
    Los Angeles, CA 90095

    http://www.jamesstroud.com
    James Stroud, Feb 8, 2009
    #2
    1. Advertising

  3. Randy Smith

    Guest

    Re: TkInter: Problem with propagation of resize events throughgeometry manager hierarchy?

    On Feb 8, 6:27 am, James Stroud <> wrote:

    > I can't test your code because I don't have the test image and for some
    > reason it does not recognize a tiff of my own. But, just glancing at
    > your code, it looks like a quick-fix would be to set self.zoom to a
    > sentinel at the end of refresh() and return 'break' at the top of the
    > methods that use self.zoom if it is said sentinel value (e.g. "if
    > self.zoom == WHATEVER_SENTINEL: return 'break'). You may also want to
    > return 'break' for event responders that should terminate the event
    > chain. This is a general technique to stop a lot of unwanted event
    > propagation.


    Thanks! I hadn't known about the "return 'break'" technique. But
    I don't follow your sentinel suggestion; how would that sentinel
    ever get reset? It seems as if the first time through the event
    chain it'd be set to the sentinel, and the routines that pay
    attention to it would never execute. What am I missing?
    I tried simply returning 'break' at the end of "refresh()" and
    that made no change in behavior.

    (Note that zoom should be constant at 1.0 for the life of
    this program; I put it in because I'm planning to put in expansion/
    contraction of images after I get resize & scrolling working
    together. Eliminating all multiplications by self.zoom confirms
    this belief; no change in behavior).

    One thing I've been looking for (including in the source :-J) is
    a description of the precise process that the geometry manager
    goes through in figuring out and then setting sizes for the
    various widgets (on resize or, apparently, startup). I suspect
    with that + the "return 'break'" technique I could get this to
    work. But I haven't been having any luck finding that documentation.

    If you'd like me to send you the test.tiff image, I'm happy to, but
    it's nothing special; just a screen capture of a random google maps
    satellite view that I use for testing.

    -- Randy


    >
    > James
    >
    > --
    > James Stroud
    > UCLA-DOE Institute for Genomics and Proteomics
    > Box 951570
    > Los Angeles, CA 90095
    >
    > http://www.jamesstroud.com
    , Feb 8, 2009
    #3
    1. Advertising

Want to reply to this thread or ask your own question?

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. Oleg
    Replies:
    1
    Views:
    713
    Francisco Rodriguez
    Feb 18, 2004
  2. Binary

    VHDL propagation time

    Binary, Dec 12, 2005, in forum: VHDL
    Replies:
    4
    Views:
    768
  3. Alan G Isaac

    color propagation in tkinter

    Alan G Isaac, Apr 20, 2009, in forum: Python
    Replies:
    4
    Views:
    444
    John Posner
    Apr 23, 2009
  4. Chris

    Events Events Events Please Help

    Chris, Aug 30, 2005, in forum: ASP .Net Web Controls
    Replies:
    0
    Views:
    340
    Chris
    Aug 30, 2005
  5. Replies:
    2
    Views:
    1,248
    Adrienne
    Feb 13, 2005
Loading...

Share This Page