C Programming | xlib 101

FreeBSD gearing-up

Index


Moving on the graphical user interfaces field, the X Window System (aka X11) is one of the most used windowing system to make user interfaces in the *nix world.

The X Window System made possible for the first time to make portable programs to dozens of different computers. It's a complex system based on some premises that can be easily understood. A client-server model, based on displays and screens with windows and events management. The functionality to work with X11 is provided by the X Library.

The Xlib basic C recipe

One of the books I have about X11, that talks about Xlib only, has 783 pages of information. And it's just one of the several documentation resources I happen to have. At a first glance, this can be a bit overwhelming. There is a lot of content to learn, and even several ways to do the same thing working with the library, so let's start small.

For a basic program displaying an X11 window we need the following:

That isn't much, right? Well, we may need some more variables in the mix, but as a complement to the core ones. In the next section we'll discuss them. Nothing complicated is required, for now.

How to bake it

Let's implement a basic example to display an Xlib window on the screen. Considering this guide isn't meant to cover the best practices (that will come later on), but a handful 101 set of instructions on Xlib, our program can be ready to run in less than 200 lines of code.

We could just place every single function call and logic inside the main() function of our program, but to make things easier, this guide separates each key steps into its own function than can be called later at main().

The first thing we need to do is to include the X11/Xlib.h header to have access to the library core. The other X11 header we need to consider is the X11/keysym.h so we can make our life easier handling keyboard inputs later. Next we have to create a program structure that contains the essential variables to glue the window program.

Essential variables

In order to properly use the key elements for an Xlib program, we need some additional variables in the mix.

As an optional variable we can add XSizeHints that is used to provide the window manager some information about the preferred size for the program's top level window.

Summing up, the structure we created should look like this:

typedef struct {
    Display *dp;
    char *dp_name;
    unsigned int dp_w, dp_h;
    int scr_num;
    Screen *scr;
    Window win;
    XWindowAttributes xwa;
    unsigned int w_w, w_h, w_b;
    int x, y;
    char *win_name;
    XEvent ev;
    GC gc;
    XFontStruct *fnt_info;
    KeySym ks;
} prog_t;

Instancing our structure

With our structure defined, let's jump into action. Since we grouped all our required variables, a clean way to initialize them is via an init subroutine.

We can allocate a prog_t instance in memory and for now, just set up the char pointer variables.

Note that the display name variable is initialized to NULL. This is required when the name is not specified, that way Xlib is going to use the environment $DISPLAY variable when calling XOpenDisplay().

prog_t *prog;

void program_init() {
    prog = calloc(1, sizeof(prog_t));
    prog->dp_name = NULL;
    prog->win_name = "Xlib Sandbox";
}

Connecting to a server

This part is going to be common to almost every Xlib based program. As we mentioned before, calling XOpenDisplay() returns a pointer to a structure of type Display if the connection is successful. If not, it will return NULL so we can handle a fail-safe load.

The environment variable $DISPLAY is a string with the following format: hostname:number.screen_number.

If the connection to the server is successful, we can associate a screen to our program. The following functions' return values are required:

Next we need to acquire some window information, as we need to know the screen size for our program. In order to achieve it we can access directly to the Display structure or we can use our XWindowAttributes variable. The main difference is that accessing the Display directly only works for the root window, and the second method is less efficient but works for any window.

void connect_to_server() {
    // open a display and initialize our Display struct
    if((prog->dp = XOpenDisplay(prog->dp_name)) == NULL) {
        printf("unable to connect to display\n");
        exit(-1);
    }

    // get the screen
    prog->scr_num = DefaultScreen(prog->dp);
    prog->scr = DefaultScreenOfDisplay(prog->dp);

    // ask for window information
    if(XGetWindowAttributes(prog->dp, RootWindow(prog->dp, prog->scr_num), &prog->xwa) == 0) {
        printf("unable to get window attributes\n");
        exit(-1);
    }

    // if the previous block doesn't fail, associate the display w and h to the provided info
    prog->dp_w = prog->xwa.width;
    prog->dp_h = prog->xwa.height;
}

