Integrating a GTK+ C widget into Python

Last year I wrote a GTK+ widget to show and modify the properties of any GObject. The widget was written in C for use in a C application. For another project, I also have to modify the properties of GObjects but this time the application is written in Python using PyGObject. Here’s how I did it.

Python extension

Obviously, a widget that is written in pure C and does not come with any introspection data is unknown to Python’s type system. For this reason, we must write a small extension module, that instantiates the object. In my case, I want to call egg_property_tree_view_new() that either takes NULL or an object that is going to be observed. First we include headers for Python, Python GObject access and our widget:

#include <Python.h>
#include <pygobject.h>
#include "egg-property-tree-view.h"

Our module provides one function called create_property_view that takes a GObject object and returns the constructed tree view object:

static PyObject *
create_property_view (PyObject *self,
                      PyObject *args)
{
    PyObject *object;
    GObject *observed;
    GtkWidget *widget;

First, let’s check if an argument was passed:

    if (!PyArg_ParseTuple (args, "O", &object))
        return NULL;

If the argument is not None, we unwrap the contained object with pygobject_get():

    observed = object == Py_None ? NULL : pygobject_get (object);

    if ((observed != NULL) && (!G_IS_OBJECT (observed)))
        goto err;

If everything is okay, we create a new widget with our C API, wrap it using pygobject_new() and return it:

    widget = egg_property_tree_view_new (observed);
    object = pygobject_new (G_OBJECT (widget));
    return object;

If something unexpected happened, we throw a type error exception:

err:
    PyErr_SetString (PyExc_TypeError,
                     "Argument must be a GObject");
    return NULL;
}

Last but not least, we define our exported function and initialize our module. Note, that you have to call init_pygobject() for GTK+ 2 or pygobject_init() for GTK+ 3, or any attempt to call pygobject_foo() will fail badly.

static PyMethodDef methods[] = {
    { "create_property_view", create_property_view,
      METH_VARARGS, "Create EggPropertyTreeView" },
    { NULL, NULL, 0, NULL }
};

PyMODINIT_FUNC
initfoomodule (void)
{
    PyObject *module = Py_InitModule ("foomodule", methods);

    if (module == NULL)
        return;

    init_pygobject ();
}

Build environment

Once we have our Python wrapper together, we need to build it. For distribution purposes it is a good idea to use distutils for that. Unfortunately, distutils does not come with built-in support for pkg-config. Here I used a function from the German Python forums that calls pkg-config and creates a dict1 for consumption by distutils.core.Extension. The following setup.py builds the module using

from distutils.core import setup, Extension
from subprocess import PIPE, Popen

def pkgconfig(*packages):
    flags = {
        '-D': 'define_macros',
        '-I': 'include_dirs',
        '-L': 'library_dirs',
        '-l': 'libraries'}
    cmd = ['pkg-config', '--cflags', '--libs'] + list(packages)
    proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
    output, error = proc.stdout.read(), proc.stderr.read()

    if error:
        raise ValueError(error)

    config = {}

    for token in output.split():
        if token != '-pthread':
            flag, value = token[:2], token[2:]
            config.setdefault(flags[flag], []).append(value)

    if 'define_macros' in config:
        macros = [(name, None) for name in config['define_macros']]
        config['define_macros'] = macros

    return config

module = Extension('foomodule',
                   sources=['module/egg-property-cell-renderer.c',
                            'module/egg-property-tree-view.c',
                            'module/foomodule.c'],
                   extra_compile_args=['-std=c99'],
                   **pkgconfig('gtk+-2.0 pygobject-2.0'))

setup(
    ext_modules=[module]
    # Further package description
)
1

I had to change the code a bit, because GTK+ requires the -pthread cflag, on which the token “parser” chokes on.

Glueing everything together

Now,

python setup.py install

builds our extension module and installs it into the global site-packages or – as I usually do – into a virtualenv. With everything in place, we can happily mix and match objects created via GObject introspection or our own wrappers:

import foomodule
from gi.repository import Gtk

window = Gtk.Window()
view = foomodule.create_property_view(window)
window.add(view)
window.show_all()

Gtk.main()

There is a disadvantage of a hand-written wrapper compared to the generated introspection data: you have to translate every C method manually. However, in most cases, widgets are entirely characterized by their properties. Hence, a Python programmer can use object.set_properties or object.props to change the behaviour of the widget instead of calling object.set_certain_property(value).