I'm trying to make a widget to hold an image that will automatically resize to fit its container, e.g. if packed directly into a window, then expanding that window will expand the image.
I have some code that is semi functional but I've had to add a couple of constants into one of the routines to prevent the auto resize from re triggering itself (causing it to keep growing in size)
I'm sure that the reason for this is due to the widgets internal padding/border, but even trying to take that into account I get this issue.
I'm using python 3.3.2, and PIL 1.1.7 on 64 bit Windows 7 my code is the following:
from tkinter import tix
from PIL import Image, ImageTk
def Resize_Image(image, maxsize):
r1 = image.size[0]/maxsize[0] # width ratio
r2 = image.size[1]/maxsize[1] # height ratio
ratio = max(r1, r2)
newsize = (int(image.size[0]/ratio), int(image.size[1]/ratio)) # keep image aspect ratio
image = image.resize(newsize, Image.ANTIALIAS)
return image
class Pict_Frame(tix.Label):
def __init__(self, parent=None, picture=None, maxupdate=None, **kwargs):
tix.Label.__init__(self, parent, **kwargs)
self.bind("<Configure>", self._resize_binding)
self.maxupdate = maxupdate
self.update_after_id = None
self.photo = None
self.image = None
if picture:
self.set_picture(picture)
def _resize_binding(self, event):
if self.photo:
if not self.maxupdate:
self.load_picture()
else:
if not self.update_after_id:
self.update_after_id = self.after(int(1000/self.maxupdate), self.load_picture)
def load_picture(self):
if self.photo:
if self.update_after_id:
self.update_after_id = None
if (self.winfo_width() > 1) and (self.winfo_height() > 1): # prevent updates before widget gets sized
self.image = ImageTk.PhotoImage(Resize_Image(self.photo, (
self.winfo_width()-int(self.cget("bd"))-1, self.winfo_height()-int(self.cget("bd"))-1)))
# here is where I added the constants ^^^
# but even using cget to get the border size I have had to add to this
# to prevent the resize loop, and when using other widget styles
#(raised etc) this problem persists
self.configure(image=self.image)
def set_picture(self, filename):
with open(filename, mode="rb") as file:
self.photo = Image.open(file)
self.photo.load() # load image into memory to allow resizing later without file access
self.load_picture()
if __name__ == "__main__":
test = Pict_Frame(bg="grey", bd=2, relief="raised",
maxupdate=2, # allows problem to be easily seen
picture="image.jpg")
test.pack(fill="both", expand=True)
test.master.mainloop()
when I apply other styles, such as a thicker border (10px) this resizing problem occurs showing that the constants don't really solve the problem.
so is there any method to get only the space inside the widget, instead of its requested size?
I believe I have now solved this, but it really needs a lot more testing with different parameters to ensure accurate results. The code I have use to test this is as follows:
from tkinter import tix
from PIL import Image, ImageTk
def Resize_Image(image, maxsize):
r1 = image.size[0]/maxsize[0] # width ratio
r2 = image.size[1]/maxsize[1] # height ratio
ratio = max(r1, r2)
newsize = (int(image.size[0]/ratio), int(image.size[1]/ratio)) # keep image aspect ratio
image = image.resize(newsize, Image.ANTIALIAS)
return image
class Pict_Frame(tix.Label):
def __init__(self, parent=None, picture=None, maxupdate=None, imagesize=None, **kwargs):
tix.Label.__init__(self, parent, **kwargs)
self.bind("<Configure>", self._resize_binding)
self.maxupdate = maxupdate
self.imagesize = imagesize
self.update_after_id = None # used for update rate limiting
self.photo = None # used to store raw image from file for later use
self.image = None # used for reference to the resized image
if imagesize:
self.photo=Image.new("RGB", (1,1)) # create empty image to insert
self.image=ImageTk.PhotoImage(self.photo) # create instance of image for PIL
self.configure(image=self.image)
self.configure(width=imagesize[0], height=imagesize[1]) # not label uses pixels for size, set size passed in
if picture:
self.set_picture(picture) # we have a picture so load it now
def _resize_binding(self, event):
if self.photo: # we have a picture
if not self.maxupdate: # no rate limiting
self.load_picture()
else:
if not self.update_after_id: # if we're not waiting then queue resize
self.update_after_id = self.after(int(1000/self.maxupdate), self.load_picture)
def load_picture(self):
if self.photo:
if self.update_after_id:
self.update_after_id = None
if (self.winfo_width() > 1) and (self.winfo_height() > 1): # prevent updates before widget gets sized
bd = self.cget("bd") # get the border width
if type(bd) != int: # if there was no border set we get an object back
pad = 4 # set this explicitly to avoid problems
else:
pad = int(bd*2) # we have a border both sides, so double the retrieved value
newsize = (self.winfo_width()-pad, self.winfo_height()-pad)
elif self.imagesize: # only use the passed in image size if the widget has not rendered
newsize = self.imagesize
else:
return # widget not rendered yet and no size explicitly set, so break until rendered
self.image = ImageTk.PhotoImage(Resize_Image(self.photo, newsize))
self.configure(image=self.image)
def set_picture(self, filename):
with open(filename, mode="rb") as file:
self.photo = Image.open(file)
self.photo.load() # load image into memory to allow resizing later without file access
self.load_picture()
and my test cases were:
import os
path = "E:\imagefolder"
images = []
ind = 0
for item in os.listdir(path): # get a fully qualified list of images
if os.path.isdir(os.path.join(path, item)):
if os.path.isfile(os.path.join(path, item, "thumb.jpg")):
images.append(os.path.join(path, item, "thumb.jpg"))
def callback():
global ind
ind += 1
if ind >= len(images):
ind = 0
pict.set_picture(images[ind])
ignore_test_cases = []
if 1 not in ignore_test_cases:
print("test case 1: - no border no set size")
root = tix.Tk()
tix.Button(root, text="Next Image", command=callback).pack()
pict = Pict_Frame(parent=root, bg="grey",
maxupdate=2, # allows problem to be easily seen
picture=images[ind])
pict.pack(fill="both", expand=True)
tix.Button(root, text="Next Image", command=callback).pack()
root.mainloop()
if 2 not in ignore_test_cases:
print("test case 2: - small border no set size")
root = tix.Tk()
tix.Button(root, text="Next Image", command=callback).pack()
pict = Pict_Frame(parent=root, bg="grey", bd=2, relief="raised",
maxupdate=2,
picture=images[ind])
pict.pack(fill="both", expand=True)
tix.Button(root, text="Next Image", command=callback).pack()
root.mainloop()
if 3 not in ignore_test_cases:
print("test case 3: - large border no set size")
root = tix.Tk()
tix.Button(root, text="Next Image", command=callback).pack()
pict = Pict_Frame(parent=root, bg="grey", bd=10, relief="raised",
maxupdate=2,
picture=images[ind])
pict.pack(fill="both", expand=True)
tix.Button(root, text="Next Image", command=callback).pack()
root.mainloop()
if 4 not in ignore_test_cases:
print("test case 4: - no border with set size")
root = tix.Tk()
tix.Button(root, text="Next Image", command=callback).pack()
pict = Pict_Frame(parent=root, bg="grey",
maxupdate=2,
imagesize=(256,384),
picture=images[ind])
pict.pack(fill="both", expand=True)
tix.Button(root, text="Next Image", command=callback).pack()
root.mainloop()
if 5 not in ignore_test_cases:
print("test case 5: - small border with set size")
root = tix.Tk()
tix.Button(root, text="Next Image", command=callback).pack()
pict = Pict_Frame(parent=root, bg="grey", bd=2, relief="raised",
maxupdate=2,
imagesize=(256,384),
picture=images[ind])
pict.pack(fill="both", expand=True)
tix.Button(root, text="Next Image", command=callback).pack()
root.mainloop()
if 6 not in ignore_test_cases:
print("test case 6: - large border with set size")
root = tix.Tk()
tix.Button(root, text="Next Image", command=callback).pack()
pict = Pict_Frame(parent=root, bg="grey", bd=10, relief="raised",
maxupdate=2,
imagesize=(256,384),
picture=images[ind])
pict.pack(fill="both", expand=True)
tix.Button(root, text="Next Image", command=callback).pack()
root.mainloop()
if 10 not in ignore_test_cases:
print("test case fullscreen: - small border no set size, in fullscreen window with expansion set up")
root = tix.Tk()
root.state("zoomed")
root.grid_columnconfigure(1, weight=2)
root.grid_columnconfigure(2, weight=1)
root.grid_rowconfigure(2, weight=1)
tix.Button(root, text="Next Image", command=callback).grid(column=2, row=1, sticky="nesw")
pict = Pict_Frame(parent=root, bg="grey",# bd=10, relief="raised",
maxupdate=2,
picture=images[ind])
pict.grid(column=2, row=2, sticky="nesw")
tix.Button(root, text="Next Image", command=callback).grid(column=2, row=3, sticky="nesw")
root.mainloop()
if 11 not in ignore_test_cases:
print("test case fullscreen: - small border no set size, in fullscreen window with expansion set up")
root = tix.Tk()
root.state("zoomed")
root.grid_columnconfigure(1, weight=2)
root.grid_columnconfigure(2, weight=1)
root.grid_rowconfigure(1, weight=1)
frame = tix.Frame(root)
frame.grid(column=2, row=1, sticky="nesw")
frame.grid_columnconfigure(1, weight=1)
frame.grid_rowconfigure(2, weight=1)
tix.Button(frame, text="Next Image", command=callback).grid(column=1, row=1, sticky="nesw")
pict = Pict_Frame(parent=frame, bg="grey",# bd=10, relief="raised",
maxupdate=2,
picture=images[ind])
pict.grid(column=1, row=2, sticky="nesw")
tix.Button(frame, text="Next Image", command=callback).grid(column=1, row=3, sticky="nesw")
root.mainloop()
The only issue I have had with this code is that when I am using the widget in a full-screen application the re-sizing doesn't work as intended, when using the grid method and setting the weight of the right column to 1 (with the pict widget) and the left column (empty) to 1, the right column ends up taking approx 2/3rds the width of the screen.
I suspect this is due to explicitly setting the size of the image, which then makes it wider, meaning the geometry manager wants to make it wider still (ad infinitum) until it reaches some equilibrium. But if anyone can shed any light on this (or even a solution) it would be appreciated.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With