Writing a Custom Widget Using PyGTK

One of the things that I wanted to add to my simple PyWine application was an easy way for people to rate their wine. There were lots of different ways to do it but since I was looking for a tutorial to write I decided that I wanted to do it the way that you rate songs in iTunes. If you’ve never used iTunes before, you can rate songs on a sliding scale from zero to five using stars. It basically functions like a slider or a Horizontal Scale except that when drawing it’s not a line, it’s a row of stars.

Python PyGTK Windows

The full source for this tutorial can be downloaded here.

The three the most useful links that I found on this subject were: A song for the lovers, the writing a widget turotial on the PyGTK website, and the widget.py example in the PyGTK cvs.

The skeleton of the following code will be mostly based off of the widget.py example, but since this example will try to accomplish a bit more there will be some extra code. In order to understand this tutorial better I suggest you give widget.py a couple of reads.

The starting point is a file names starhscale.py which starts off with some rather standard python stuff:

[code lang=”python”]
#!/usr/bin/env python

import gtk
import gobject
from gtk import gdk
raise SystemExit

import pygtk
if gtk.pygtk_version < (2, 0): print "PyGtk 2.0 or later required for this widget" raise SystemExit [/code]

Not too much surprising there, now it’s time to create and initialize our class, we’ll call it StarHScale:

[code lang=”python”]
class StarHScale(gtk.Widget):
“””A horizontal Scale Widget that attempts to mimic the star
rating scheme used in iTunes”””

def __init__(self, max_stars=5, stars=0):
“””Initialization, numstars is the total number
of stars that may be visible, and stars is the current
number of stars to draw”””

#Initialize the Widget

self.max_stars = max_stars
self.stars = stars

# Init the list to blank
self.sizes = []
for count in range(0,self.max_stars):
self.sizes.append((count * PIXMAP_SIZE) + BORDER_WIDTH)

So what’s happening here? Well the first thing you see is the definition of our StarHScale widget that is a subclass of gtk.Widget, which is the base class for all widgets in PyGTK. Then we have a rather simple __init__ routine where we set some parameters (the max number of stars to show and the current number of stars to show) and initialize the parent class.

You’ll also notice that at the end of the function there is a list created, this list maps the X (horizontal) position of each star. It might not make much sense now, but it will become clear when you see how it is used. PIXMAP_SIZE and BORDER_WIDTH are “globals” that are defined outside of the StarHScale class as follows:

[code lang=”python”]

The next function we will write is the do_realize() function. The do_realize() function is related to the gtk.Widget.realize() function and is called when a widget is supposed to allocate its GDK windowing resources.

It may seem a bit complicated, but the do_realize() function is simply where widgets create their GDK windows resources (most probably a gtk.gdk.Window) where the widget will eventually be drawn to). In order to fully understand this it may be helpful to understand what a gtk.gdk.Window is, here is an explanation from the PyGTK documentation:

gtk.gdk.Window is a rectangular region on the screen. It’s a low-level object, used to implement high-level objects such as gtk.Widget and gtk.Window. A gtk.Window is a toplevel window, the object a user might think of as a “window” with a titlebar and so on. A gtk.Window may contain several gtk.gdk.Window objects since most widgets use a gtk.gdk.Window.

A gtk.gdk.Window object interacts with the native window system for input and events. Some gtk.Widget objects do not have an associated gtk.gdk.Window and therefore cannot receive events. To receive events on behalf of these “windowless” widgets a gtk.EventBox must be used.

So a gtk.gdk.Window is not a “window” as we normally think of one, it’s basically a rectangular region on the screen that will be used for “drawing” of some sort. So for our StarHScale widget, it’s gtk.gdk.Window will be the area where the stars will be drawn. If you have done programming with other toolkits or other languages it may be helpful to think of this as the “surface” that the widget draws on. Much of the do_realize() code is taken from the widget.py example:

[code lang=”python”]
def do_realize(self):
“””Called when the widget should create all of its
windowing resources. We will create our gtk.gdk.Window
and load our star pixmap.”””

# First set an internal flag telling that we’re realized
self.set_flags(self.flags() | gtk.REALIZED)

# Create a new gdk.Window which we can draw on.
# Also say that we want to receive exposure events
# and button click and button press events

