C Programming | xlib 101
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:
- A Display structure pointer, which contains the data returned when we open a connection with the X11 server. It needs to be paired with a Screen.
- A Graphic Context, that is a server resource that contains values for the variables that apply to the graphic primitives provided by Xlib.
- A Window which is no more than the rectangular area on the screen that we use to draw our graphics, and it's represented as an UUID for the program.
- An Event Tracker defined by a union that contains all the event structure types.
- A Font to be able to render text into the window.
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.
We want to add a Display pointer and a variable that keeps track of the screen number (
*dp
andscr_num
). This two are widely used by Xlib routines as arguments. If we need to call them on separate source files, we can also declare them there asexternal
. The screen number variable should be paired with a pointer to an Xlib defined Screen (*scr
).Related to the Display pointer we need to store the display's width and height, as an unsigned integers (
dp_w
anddp_h
) that can later be filled by calling Xlib functions for that task.X11 works in a server/client way, and in our program we require a variable to indicate which server should we connect to. It's represented by a
char *
that we should initialize toNULL
and later populate it using theXOpenDisplay()
function from Xlib (*dp_name
).Then we want to have a Window (
win
) and the required variables to know which size the window is (w_w
,w_h
), its border size (w_b
), and where it needs to be placed on the screen when the program starts (x
,y
). Also, providing a name for the window can be done (*win_name
).
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.
The
XWindowAttributes
structure can be useful to retrieve information about the root window, and in our case, determining the width and height for our display (xwa
).In order to keep track of the events that occur, we need a variable of type
XEvent
which stores that information (ev
).The next thing we need is our graphics context, that as the Window, it's represented by an ID, and accessible via the Xlib type
GC
.Fonts are a bit tricky. We'll discover more about them later, but for now we need to include a variable of the type
XFontStruct
to store some font's info when we load it (*fnt_info
).
Summing up, the structure we created should look like this:
typedef struct {
*dp;
Display char *dp_name;
unsigned int dp_w, dp_h;
int scr_num;
*scr;
Screen ;
Window win;
XWindowAttributes xwaunsigned int w_w, w_h, w_b;
int x, y;
char *win_name;
;
XEvent ev;
GC gc*fnt_info;
XFontStruct ;
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 callingXOpenDisplay()
.
*prog;
prog_t
void program_init() {
= calloc(1, sizeof(prog_t));
prog ->dp_name = NULL;
prog->win_name = "Xlib Sandbox";
prog}
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:
DefaultScreen()
returns the default screen number provided byXOpenDisplay()
. This should be used to retrieve the screen number in applications using only a single screen.DefaultScreenOfDisplay()
returns a pointer to the default screen.
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) {
("unable to connect to display\n");
printf(-1);
exit}
// get the screen
->scr_num = DefaultScreen(prog->dp);
prog->scr = DefaultScreenOfDisplay(prog->dp);
prog
// ask for window information
if(XGetWindowAttributes(prog->dp, RootWindow(prog->dp, prog->scr_num), &prog->xwa) == 0) {
("unable to get window attributes\n");
printf(-1);
exit}
// if the previous block doesn't fail, associate the display w and h to the provided info
->dp_w = prog->xwa.width;
prog->dp_h = prog->xwa.height;
prog}
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:
XCreateWindow()
allows us to set specific window attributes when creating a window.XCreateSimpleWindow()
creates a window inheriting its attributes from its parent window.
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?
ExposureMask
selects events of typeExpose
which tell the application to redraw itself. This events happen when the window is displayed for the first time, and when it becomes visible after being obscured.StructureNotifyMask
selects various types of events. For now we only need to catch the events of typeConfigureNotify
that gives information of the window size when it has been resized.
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() {
->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), \
prog(prog->dp, prog->scr_num));
WhitePixel
(prog->dp, prog->win, ExposureMask | KeyPressMask | ButtonPressMask |
XSelectInput);
StructureNotifyMask(prog->dp, prog->win);
XMapWindow}
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:
fid
is aFont
type that holds the font ID for the loaded font. We need to call this property to use the font.ascent
is an integer value that specifies the maximum extent above the font's baseline for spacing.descent
is an integer value that specifies the maximum extent below the font's baseline for spacing.
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) {
("unable to load font %s, fallback to default\n", font_name);
printf->fnt_info = XLoadQueryFont(prog->dp, "fixed");
prog}
}
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
->gc = XCreateGC(prog->dp, prog->win, valuemask, &gc_values);
prog
(prog->dp, prog->gc, prog->fnt_info->fid);
XSetFont(prog->dp, prog->gc, BlackPixel(prog->dp, prog->scr_num));
XSetForeground}
Print some text
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:
Calculate the text width using the provided
XTextWidth()
function, that requires a font and the length of the text to print.Calculate the font height using the font ascent and font descent information previously loaded in the
XFontStruct
.Calculate the text position in the window. In this example we are placing it at the top center location of the window, so for the top part we need the font height, and for the center part we need to calculate the difference between the window's width and the text's length, divided by two.
Call the provided
XDrawString()
function with the calculated parameters to print the text in the window.
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;
(prog->dp, prog->win, prog->gc, _txt_pos, _fnt_h, _txt, strlen(_txt));
XDrawString}
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.
In the
Expose
case we can call our placing text function.The
ConfigureNotify
case tells the new width and height of the window to our program.The
KeyPress
case can check for the desired key to run an action. In this example, we want the Escape key to quit the program. We added a special variable of typeKeySym
to handle the keyboard for us inside Xlib. Note that we are getting the key value by callingXLookupKeysym
which requires a parameter of typeXKeyEvent *
and theXEvent
is a union of all event types, so we are casting it as the required one. Once we have ourkeysym
value, we can compare against the key we want to run the action and execute it inside the condition.
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(;;) {
(prog->dp, &prog->ev);
XNextEvent
switch(prog->ev.type) {
case Expose:
();
place_textbreak;
case ConfigureNotify:
->w_w = prog->ev.xconfigure.width;
prog->w_h = prog->ev.xconfigure.height;
progbreak;
case KeyPress:
->ks = XLookupKeysym((XKeyEvent *)&prog->ev, 0);
progif(prog->ks == XK_Escape) {
();
program_free(0);
exit}
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() {
(prog->dp, prog->fnt_info->fid);
XUnloadFont(prog->dp, prog->gc);
XFreeGC(prog->dp);
XCloseDisplay
(prog);
free}
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("-misc-tamsyn-medium-r-normal-*-15-108-100-100-c-80-iso8859-1");
load_font();
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.