I’ve been doing a little bit of GUI work in GTK. Something I really missed was a dynamically resizable image control – the gtk.Image object just seems to assume the same dimensions as the picture it’s displaying.
I found a good tutorial about rendering a custom widget using Cairo, with the widget itself inheriting from gtk.DrawingArea. Another useful resource was a C++/gtkmm article about drawing a gtk.gdk.Pixbuf object in a Cairo context. Here’s some screenshots of a demo program in Ubuntu 8.04 and Windows XP.

Scaled, aspect ratio maintained.

Stretched, aspect ratio out the window.

Aspect preserved, black background.

Running in Windows XP.

Prevented from exceeding original resolution.

Enlargement allowed, constant aspect enforced.
resizableimage.py
Here’s the key bits of code for the ResizableImage control:
import pygtk
import gtk
from gtk import DrawingArea
class ResizableImage(DrawingArea):
def __init__(self, aspect=True, enlarge=False,
interp=gtk.gdk.INTERP_NEAREST, backcolor=None, max=(1600,1200)):
"""Construct a ResizableImage control.
Parameters:
aspect -- Maintain aspect ratio?
enlarge -- Allow image to be scaled up?
interp -- Method of interpolation to be used.
backcolor -- Tuple (R, G, B) with values ranging from 0 to 1,
or None for transparent.
max -- Max dimensions for internal image (width, height).
"""
DrawingArea.__init__(self)
self.pixbuf = None
...
self.connect('expose_event', self.expose)
The expose event is triggered when the control needs to be repainted. Here we just obtain a Cairo context and call draw().
def expose(self, widget, event):
# Load Cairo drawing context.
self.context = self.window.cairo_create()
# Set a clip region.
self.context.rectangle(
event.area.x, event.area.y,
event.area.width, event.area.height)
self.context.clip()
# Render image.
self.draw(self.context)
return False
Then this is where the image actually gets rendered to the control. The size of the image is determined by resizeToFit(). A resized version of the gtk.Pixbuf member is created using scale_simple(). The gtk.Pixbuf image is displayed using set_source_pixbuf().
def draw(self, context):
# Get dimensions.
rect = self.get_allocation()
x, y = rect.x, rect.y
# Remove parent offset, if any.
parent = self.get_parent()
if parent:
offset = parent.get_allocation()
x -= offset.x
y -= offset.y
# Fill background color.
if self.backcolor:
context.rectangle(x, y, rect.width, rect.height)
context.set_source_rgb(*self.backcolor)
context.fill_preserve()
# Check if there is an image.
if not self.pixbuf:
return
width, height = resizeToFit(
(self.pixbuf.get_width(), self.pixbuf.get_height()),
(rect.width, rect.height),
self.aspect,
self.enlarge)
x = x + (rect.width - width) / 2
y = y + (rect.height - height) / 2
context.set_source_pixbuf(
self.pixbuf.scale_simple(width, height, self.interp), x, y)
context.paint()
def set_from_pixbuf(self, pixbuf):
width, height = pixbuf.get_width(), pixbuf.get_height()
# Limit size of internal pixbuf to increase speed.
if not self.max or (width < self.max[0] and height < self.max[1]):
self.pixbuf = pixbuf
else:
width, height = resizeToFit((width, height), self.max)
self.pixbuf = pixbuf.scale_simple(
width, height,
gtk.gdk.INTERP_BILINEAR)
self.invalidate()
The self.max variable limits the size of the internal gtk.Pixbuf to prevent slow render times for larger images. The default maximum is 1600×1200. It can be disabled by setting max=None in the constructor call.
def set_from_file(self, filename):
self.set_from_pixbuf(gtk.gdk.pixbuf_new_from_file(filename))
def invalidate(self):
self.queue_draw()
...
A couple of functions external to the class:
def resizeToFit(image, frame, aspect=True, enlarge=False):
"""Resizes a rectangle to fit within another.
Parameters:
image -- A tuple of the original dimensions (width, height).
frame -- A tuple of the target dimensions (width, height).
aspect -- Maintain aspect ratio?
enlarge -- Allow image to be scaled up?
"""
if aspect:
return scaleToFit(image, frame, enlarge)
else:
return stretchToFit(image, frame, enlarge)
def scaleToFit(image, frame, enlarge=False):
image_width, image_height = image
frame_width, frame_height = frame
image_aspect = float(image_width) / image_height
frame_aspect = float(frame_width) / frame_height
# Determine maximum width/height (prevent up-scaling).
if not enlarge:
max_width = min(frame_width, image_width)
max_height = min(frame_height, image_height)
else:
max_width = frame_width
max_height = frame_height
# Frame is wider than image.
if frame_aspect > image_aspect:
height = max_height
width = int(height * image_aspect)
# Frame is taller than image.
else:
width = max_width
height = int(width / image_aspect)
return (width, height)
def stretchToFit(image, frame, enlarge=False):
image_width, image_height = image
frame_width, frame_height = frame
# Stop image from being blown up.
if not enlarge:
width = min(frame_width, image_width)
height = min(frame_height, image_height)
else:
width = frame_width
height = frame_height
return (width, height)
demogtk.py
Here’s most of the code for the demo program:
import pygtk
import gtk
import gtk.glade
import os
from resizableimage import ResizableImage
class DemoGtk:
def __init__(self):
# Initiate GUI.
self.gladefile = "demo.glade"
self.wTree = gtk.glade.XML(self.gladefile)
# Find widgets that we use.
self.frame = self.wTree.get_widget('pictureframe')
self.txtFile = self.wTree.get_widget('txtFile')
self.chkAspect = self.wTree.get_widget('chkAspect')
self.chkEnlarge = self.wTree.get_widget('chkEnlarge')
# Connect signals.
signals = {
'on_window_destroy': self.destroy,
'on_txtFile_changed': self.openFile,
'on_chkAspect_clicked': self.changeAspect,
'on_chkEnlarge_clicked': self.changeEnlarge
}
self.wTree.signal_autoconnect(signals)
# Do some runtime GUI.
self.image = ResizableImage(
self.chkAspect.get_active(),
self.chkEnlarge.get_active(),
gtk.gdk.INTERP_BILINEAR)
self.image.show()
self.frame.add(self.image)
Options for interpolation type are gtk.gdk.INTERP_NEAREST, gtk.gdk.INTERP_TILES, gtk.gdk.INTERP_BILINEAR, gtk.gdk.INTERP_HYPER. I used “bilinear” here to get decent quality pictures. The “nearest” option is alright if you need more performance.
def destroy(self, widget, data=None):
gtk.main_quit()
def openFile(self, widget, data=None):
filename = widget.get_text()
if not os.path.isfile(filename):
return
self.image.set_from_file(filename)
def changeAspect(self, widget, data=None):
self.image.set_aspect(widget.get_active())
def changeEnlarge(self, widget, data=None):
self.image.set_enlarge(widget.get_active())
def main():
gui = DemoGtk()
gtk.main()
if __name__ == '__main__':
main()
Doesn’t look like I’m able to upload the source to WordPress. Just let me know in the comments if you’re after a bit more detail!