Creating a window

The first window that we create in an Xlib program is going to be a child of the root window. This is important since most of the window parameters can be loaded as part of the user's program configuration, but most likely the window manager is going to ignore some values by default for this first window.

The variables x and y that we defined are meant to place our window in the screen, since it's the main window of the program we can set them to 0 and let the user move the program around when loaded.

For the other window parameters (w_*), we can hard-code the values for now, leaving the option to implement a configuration file later on.

Now it's time to finally create a window. There are two main functions to create a window in Xlib:

In this example we are going to use the second one to avoid the extra verbose. Note that the last two parameters required are black and white pixel information. The way Xlib is designed, colors are a bit complex to implement. Xlib is meant to work in a huge variety of hardware and can only guarantee black and white color to work.

Next we need to specify which type of events our program should respond to, by calling XSelectInput, that as a third parameter, takes events masks. This event masks can be combined using a bitwise OR. In the code of the example, KeyPressMask and ButtonPressMask are pretty much self-explanatory event masks, but what about the other two?

A key part in order to display the window into the screen is to map it to the Display. Calling XMapWindow() does the work for us, but remember to call it! Otherwise no window will be shown.

void create_window() {
    prog->x = prog->y = 0;
    prog->w_b = 4;
    prog->w_w = prog->dp_w / 3;
    prog->w_h = prog->dp_h / 4;

    prog->win = XCreateSimpleWindow(prog->dp, RootWindow(prog->dp, prog->scr_num), prog->x,\
            prog->y, prog->w_w, prog->w_h, prog->w_b, BlackPixel(prog->dp, prog->scr_num), \
            WhitePixel(prog->dp, prog->scr_num));

    XSelectInput(prog->dp, prog->win, ExposureMask | KeyPressMask | ButtonPressMask |
            StructureNotifyMask);
    XMapWindow(prog->dp, prog->win);
}

Dealing with fonts

We need text in the screen, without it any interactive program is not going to be useful at all at some point. We included a XFontStruct earlier and we need to pass some info into it.

Xlib offers the function XLoadQueryFont() to help in the process, and by default we can call to load the fixed font. We can also use that font as a fallback font if the one provided by the user can't be loaded.

When we load a font into the XFontStruct, we load a bunch of information about it that can help later to render text. In this guide we need to know about the following properties:

Combining the ascent and descent properties we can get the font's height.

The basic font handling in Xlib should be enough to cover this setup, however for modern day programs, the use of libraries like Xft can help.

// leave a font name parameter in the function, so we can pass our own font to the program
void load_font(char *font_name) {
    if((prog->fnt_info = XLoadQueryFont(prog->dp, font_name)) == NULL) {
        printf("unable to load font %s, fallback to default\n", font_name);
        prog->fnt_info = XLoadQueryFont(prog->dp, "fixed");
    }
}

To pass a custom font name, typing just the font name like arial it's likely not going to work. We can take advantage of the xfontsel(1) utility that can generate a font name like this: "-misc-tamsyn-medium-r-normal-*-15-108-100-100-c-80-iso8859-1".

As a hint, if you already have some fonts' configuration in the .Xresources file, just grab the font name from there as a starting point.

Create a graphic context

With out font loaded, we can proceed to create a graphic context in our program. The graphic context is a server resource that contains values for the variables that apply to the graphic primitives provided by Xlib.

Our graphic context requires two more variables to be created, a value mask and a variable of type XGCValues.

The first one, the value mask, specifies which components in the graphic context are going to be. I can contain zero or more than one mask bit.

The second one stores the graphic context values returned when the graphic context is created.

Since we are not accessing those variables later in the program, they are placed outside the main structure we created, but a cleaner approach would be to include them there.

