This code is a complete hack. I’ve probably done it wrong in someones eyes out there, and if by chance you are better at this sort of thing, and see room for improvements please pass it along. I can only stand so much insanity building this thing. It works for what I need.
This code comes from lots of different sources:
http://www.pygtk.org/articles/writing-a-custom-widget-using-pygtk/writing-a-custom-widget-using-pygtk.htm
http://faq.pygtk.org/index.py?req=show&file=faq13.045.htp # Create a custom gtk.CellRenderer
http://faq.pygtk.org/index.py?req=show&file=faq13.041.htp # How to add images/animations
http://nullege.com/codes/show/src%40t%40r%40translate-HEAD%40src%40trunk%40virtaal%40virtaal%40views%40widgets%40cellrendererwidget.py/147/gtk.CellEditable/python # Add a widget to a cell
Now that I’ve given credit where credit is due … time to get into the code.
Here’s the complete package, images and everything.
#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright 2009 Zuza Software Foundation # Copyright 2012 Eugene R. Miller # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, see <http: //www.gnu.org/licenses>. # # This code comes from lots of different sources: # http://faq.pygtk.org/index.py?req=show&file=faq13.045.htp # Create a custom gtk.CellRenderer # http://faq.pygtk.org/index.py?req=show&file=faq13.041.htp # How to add images/animations # http://nullege.com/codes/show/src%40t%40r%40translate-HEAD%40src%40trunk%40virtaal%40virtaal%40views%40widgets%40cellrendererwidget.py/147/gtk.CellEditable/python # how to add a widget. import gtk, random import pango import gtk, pygtk, gobject from gobject import idle_add, PARAM_READWRITE, SIGNAL_RUN_FIRST, TYPE_PYOBJECT star_grey = gtk.gdk.pixbuf_new_from_file("star.grey.png") star_hover = gtk.gdk.pixbuf_new_from_file("star.selected.png") star_selected = gtk.gdk.pixbuf_new_from_file("star.hover.png") no_star = gtk.gdk.pixbuf_new_from_file("no.star.png") no_star_selected = gtk.gdk.pixbuf_new_from_file("no.star.selected.png") no_star_grey = gtk.gdk.pixbuf_new_from_file("no.star.grey.png") class CellRendererStar(gtk.GenericCellRenderer):    __gproperties__ = {       "rating": (gobject.TYPE_INT, "Rating", "Rating", 0, 10, 0, gobject.PARAM_READWRITE),    }    def __init__(self, size=30, data_col=1):       self.__gobject_init__()       self.props.mode = gtk.CELL_RENDERER_MODE_EDITABLE       self.image = None       self.rating = None       self.size = size       self.star_grey = star_grey.scale_simple(self.size, self.size, gtk.gdk.INTERP_BILINEAR)       self.star_selected = star_selected.scale_simple(self.size, self.size, gtk.gdk.INTERP_BILINEAR)       self.star_hover = star_hover.scale_simple(self.size, self.size, gtk.gdk.INTERP_BILINEAR)       self.no_star = no_star.scale_simple(self.size, self.size, gtk.gdk.INTERP_BILINEAR)       self.no_star_grey = no_star_grey.scale_simple(self.size, self.size, gtk.gdk.INTERP_BILINEAR)       self.no_star_selected = no_star_selected.scale_simple(self.size, self.size, gtk.gdk.INTERP_BILINEAR)       self.rating = -1       self.editablemap = {}       self.stars_hover = {}       self.stars = {}       self.data_col = data_col       self._starting_edit = False    def do_set_property(self, pspec, value):       # print "PSPEC:",pspec.name,"=",value       if pspec.name == 'rating':          if value != self.rating:             # print "REMADE!"             self.rating = value             self.make_stars()       setattr(self, pspec.name, value)    def make_stars(self):       rating = self.rating       if self.stars.has_key(rating):          self.image = self.stars[rating]          self.image.show()          return       self.image = gtk.Image()       self.image.show()       # self.widget.value = rating       num_of_stars = 6       target_width = self.size * num_of_stars       target_height = self.size       # gtk.gdk.Pixbuf(colorspace, has_alpha, bits_per_sample, width, height)       self.pb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, target_width, target_height)       for i in range(0,num_of_stars,1):          if i == 0:             if rating == 0:                self.no_star.copy_area(                   0, # src_x                   0, # src_y                   self.size, # width                   self.size, # height                   self.pb, # dest_pixbuf                   (i*self.size), # dest_x                   0, # dest_y                )             elif rating >= i:                self.no_star_selected.copy_area(                   0, # src_x                   0, # src_y                   self.size, # width                   self.size, # height                   self.pb, # dest_pixbuf                   (i*self.size), # dest_x                   0, # dest_y                )             else:                self.no_star_grey.copy_area(                   0, # src_x                   0, # src_y                   self.size, # width                   self.size, # height                   self.pb, # dest_pixbuf                   (i*self.size), # dest_x                   0, # dest_y                )             continue          if rating >= i:             # copy_area(src_x, src_y, width, height, dest_pixbuf, dest_x, dest_y)             self.star_selected.copy_area(                0, # src_x                0, # src_y                self.size, # width                self.size, # height                self.pb, # dest_pixbuf                (i*self.size), # dest_x                0, # dest_y             )          else:             self.star_grey.copy_area(                0, # src_x                0, # src_y                self.size, # width                self.size, # height                self.pb, # dest_pixbuf                (i*self.size), # dest_x                0, # dest_y             )       self.image.set_from_pixbuf(self.pb)       self.image.show()       self.stars[rating] = self.image    def make_stars_hover(self, rating):       self.image = gtk.Image()       self.image.show()       if self.stars_hover.has_key(rating):          self.image = self.stars_hover[rating]          self.image.show()          return       num_of_stars = 6       target_width = self.size * num_of_stars       target_height = self.size       self.pb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, target_width, target_height)       for i in range(0,num_of_stars,1):          if i == 0:             if rating == 0:                self.no_star.copy_area(                   0, # src_x                   0, # src_y                   self.size, # width                   self.size, # height                   self.pb, # dest_pixbuf                   (i*self.size), # dest_x                   0, # dest_y                )             elif rating >= i:                self.no_star_selected.copy_area(                   0, # src_x                   0, # src_y                   self.size, # width                   self.size, # height                   self.pb, # dest_pixbuf                   (i*self.size), # dest_x                   0, # dest_y                )             else:                self.no_star_grey.copy_area(                   0, # src_x                   0, # src_y                   self.size, # width                   self.size, # height                   self.pb, # dest_pixbuf                   (i*self.size), # dest_x                   0, # dest_y                )             continue          if rating >= i:             # copy_area(src_x, src_y, width, height, dest_pixbuf, dest_x, dest_y)             self.star_hover.copy_area(                0, # src_x                0, # src_y                self.size, # width                self.size, # height                self.pb, # dest_pixbuf                (i*self.size), # dest_x                0, # dest_y             )          else:             self.star_grey.copy_area(                0, # src_x                0, # src_y                self.size, # width                self.size, # height                self.pb, # dest_pixbuf                (i*self.size), # dest_x                0, # dest_y             )       self.image.set_from_pixbuf(self.pb)       self.image.show()       self.stars_hover[rating] = self.image    def do_get_property(self, pspec):       return getattr(self, pspec.name)    def func(self, model, path, iter, (image, tree)):       # print "FUNC:",model,path,iter, (image, tree)       if model.get_value(iter, 0) == image:          self.redraw = 1          cell_area = tree.get_cell_area(path, tree.get_column(0))          tree.queue_draw_area(cell_area.x, cell_area.y, cell_area.width, cell_area.height)    def animation_timeout(self, tree, image):       if image.get_storage_type() == gtk.IMAGE_ANIMATION:          self.redraw = 0          image.get_data('iter').advance()          model = tree.get_model()          model.foreach(self.func, (image, tree))          if self.redraw:             gobject.timeout_add(image.get_data('iter').get_delay_time(),                self.animation_timeout, tree, image)          else:             image.set_data('iter', None)    def on_activate(event, widget, path, background_area, cell_area, flags):       print "ON_ACTIVATE:",event, widget, path, background_area, cell_area, flags    def on_button_press(self, tree, event):       # "on_button_press:", tree, event       try:          path, col, x, y = tree.get_path_at_pos(int(event.x), int(event.y))       except TypeError:          return       rends = col.get_cell_renderers()       if rends[0] != self:          return       cell_area = tree.get_cell_area(path, tree.get_column(0))       rating = x / self.size       # print "x, y:", x, y       model = tree.get_model()       model[path][self.data_col] = rating       expose_area = tree.get_background_area(path, col)       flags = gtk.CELL_RENDERER_SELECTED       self.make_stars_hover(rating)       self.on_render(tree.get_bin_window(), tree, tree.get_background_area(path, col), tree.get_cell_area(path, col), expose_area, flags)    def on_motion_notify(self, tree, event):       try:          path, col, x, y = tree.get_path_at_pos(int(event.x), int(event.y))       except TypeError:          return       rends = col.get_cell_renderers()       flags = 0       if rends[0] != self:          return       expose_area = background_area = tree.get_background_area(path, col)       cell_area = tree.get_cell_area(path, tree.get_column(0))       x = event.x       y = event.y       window = tree.get_bin_window()       x, y, mask = window.get_pointer()       if x > (expose_area.x+5) and int(x) < (expose_area.x + (expose_area.width-5)) and int(y) > expose_area.y and int(y) < (expose_area.y + cell_area.height):          # print "IN!"          flags = gtk.CELL_RENDERER_PRELIT       # self.make_stars_hover(rating)       self.on_render(window, tree, background_area , tree.get_cell_area(path, col), expose_area, flags)    def on_render(self, window, widget, background_area, cell_area, expose_area, flags):       self.props.mode = gtk.CELL_RENDERER_MODE_INERT       if not self.image:          return       # self.make_stars()       if flags & gtk.CELL_RENDERER_PRELIT:          x, y, mask = window.get_pointer()          try:             # print "background_area:",background_area             path, col, _x, _y = widget.get_path_at_pos(int(x), int(y))             if x > background_area.x and x < (background_area.x + background_area.width) and y > background_area.y and y < (background_area.y + background_area.height):                # print "x:(%s) > xa:(%s) and x:(%s) < xa+w:(%s)" % (_x, background_area.x, _x, (background_area.x + background_area.width) )                rating = _x / self.size                self.make_stars_hover(rating)             else:                # print "OUT!"                self.make_stars()             # self.make_stars_hover(rating)          except TypeError:             # print "TYPEERROR!"             self.make_stars()       else:          self.make_stars()       pix_rect = gtk.gdk.Rectangle()       pix_rect.x, pix_rect.y, pix_rect.width, pix_rect.height = self.on_get_size(widget, cell_area)       pix_rect.x += cell_area.x       pix_rect.y += cell_area.y       pix_rect.width -= 2 * self.get_property("xpad")       pix_rect.height -= 2 * self.get_property("ypad")       draw_rect = cell_area.intersect(pix_rect)       draw_rect = expose_area.intersect(draw_rect)       pix = self.image.get_pixbuf()       window.draw_pixbuf(widget.style.black_gc, pix, draw_rect.x-pix_rect.x, draw_rect.y-pix_rect.y, draw_rect.x, draw_rect.y, draw_rect.width, draw_rect.height, gtk.gdk.RGB_DITHER_NONE, 0, 0)    def on_get_size(self, widget, cell_area):       if not self.image:          return 0, 0, 0, 0       if self.image.get_storage_type() == gtk.IMAGE_ANIMATION:          animation = self.image.get_animation()          pix_rect = animation.get_iter().get_pixbuf()       elif self.image.get_storage_type() == gtk.IMAGE_PIXBUF:          pix = self.image.get_pixbuf()       else:          return 0, 0, 0, 0       pixbuf_width = pix.get_width()       pixbuf_height = pix.get_height()       calc_width = self.get_property("xpad") * 2 + pixbuf_width       calc_height = self.get_property("ypad") * 2 + pixbuf_height       x_offset = 0       y_offset = 0       if cell_area and pixbuf_width > 0 and pixbuf_height > 0:          x_offset = self.get_property("xalign") * (cell_area.width - calc_width - self.get_property("xpad"))          y_offset = self.get_property("yalign") * (cell_area.height - calc_height - self.get_property("ypad"))       # print "on_get_size:",x_offset, y_offset, calc_width, calc_height       return x_offset, y_offset, calc_width, calc_height gobject.type_register(CellRendererStar) if __name__ == "__main__":    class Tree(gtk.TreeView):       def __init__(self):          self.store = gtk.ListStore(str, int, int)          gtk.TreeView.__init__(self)          self.set_model(self.store)          self.set_headers_visible(True)          self.append_column(gtk.TreeViewColumn('First', gtk.CellRendererText(), text=0))          # self.append_column(gtk.TreeViewColumn('Second', CellRendererWidget(lambda widget:widget.get_label()), widget=1))          cell = CellRendererStar(30,1)          cell.set_property('xalign',0.0)          self.append_column(gtk.TreeViewColumn('Erm', cell, rating=1))          self.connect("motion-notify-event", cell.on_motion_notify)          self.connect("button-press-event", cell.on_button_press)          cell = CellRendererStar(30,2)          cell.set_property('xalign',0.0)          self.append_column(gtk.TreeViewColumn('Steph', cell, rating=2))          self.connect("motion-notify-event", cell.on_motion_notify)          self.connect("button-press-event", cell.on_button_press)          self.connect("key-release-event", self.on_key_release)          self.set_property('reorderable',True)          self.set_property('rubber-banding', True)          select = self.get_selection()          select.set_mode(gtk.SELECTION_MULTIPLE)       def insert(self, name):          for r in range(1,20):             self.store.append(["%s.%s" % (r, name), random.randint(0, 7), random.randint(0, 7)])       def on_key_release(self, widget, event):          print "on_key_release:",event.keyval,'string:', event.string, 'keycode:', event.hardware_keycode, 'state:',event.state          if event.hardware_keycode == 119 and event.state == 0:             selection = self.get_selection()             selected_rows = selection.get_selected_rows()             print "selected_rows:",selected_rows             liststore, rows = selected_rows             rows.reverse()             for r in rows:                del liststore[r]    def on_change(*args):       print "CHANGED!", args    w = gtk.Window()    w.set_position(gtk.WIN_POS_CENTER)    w.connect('delete-event', gtk.main_quit)    t = Tree()    m = t.get_model()    m.connect("row-changed", on_change)    t.insert('foo')    w.add(t)    w.show_all()    gtk.main()