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

R

Randy Smith

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()
 
J

James Stroud

Randy said:
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
 
C

curiouserrandy

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
 

Ask a Question

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

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,744
Messages
2,569,484
Members
44,903
Latest member
orderPeak8CBDGummies

Latest Threads

Top