Once we have it, we can set our loaded font into it, and we can also specify that our foreground color is going to be black. By default the background color of an Xlib program is white, but the foreground is undefined.

void create_graphic_context() {
    unsigned long valuemask = 0;
    XGCValues gc_values;

    prog->gc = XCreateGC(prog->dp, prog->win, valuemask, &gc_values);

    XSetFont(prog->dp, prog->gc, prog->fnt_info->fid);
    XSetForeground(prog->dp, prog->gc, BlackPixel(prog->dp, prog->scr_num));
}

If we want to see our loaded font and graphic context setup in action, best thing we can do is to render text into the window.

The process is pretty much straight forward. We need a char pointer that contains the text, as a key part for the rest of the function to work, and then we need to follow these steps:

void place_text() {
    char *_txt = "Welcome to the Xlib sandbox program";
    int _txt_width = XTextWidth(prog->fnt_info, _txt, strlen(_txt));
    int _fnt_h = prog->fnt_info->ascent + prog->fnt_info->descent;
    int _txt_pos = (prog->w_w - _txt_width) / 2;
    XDrawString(prog->dp, prog->win, prog->gc, _txt_pos, _fnt_h, _txt, strlen(_txt));
}

Run the program loop

An infinite loop does the trick for the program to keep running until the action we defined to exit is called.

Events are generated asynchronously and the server queues them for each client request. While we are in our infinite loop, we can listen to the program's events, by calling XNextEvent(). An event can be generated as a side effect of an Xlib action, or reported from some device activity.

By accessing the type property of our XEvent variable we can switch between different event masks. Earlier in the guide, we specified which input should our program be listening to. It makes sense to include all those flags in our switch control to actually react to the events.

All the other event types are going to pass-through into the default case and be ignored since we don't need them, for now.

void program_loop() {
    for(;;) {
        XNextEvent(prog->dp, &prog->ev);

        switch(prog->ev.type) {
            case Expose:
                place_text();
                break;
            case ConfigureNotify:
                prog->w_w = prog->ev.xconfigure.width;
                prog->w_h = prog->ev.xconfigure.height;
                break;
            case KeyPress:
                prog->ks = XLookupKeysym((XKeyEvent *)&prog->ev, 0);
                if(prog->ks == XK_Escape) {
                    program_free();
                    exit(0);
                }
            default:
                break;
        }
    }
}

Clean before exiting

Even when one has 32GB of RAM in the computer, taking care of freeing memory that's no longer needed is a good practice. The functions to clean the memory here (except the already known free()) are part of the Xlib library.

They are pretty much self-explanatory, one to unload the font, one to destroy the graphic context and one to close the display. Remember to call the XCloseDisplay at the end, since the Display structure is required as a parameter for the other freeing functions.

void program_free() {
    XUnloadFont(prog->dp, prog->fnt_info->fid);
    XFreeGC(prog->dp, prog->gc);
    XCloseDisplay(prog->dp);

    free(prog);
}

The main function

After splitting up all the code in several subroutines, our entry for the program can be presented less than ten lines of code.

Loading the font from the user can be achieved by reading a configuration file, a step that we'll explore in the future, or by passing an argument to the program when running it. In this case is just hard-coded for convenience.

int main(int argc, char **argv) {
    program_init();
    connect_to_server();
    create_window();
    load_font("-misc-tamsyn-medium-r-normal-*-15-108-100-100-c-80-iso8859-1");
    create_graphic_context();

    program_loop();

    return 0;
}

Compile an Xlib program

Once we have all our code ready, compiling just using Xlib it's easy. Adding the flag -lX11 to the compiler does the trick. Make sure to take a look at the complete example of this article on the repo, where you can find a ready to use makefile.

What's next

We barely scratched the surface of Xlib. This guide was a hello world like introduction. In depth event handling, font rendering, user interaction, image drawing, and much more is left to discover.