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.
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!






October 5, 2008 at 8:35 pm |
Thanks for posting this code! I needed something just like this, and (nicely) stumbled across this web site!
October 30, 2008 at 2:04 pm |
Hey man,
I was browsing through the web that i found ur blog..
great python application u’ve got…
i assume u r doin mechatronic engineering at uq..
i am doing csse1001 at the moment, kinda stunned by what python can actually do..
-rahim-
April 6, 2009 at 11:57 am |
Haha! I needed this.
It’s actually hard(-ish) to use get_allocation() correctly.