self.window = gdk.Window(
event_mask=self.get_events() | gdk.EXPOSURE_MASK

# Associate the gdk.Window with ourselves, Gtk+ needs a reference
# between the widget and the gdk window

# Attach the style to the gdk.Window, a style contains colors and
# GC contextes used for drawing

# The default color of the background should be what
# the style (theme engine) tells us.
self.style.set_background(self.window, gtk.STATE_NORMAL)
# load the star xpm
self.pixmap, mask = gtk.gdk.pixmap_create_from_xpm_d(
self.window, self.style.bg[gtk.STATE_NORMAL], STAR_PIXMAP)

# self.style is a gtk.Style object, self.style.fg_gc is
# an array or graphic contexts used for drawing the forground
# colours
self.gc = self.style.fg_gc[gtk.STATE_NORMAL]

self.connect(“motion_notify_event”, self.motion_notify_event)

There is quite a bit of code here so I’ll take some time to explain it. The first step is to set a flag so that lets us, and anyone else that wants to know, that we have been realized – that we have a gtk.gdk.Window associated with ourselves.

The next step is to actually create the gtk.gdk.Window that will be associated with the StarHScale widget. When we create it we also set many of it’s attributes. You can read more about all the available attributes in the PyGTK documentation but here are the attributes that we are setting:

parent: a gtk.gdk.Window
width: the width of the window in pixels
height: the height of the window in pixels
window_type: the window type
event_mask: the bitmask of events received by the window
wclass: the class of window – either gtk.gdk.INPUT_OUTPUT or gtk.gdk.INPUT_ONLY

We add a few events to the event mask of the gtk.gdk.Window because this widget will be interacting with the mouse. Then we make some necessary connections between the gtk.gdk.Window, the widget, and the widgets style. Finally we set the background colour and move the window into the position that has been allocated for us (self.allocation).

The next step is where the do_realize() code begins to diverge from the widget.py example. The next step is where we create our star pixmap using the pixmap_create_from_xpm_d function:

[code lang=”python”]
# load the star xpm
self.pixmap, mask = gtk.gdk.pixmap_create_from_xpm_d(
, self.style.bg[gtk.STATE_NORMAL]

Here is a description of what a gtk.gdk.Pixmap is:

A gtk.gdk.Pixmap is an offscreen gtk.gdk.Drawable. It can be drawn upon with the standard gtk.gdk.Drawable drawing primitives, then copied to another gtk.gdk.Drawable (such as a gtk.gdk.Window) with the draw_drawable() method. The depth of a pixmap is the number of bits per pixels. A bitmaps are simply a gtk.gdk.Pixmap with a depth of 1. (That is, they are monochrome pixmaps – each pixel can be either on or off).

What we will use the pixmap for is the drawing of each of our stars. Since we want the widget to be portable without having an xpm file around we simply load it’s data. To do so we have to define the STAR_PIXMAP “global” outside of our StarHScale as follows:

[code lang=”python”]
STAR_PIXMAP = [“22 22 77 1″,
” c None”,
“. c #626260″,
“+ c #5E5F5C”,
“@ c #636461″,
“# c #949492″,
“$ c #62625F”,
“% c #6E6E6B”,
“& c #AEAEAC”,
“* c #757673″,
“= c #61625F”,
“- c #9C9C9B”,
“; c #ACACAB”,
“> c #9F9F9E”,
“, c #61635F”,
“‘ c #656663″,
“) c #A5A5A4″,
“! c #ADADAB”,
“~ c #646562″,
“{ c #61615F”,
“] c #6C6D6A”,
“^ c #797977″,
“/ c #868684″,
“( c #A0A19E”,
“_ c #AAAAA8″,
“: c #A3A3A2″,
“< c #AAAAA7", "[ c #9F9F9F", "} c #888887", "| c #7E7E7C", "1 c #6C6C69", "2 c #626360", "3 c #A5A5A3", "4 c #ABABAA", "5 c #A9A9A7", "6 c #A2A2A1", "7 c #A3A3A1", "8 c #A7A7A6", "9 c #A8A8A6", "0 c #686866", "a c #A4A4A2", "b c #A4A4A3", "c c #A1A19F", "d c #9D9D9C", "e c #9D9D9B", "f c #A7A7A5", "g c #666664", "h c #A1A1A0", "i c #9E9E9D", "j c #646461", "k c #A6A6A4", "l c #A0A09F", "m c #9F9F9D", "n c #A9A9A8", "o c #A0A09E", "p c #9B9B9A", "q c #ACACAA", "r c #60615E", "s c #ADADAC", "t c #A2A2A0", "u c #A8A8A7", "v c #6E6F6C", "w c #787976", "x c #969695", "y c #8B8B8A", "z c #91918F", "A c #71716E", "B c #636360", "C c #686966", "D c #999997", "E c #71716F", "F c #61615E", "G c #6C6C6A", "H c #616260", "I c #5F605E", "J c #5D5E5B", "K c #565654", "L c #5F5F5D", " ", " ", " . ", " + ", " @#$ ", " %&* ", " =-;>, “,
” ‘;)!’ “,
” ~{{]^/(_:< [}|*1@, ", " 23&4_5367895&80 ", " 2a4b:7c>def)g “,
” 2c4:h>id56j “,
” {k8lmeln2 “,
” j8bmoppqr “,
” {stusnd4v “,
” ws;x@yq;/ “,
” zfAB {CmD{ “,
” rE{ FGH “,
” IJ KL “,
” “,
” “,
” “]

The star is based off of star found in the Art Libre Set of the wonderful Tango Desktop Project. I simply darkened it a bit.

Then we make a quick reference to the normal state foreground gtk.gdk.GC (graphic context) associated with our style. A gtk.gdk.GC is simply an object that “encapsulates information about the way things are drawn, such as the foreground color or line width. By using graphics contexts, the number of arguments to each drawing call is greatly reduced, and communication overhead is minimized, since identical arguments do not need to be passed repeatedly. (From the PYGTK Docs )” So it’s basically a bunch of drawing settings encapsulated in one simple object.

Finally to finish off the do_realize() function we connect ourselves with the “motion_notify_event” which we will use to track when the user moves the mouse over our widget.

The next step in our widget creation is the do_unrealize() function, which is called when a widget should free all of its resources. The widget.py example calls:

[code lang=”python”]

But I got a type error running that, so instead I simply destroyed the window. I’m not entirely sure what the correct approach is, or if one even has to worry about clearing the resources, either way this is code that i used:

[code lang=”python”]
def do_unrealize(self):
# The do_unrealized method is responsible for freeing the GDK resources
# De-associate the window we created in do_realize with ourselves

The next two functions deal with the size of our widget. The first function do_size_request() is called by PyGTK so that PyGTK can figure out how large the widget wants to be. The second function, do_size_allocate() is called by PyGTK in order to tell the widget how large it should actually be:

[code lang=”python”]
def do_size_request(self, requisition):
“””From Widget.py: The do_size_request method Gtk+ is calling
on a widget to ask it the widget how large it wishes to be.
It’s not guaranteed that gtk+ will actually give this size
to the widget. So we will send gtk+ the size needed for
the maximum amount of stars”””

requisition.height = PIXMAP_SIZE
requisition.width = (PIXMAP_SIZE * self.max_stars) + (BORDER_WIDTH * 2)

def do_size_allocate(self, allocation):
“””The do_size_allocate is called by when the actual
size is known and the widget is told how much space
could actually be allocated Save the allocated space
self.allocation = allocation. The following code is
identical to the widget.py example”””

if self.flags() & gtk.REALIZED:

The next function is the do_expose_event() function, which is called when the widget should actually draw itself. For the StarHScale this function is actually pretty simple:

[code lang=”python”]
def do_expose_event(self, event):
“””This is where the widget must draw itself.”””

#Draw the correct number of stars. Each time you draw another star
#move over by 22 pixels. which is the size of the star.
for count in range(0,self.stars):
self.window.draw_drawable(self.gc, self.pixmap, 0, 0
, self.sizes[count]
, 0,-1, -1)

Basically we simply loop through the current number of stars (self.stars) and draw our star pixmap to the window using the draw_drawable function. We use the self.sizes list (which we calculated in the __init__ function) to determine the x position where we will draw the star.

Now comes the time where we actually need to let the user interact with the widget and show and hide the stars. To do so we need to pay attention to the “motion_notify_event” and the “button_press_event”. One thing you may have noticed in the do_realize() function is that we pay attention to the gtk.POINTER_MOTION_MASK and the gtk.POINTER_MOTION_HINT_MASK, the reason for this is explained in the PyGTK documentation:

It turns out, however, that there is a problem with just specifying POINTER_MOTION_MASK. This will cause the server to add a new motion event to the event queue every time the user moves the mouse. Imagine that it takes us 0.1 seconds to handle a motion event, but the X server queues a new motion event every 0.05 seconds. We will soon get way behind the users drawing. If the user draws for 5 seconds, it will take us another 5 seconds to catch up after they release the mouse button! What we would like is to only get one motion event for each event we process. The way to do this is to specify POINTER_MOTION_HINT_MASK.

When we specify POINTER_MOTION_HINT_MASK, the server sends us a motion event the first time the pointer moves after entering our window, or after a button press or release event. Subsequent motion events will be suppressed until we explicitly ask for the position of the pointer using the gtk.gdk.Window method:

x, y, mask = window.get_pointer()

Our motion_notify_event handler is as follows:

[code lang=”python”]
def motion_notify_event(self, widget, event):
# if this is a hint, then let’s get all the necessary
# information, if not it’s all we need.
if event.is_hint:
x, y, state = event.window.get_pointer()
x = event.x
y = event.y
state = event.state

new_stars = 0
if (state & gtk.gdk.BUTTON1_MASK):
# loop through the sizes and see if the
# number of stars should change

This function is pretty simple, first we check to see if the event is a hint or not, if it is a hint we ask GTK+ to get us the real pointer information. If it is not a hint then we just collect the information from the passed gtk.gdk.Event object.

Then we check the events state to make sure that the left mouse button is down, and if it is we pass the x coordinate of the mouse pointer to the self.check_for_new_stars() function which will determine how many stars should be shown.

The other event that lets the user hide and show stars is the button press event which we handle using the do_button_press_event() gtk.Wdiget virtual method that gets called when a button is pressed on the widget:

[code lang=”python”]
def do_button_press_event(self, event):
“””The button press event virtual method”””

# make sure it was the first button
if event.button == 1:
#check for new stars
return True

This function is very simple, first we check to make sure that it was the left button that fired the gtk.gdk.BUTTON_PRESS_EVENT, and if it was we pass event.x (the position the mouse was in at the time of the event) to the check_for_new_stars() function.

[code lang=”python”]
def check_for_new_stars(self, xPos):
“””This function will determine how many stars
will be show based on an x coordinate. If the
number of stars changes the widget will be invalidated
and the new number drawn”””

# loop through the sizes and see if the
# number of stars should change
new_stars = 0
for size in self.sizes:
if (xPos < size): # we've reached the star number break new_stars = new_stars + 1 #set the new value self.set_value(new_stars) [/code] check_for_new_stars() is a relatively straight-forward function. It takes an x coordinate as a parameter and then determines how many stars should be visible based on that. To see how many stars should be visible we loop through the self.sizes list and compare the pre-calculated starting point of each star with the passed in x coordinate. We keep adding more stars until the x coordinate is no longer larger then the starting position of the current star. Then we make sure that a new star should be added and if it is we call self.set_value() to set the number of stars. [code lang="python"] def set_value(self, value): """Sets the current number of stars that will be drawn. If the number is different then the current number the widget will be redrawn""" if (value >= 0):
if (self.stars != value):
self.stars = value
#check for the maximum
if (self.stars > self.max_stars):
self.stars = self.max_stars
#redraw the widget

set_value() is another simple function that performs a few validation checks and then sets the current number of stars. If the number of stars has changed, the widget will be redrawn.

Now there are three functions remaining and these are simply to make the widget more usable. They are pretty self explanatory:

[code lang=”python”]
def get_value(self):
“””Get the current number of stars displayed”””

return self.stars

def set_max_value(self, max_value):
“””set the maximum number of stars”””

if (self.max_stars != max_value):
“””Save the old max in case it is less then the
current number of stars, in which case we will
have to redraw”””

if (max_value > 0):
self.max_stars = max_value
#reinit the sizes list (should really be a separate function
self.sizes = []
for count in range(0,self.max_stars):
self.sizes.append((count * PIXMAP_SIZE) + BORDER_WIDTH)
“””do we have to change the current number of
if (self.stars > self.max_stars):

def get_max_value(self):
“””Get the maximum number of stars that can be shown”””

return self.max_stars

Now we finish of starhscale.py with a little bit of code that will simply create a window and add the StarHScale widget to that window if someone executes the starhscale.py file directly:

[code lang=”python”]
if __name__ == “__main__”:
# register the class as a Gtk widget

win = gtk.Window()
win.connect(‘delete-event’, gtk.main_quit)

starScale = StarHScale(10,5)


So if you run the file you should see the following:

Whew! So that’s it, I hope that you found this tutorial useful, now the next step (in the next tutorial) is to add it to the gtk.TreeView.

The full source can be downloaded here.

29 thoughts on “Writing a Custom Widget Using PyGTK”

  1. Nice tutorial! I’ve been looking for something just like this! (I’m trying to learn Python and GTK at the same time, since I figured PyGTK would be easier to pick up than raw GTK). So if any of my comments below sound clueless, it’s because they are :)

    For some reason, this script doesn’t work for me – I just get an empty window (a little bit taller than in your animated GIF, above).

    Anyway, here are some things that caught my eye:

    Typo in the tutorial: “using the pixmap_create_from_xmp_d function:” -> change “xmp” to “xpm”

    Tutorial: “Since we want the widget to be portable without having an xpm file around we simple load it’s data.” -> “… simply load its data.”

    Here’s a patch for some really minor fixes (I hope this doesn’t make the comment too long :) :


    — starhscale.py 2006-07-25 18:40:54.000000000 -0700
    +++ starhscale-treitter.py 2006-08-19 14:15:07.000000000 -0700
    @@ -142,7 +142,7 @@
    rating scheme used in iTunes”””

    def __init__(self, max_stars=5, stars=0):
    – “””Initialization, numstars is the total number
    + “””Initialization, max_stars is the total number
    of stars that may be visible, and stars is the current
    number of stars to draw”””

    @@ -170,7 +170,7 @@
    # and button click and button press events

    self.window = gtk.gdk.Window(
    – self.get_parent_window(),
    + parent=self.get_parent_window(),
    @@ -252,7 +252,6 @@
    y = event.y
    state = event.state

    – new_stars = 0
    if (state & gtk.gdk.BUTTON1_MASK):
    # loop through the sizes and see if the
    # number of stars should change
    @@ -268,7 +267,7 @@
    return True

    def check_for_new_stars(self, xPos):
    – “””This function will determin how many stars
    + “””This function will determine how many stars
    will be show based on an x coordinate. If the
    number of stars changes the widget will be invalidated
    and the new number drawn”””
    @@ -308,13 +307,13 @@
    “””set the maximum number of stars”””

    if (self.max_stars != max_value):
    – “””Save the old max incase it is less then the
    + “””Save the old max incase it is less than the
    current number of stars, in which case we will
    have to redraw”””

    if (max_value > 0):
    self.max_stars = max_value
    – #reinit the sizes list (should really be a sperate function
    + #reinit the sizes list (should really be a separate function)
    self.sizes = []
    for count in range(0,self.max_stars):
    self.sizes.append((count * PIXMAP_SIZE) + BORDER_WIDTH)

  2. Hi Travis,

    Thanks for the kind words, and far catching all of my mistakes. I’m not sure but did the comment cut off part of your patch? It looks like it might have, but if not I have updated a second version of the file with your changes (see below).

    Now as far as the widget not working, what OS are you running? I just noticed that it works fine on my Linux box but fails to draw properly on my Windows system. (It will draw if I resize the window)

    I have made some changes to the code that got it working on my Windows system that you can download here. But the fix is a pretty “hacky” as I have to resize the widget each time I need it to redraw! Let me know if it works for you.

    I’m thinking that there may be something I’m missing in my drawing code, but I just can’t see it right now. I’ll take a look into this further when I get the chance and re-upload a fully fixed version.

    Thanks again for the comment and the help, very much appreciated!

  3. Hi selsine!

    (Sorry I took so long to get back to you – I hope this gets emailed to you).

    It looks like my comment didn’t get clipped at all. (Though I should’ve just linked to a separate file :)

    Version 0.2 actually works fine for me. (The original doesn’t work – I just tried both on a new installation of Ubuntu Edgy Eft).

    I’ll likely bug you some more when I dive further into Python/GTK :)

  4. Hey Travis,

    No Problem, I’m glad the new version works! I’m still meaning to look into this abit more to figure out exactly what the problem is.

    Thanks again for all of your help!

  5. Hey, excellent tutorial! Congratulations!

    I would like to know to things, if you could help me :)

    1) How do you make a pixel map? I found that amazing!

    2) How do I integrate the star widget in the rows of the pywine program?


    Note: flavio’s comment was lost due to a database problem.

  6. Hi Flavio,

    Sorry about the comment problem.

    1) I generated the pixmap in The Gimp and then used the contents of the generated file to create the python code.

    2) I’m not sure about that just yet…it’s something that I’ve wanted to do, but it turned out to be much more difficult then I originally thought. Hopefully I can get something working and get it up here.

  7. Pingback: Las Noyas de Taran
  8. Well keep up the great work! I really think that this will be a large help for anyone who speaks Spanish and is interested in these tutorials…plus you are helping me to find bugs!

  9. hi , In (pygtk) , how can I create a popup window and attach a picture (such az PNG or GIF formats ) to it. I want to hide window’s titlebar and borders. Actually, I want to show only the picture and nothing else.

  10. Hi Mahdi,

    So all you want to do is display an image to the user? Do you want a window in anyway? Or just a window floating there? I would take a look at PyGame or the Python Imaging Library (PIL) .

  11. Is there a way to make custom widgets like this “buildable” with gtk.Builder? It would be trivial to implement the Buildable interface but then how does one get access to the widget from within gtk.Builder?

  12. all ur tuts rule this site is brill if a littel small so keep up the brilliant work!!

    i know this is slighly off topic but its not that far off

    is there a maxemum number of times u can

    self.DrawArea = gtk.DrawingArea()
    self.DrawArea.connect(“expose_event”, self.expose) # this works well
    self.DrawArea.connect(“configure_event”,self.conf) # and this one
    self.DrawArea.connect(“button_release_event”,self.moton) # but this dosnt do a thing
    self.DrawArea.connect(“motion_notify_event”,self.moton) # nor this one

    p.s they will eventually point to different places!! and are in side a class mydisplay(gtk.VBox ): in side a vbox in a window most of the program works v well but i want to add a bit of polish and hit this wall when i added the connect’s
    thank u very much
    any ideas

  13. Hmm doesn’t seem to be working for me. I followed the tutorial through and I just got a blank window so I downloaded the source and it’s exactly the same. When I resize the window 5 stars appear but I cannot interact with them at all. I’m running Ubuntu Hardy.

  14. I can confirm what Karl wrote: I tested the source on both Hardy and Intrepid, on two different computers, and until the widget is resized one can move the mouse over the window and/or click, but “motion_notify_event” will never be called.

    Once the widget is resized, _every_ mouse motion (not only mouse first entering the allocation area!) triggers motion_notify_event, and notice that every event is a hint (the “else” clause in “motion_notify_event” is never called at all).

    I had problems with anoter app (I’m writing) which doesn’t catch motion hints properly.

  15. Great work Pietro, thanks for the information. I’ll keep my eye on that bug and update the code once it is resolved. IF I find the time I’ll try to find a suitable work around.

  16. I fixed your example. the problem was that “self.allocation” was gtk.gdk.Rectangle(-1,-1,1,1) which made the do_expose_event only repaint a very small area outside the actual widget. I will send you the updated code as soon as I get home.
    or just a diff, whatever you’d prefer
    It was my first contact with pyGTK and i liked it a lot. your tutorial was very helpful (although i mainly just read the comments in the code :D)

    (additionally, i changed the code to load the picture from a file. but that was a very small change)

  17. To get the code to work with pygtk 2.14, I had to add
    self.allocation = allocation
    to do_size_allocate().

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>