For readers not familiar with Python development, PyGTK is a set of Python wrappers for the GTK+ graphical user interface library. GTK+ derives it’s name from a seemingly endless array of acronyms: GTK stands for GIMP ToolKit, where GIMP in fact means GNU Image Manipulation Program, and finally, GNU is shorthand for ‘GNU’s Not Unix!’. In this article, I will describe a problem we faced while working on a Python-based CAD application. I believe that the simple solution we came up with provides a good introduction to PyGTK widgets and custom signals.

When working with various CAD tools, users are usually given dialog windows to modify various aspects of their drawing in detail. In our application, we had to create a dialog window which would allow the user to change coordinates of vertices in a polygon. This is not a major problem, save for the fact that different polygons have different number of vertices – and each vertex has to be edited through three gtk.Entry widgets, one for each coordinate. So editing a triangle would require 9, and editing an octagon a total of 24 gtk.Entry widgets.

One thing is obvious – such a dialog cannot be created with any UI designer tool (such as Glade), simply because its content is not known until the dialog is invoked. It seems that creating a custom widget – one for each given vertex – solves the problem. Even though writing a custom widget definitely is one possible solution, it is also overkill. After all, all we need are three gtk.Entry widgets packed inside a gtk.HBox, like this:

class VertexInfo():
    def __init__(self, v):
        self.vertex = v
        self.widget = gtk.HBox()
        self.entries = [None] * 3
        for i in range(3):
            self.entries[i] = gtk.Entry()
            self.entries[i].set_text(str(v[i]))
            self.widget.pack_start(self.entries[i])

Simple enough. Now we can easily instantiate this class and pack its widget where needed:

window = gtk.Window()
vi = VertexInfo([0, 0, 0])
window.add(vi.widget)
window.show_all()

One thing is lacking for our widget-wannabe class to appear more widget-like: it has to be able to somehow inform its parent window that user has entered new values. This gives parent an opportunity to do its own processing – for example, a dialog may disable the ‘Apply’ button if values are out of some specified range. For that, we need a custom signal. The class should also provide its parent an easy access to values in entry fields, and it must make sure that these values are valid floating point numbers. These requirements ask for an overhaul:

class VertexInfo(gobject.GObject):
    def __init__(self, v):
        self.__gobject_init__()
        # same as above...
            entries[i].connect('changed', self._emit_signal)

    def _emit_signal(self, *nkwargs):
        self.emit('vertex_changed', self)

    def is_valid(self):
        try:
            for i in range(3):
                self.vertex[i] = float(self.entries[i].get_text())
            return True
        except ValueError:
            return False

Now our class inherits from GObject – this is required whenever a class wants to emit custom signals. When users start typing in any of the entry fields, that widget will emit the ‘changed’ signal. VertexInfo class will catch that signal, and emit its own, called ‘vertex_changed’. Any window that packs VertexInfo only needs to connect to that signal. Before this signal can be used, however, it must first be registered with the system:

gobject.type_register(VertexInfo)
gobject.signal_new('vertex_changed', 
                   VertexInfo, 
                   gobject.SIGNAL_RUN_FIRST, 
                   gobject.TYPE_NONE, (VertexInfo))

All that`s left to do is to connect a method that will handle the signal. Notice that this method receives one parameter, and this parameter will be the instance of VertexInfo which emitted the signal. When signal is received, the function immediately checks if the emitter contains valid floats. If this is the case, vertex coordinates are read from the widget and further processed as needed:

vi = VertexInfo([0, 0, 0])
vi.connect('vertex_changed', vertex_changed_sink)

def vertex_changed_sink(emitter):
    if emmiter.is_valid():
        vertex = [emitter.vertex]
        # Do some processing here...
    else:
        # User has entered invalid values...

This concludes today’s article. If you decide your application requires a completely custom widget, lots more information can be found here.

0 comments