Using Cairo with X11/Xlib

Cairo is a graphics library which offers common drawing primitives independently of the actual backend. It comes with a set of backends, such as PNG or PDF. One of these backends allows to use libcairo for drawing on X11 windows. Although there are very powerful libraries such as Qt or GTK, they are often far too complex for simple applications.

Following I explain how to open an X11 window using Xlib and show how to create graphics output with Cairo graphics.

Opening a Window

X11 is probably the most flexible graphical interface which makes it a little bit complicated, at least at a first sight. To open a window, you need to do the following steps:

  1. Connect to the X sever: XOpenDisplay(3).

  2. Select the output screen: DefaultScreen(3).

  3. Create a window: XCreateSimpleWindow(3).

  4. Choose input events: XSelectInput(3). Please note that this is not mandatory for opening a window but typically you'd like to receive events such as mouse clicks and key board input.

  5. Display the window: XMapWindow(3).

After the window is ready, it is mapped to a cairo Xlib surface. The following function shows how to do it.

cairo_surface_t *cairo_create_x11_surface0(int x, int y)
{
    Display *dsp;
    Drawable da;
    int screen;
    cairo_surface_t *sfc;

    if ((dsp = XOpenDisplay(NULL)) == NULL)
        exit(1);
    screen = DefaultScreen(dsp);
    da = XCreateSimpleWindow(dsp, DefaultRootWindow(dsp),
        0, 0, x, y, 0, 0, 0);
    XSelectInput(dsp, da, ButtonPressMask | KeyPressMask);
    XMapWindow(dsp, da);

    sfc = cairo_xlib_surface_create(dsp, da,
        DefaultVisual(dsp, screen), x, y);
    cairo_xlib_surface_set_size(sfc, x, y);

    return sfc;
}

Receiving Events

The next task is to receive an event. The function from above configured the window to receive mouse button and keyboard events. The function XNextEvent(3) returns the next event of the X server's event queue and it blocks if the queue is empty. If blocking is not an option for your tool because you have permanent interaction -- such as in e.g. computer games -- you have to check if there are events in the queue before retrieving it with XNextEvent(3) to avoid blocking. This is done with XPending(3). The function immediately returns the number of events in the queue. Thus, it returns 0 if there are no events.

XNextEvent(3) returns an XEvent which actually is a union of all different kinds of X events. The type field distinguishes between them. The XKeyEvent receives key codes which have to be translated with XLookupString(3) to symbols. All symbols are defined in X11/keysyndef.h. The following function shows how to do it.

int cairo_check_event(cairo_surface_t *sfc, int block)
{
    char keybuf[8];
    KeySym key;
    XEvent e;

    for (;;)
    {
        if (block || XPending(cairo_xlib_surface_get_display(sfc)))
            XNextEvent(cairo_xlib_surface_get_display(sfc), &e);
        else
            return 0;

        switch (e.type)
        {
            case ButtonPress:
                return -e.xbutton.button;
            case KeyPress:
                XLookupString(&e.xkey, keybuf, sizeof(keybuf), &key, NULL);
                return key;
            default:
                fprintf(stderr, "Dropping unhandled XEevent.type = %d.\n", e.type);
        }
    }
}

Putting all this together creates a first simple window example. Download the cairo_xlib_simple.c source file.

Animations and Full Screen

To create animations you simple repaint the image in a loop. The problem is that you basically have no control about the timing when the X server actually updates the screen. All graphic operations are queued and at some time the queue is processed by the X server. This creates the problem that the animation might flicker. The solution is to push all operations to a group (cairo_push_group()) instead of directly drawing to the surface. Finally the group is popped to the Cairo drawing source (cairo_pop_group_to_source()) and painted (cairo_paint()) all at once. Finally we force the X server to flush its queue (cairo_surface_flush()). Although this produces good results you should be aware that this (Cairo + Xlib) is not the method of choice if you intend to write a high speed graphics intensive computer game. If this is the case you should start to learn OpenGL or SDL.

Making the window fullscreen seems to be a well protected X11 secret. Fullscreen is a specific property of a window, such as "maximized", "minimized", and similar ones. The property _NET_WM_STATE_FULLSCREEN is set with the function XChangeProperty(3).

Putting all this together leads to this second final example cairo_xlib.c.

A last note: since the size of a window can change you have to react accordingly. The window change event is sent to the event queue as an XConfigureEvent. It contains the new width and height which has to be passed to the Cairo surface with cairo_xlib_surface_set_size().