Bloerg
       

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
)

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

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

Discussion

Yindong
Thu, Nov 14 2013

Hi, I am doing some test as you did. It seems that you defined exported function initfoomodule and use init_pygobject, which is used in Gtk2. However, you are using gobject introspection(pygi) with seems binded with Gtk3. Is that all right to create custom widget using those combination? In my experience, the widget could not work and “could not get typecode from object” was reported. But in pure gtk2 context it works all right

Matthias
Thu, Nov 14 2013

Yes it’s true, this example is actually for GTK+ 2. But on the other hand, GObject Introspection was already available in GTK+ 2!

Anyway, for recent GTK+ versions, you have to initialize a bit different because init_pygobject was removed in favor of

pygobject_init (-1, -1, -1);

which works in both GTK+ 2 and 3. Hope this helps.

Yindong
Fri, Nov 15 2013

Actually I did not find any articles about how to install gobject introsepction with GTK+ 2, especially on Windows.

And In GTK+ 3, it seems that python wrapper is not needed, however a .gir file and it’s corresponding .typelib file is required.

I don’t know if it is the same in GTK+ 2.

Matthias
Fri, Nov 15 2013

Well, I have no idea about Windows but you are right: if the widget library is properly wrapped you can use PyGI without a custom Python wrapper module. I actually did this for two projects before and can tell for sure, that it works for for GTK+/GLib versions >= 2.22.

Yindong
Mon, Nov 18 2013

Good work, I am trying to make my gtk2 working with PyGI on Windows. Thank you.

This post might also have some comments at Google+.