Revision as of 16:32, 4 August 2007

This tutorial will show you how to write a simple X application using
the low level C library, Xlib. The goal is to write a simple text-based clock displaying the system time, running on top of every other application - like a status bar would.

While the application is fairly simple, it still requires us to know quite a few details about X and Xlib to write a proper X application.

1 Target audience

This tutorial is dedicated to the intermediate Haskell coder. While I
will try to write the simplest code I can (probably it will even look
the dumbest, but that's me), I'm not going into much details about the
Haskell part.

2 Goals

What are we going to learn:

how to create a window and set, or change, its attributes;

how to draw in that window, specifically some text, with some properties, like

fonts or colors;

how to properly update the window;

how to handle events, like a mouse button press.

3 Background reading

These are some links that can be used as reference:

The Xlib Manual: this is the reference manual, and you should look up here every function that we are going to use in

4 Prerequisites

X11-extras: will be required in some examples. This library provides missing

bindings to the X11 graphics library and is being actively developed by Spencer Janssen at the time of this writing; it is notably being used in Xmonad. Some functions needed in this tutorial can be found only in the darcs repository of X11-extras: http://darcs.haskell.org/~sjanssen/X11-extras. Read carefully the README before
installing.

The first function,
openDisplay, is the interface to the Xlib function XOpenDisplay(), and opens a connection to the X sever that controls a display. The connection is returned and bound to dpy. By applying defaultScreen, the interface to XDefaultScreen, we get the default screen, that is required in many of the following functions. With rootWindow, the interface to XRootWindow(), we get the root window. We need it in order to set the parent window in the most important function of the above code: createSimpleWindow, the interface to XCreateSimpleWindow.

This function takes as arguments: the display, the parent window of
the window to be created, the x position, the y position, the width,
the height, the border width, the border pixel, and the background
pixel.

The x and y positions are relative to the upper left corner of the
parent window's inside borders.

In order to retrieve the values of the black and white pixels for that
specific screen, we use two specific functions:
blackPixel, the interface to the X11 library function
BlackPixel, and whitePixel, the interface to the X11 library function
WhitePixel

The function createSimpleWindow will return the window ID and, with this ID, we can start manipulating our newly created window, as we do, in the above code, with the function setTextProperty, interface to the X11 library function XSetTextProperty().

This function is needed by our code to set the window's name so your window manager will display on some decoration attached to the window (other window managers will not display anything, for instance a tiling WM like Xmonad)

Properties, such as the XTextProperty, have a string name and a numerical identifier called an atom. An atom is an ID that uniquely identifies a particular property. Property name strings are typically all upper case - with the first letter in low case when translated into Haskell - with words separated by underscores. In our example, we set the WM_NAME property to "Hello World".

Creating and manipulating a window is just the first step to have a new window displayed. In order for the window to become visible we must map it with
mapWindow, the interface to the X11 library function XMapWindow(). This will make the window visible.

Xlib will not send requests and calls to the X server immediately, but will buffer them and send the full buffer when some given conditions are met.

One way to force the flushing of the output buffer is to call sync, the interface to the X11 library function XSync(), which takes 2 arguments: the connection (dpy) and a Boolean value that indicates whether XSync() must discard all events on the event queue.

After that, the X server will eventually display our window.

The rest of the above example does nothing else but block execution for 10 seconds (to let you stare at your newly created window) and then exits.

6 Window attributes

Even though in our "Hello World" example we set the window's dimension, we have no assurance that the window manager will respect our decision.

Xmonad, for instance, will just create a window with the dimensions needed to fill its tiled screen, no matter what you set in createSimpleWindow.

But we decided to write a small clock that will behave as a statusbar, that is to say, we want to create a window that will specifically not be managed by the window manager.

In order to achieve this result we need to start dealing with window's
attributes.

There are two ways of dealing with window's attributes: the first is to set them at window's creation time, but in that case createSimpleWindow is not powerful enough.

This last one gives you an idea of the type of operation we must do in order to create a window (createSimpleWindow is just a specialization of this more complicated createWindow, with some arguments filled in with defaults): we need a function to allocate some memory for the creation of the foreign C structure, and then manipulate this foreign structure from within this function.

allocaSetWindowAttributes will take a function which takes the pointer to the foreign structure as its argument. This function will perform an IO action that is the action returned by allocaSetWindowAttributes.

In our case allocaSetWindowAttributes will take a function that will use createWindow to return the new window.

So, we will need to use createWindow inside allocaSetWindowAttributes. We will soon see how. But first let's analyze the other arguments of createWindow.

The display, the parent window, the coordinates and dimensions are the same as with createSimpleWindow. But now we must specify the depth of the screen, the window's class, the visual and the attribute mask. We also need to manipulate the XSetWindowAttribute after its creation by allocaSetWindowAttributes, before calling createWindow.

The depth is the number of bits available for each pixel to represent colors while the visual is way pixel values are translated to produce colors on the monitor.

The
WindowClass can either be copyFromParent, inputOutput, or inputOnly. In the first case the class is copied from the class of the parent window. An inputOnly window can only be used for receiving input events. In our code we are going to use inputOutput windows, windows that can receive input events and that can also be used to display some output.

The attributeMask "specifies which window attributes are defined in the attributes argument. This mask is the bitwise inclusive OR of the valid attribute mask bits. If value mask is zero, the attributes are ignored and are not referenced." (see http://www.tronche.com/gui/x/xlib/window/XCreateWindow.html).

In other words, in order to set more then one attribute, you need to pass a value mask such as:

Like simpleCreateWindow, our function is a wrapper around createWindow, but this time we are manually setting the CWOverrideRedirect flag.

As you see our function, unlike createSimpleWindow, does not have, among its arguments, the background and the border pixels. This colors can be set, for windows created with createWindow, using the attribute mask, and setting CWBackPixel and CWBorderPixel with the needed functions:
set_background_pixel and
set_border_pixel.

By the way, setting the border color with this version of mkUnmanagedWindow is actually useless since the border width is set to zero. In the next example we will set it to 1.

Our function needs also the screen now, since we have to retrieve the default depth and visual.

6.2 Changing an existing window's attributes

In order to change a window's attributes we just need the window ID in that specific X server, after that we need to unmap the window first, and then change its attributes with changeWindowAttributes, the interface to XChangeWindowAttrbutes() implemented by the darcs version of X11-extras.

flush the output buffer to have the X server actually unmap the window

change the attributes with the same procedure we used to set them when creating

the window

map the window again

flush the output buffer to see the change take effect.

You can modify this program to change other window's attributes.

7 Colors and color Ddepth

So far we have set the window background color as a window attribute. This is not the most convenient way to set the window background color: if we need to change it, we must change the window's attribute, and we have seen that this task requires unmapping the window, flushing the output with changeWindowAttributes within changeWindowAttributes, remapping the window and reflushing the output
buffer. Moreover we can do that only with the darcs version of X11-extras...

In the following sections we are going to adopt a more efficient way of setting the window's background color: we will start drawing into the window. But first we must familiarize with colors and the way the X server deals with them.

So far we have set the colors by using some functions to retrieve their pixel values: blackPixel and whitePixel. These functions take the display and the default screen and return respectively the pixel values for the black and the white colors in that screen.

A color is represented by a 32-bit unsigned integer, called a pixel value. The elements affecting the pixel representation of a color are:

the color depth;

the colormap, which is a table containing red, green, and blue intensity values;

the visual type.

All these elements are specific to a given piece of hardware, and so our X application must detect them in order to set colors appropriately for that given hardware.

The approach we are going to use to accomplish this task is this: we are going to use named colors, or colors represented by RGB triple, such as "red", "yellow", or "#FFFFFF", etc; and we are going to translate these colors into the pixel values appropriate for the screen we are operating on.

That is to say, given a display connection, a color map and a string - our color representation -, this function will return a tuple with the closest RGB values provided by the hardware and the exact RGB values, both encoded in a
Haskell Color data type. We will use the first approximated value.

The Color data type has a field name we will use to retrieve the needed pixel value: color_pixel.

To retrieve the colormap of the screen we used defaultColormap, the interface to the X11 library function
XDefaultColormap(), which requires the display and the screen, and returns the colormap of that screen.

Just give it a try. Now you can also experiment with different colors. This approach will assure that our application will work no matter the color depth of the screen we are working on.

8 Drawing in windows

The X server provides two objects that can be used to draw something
to: windows and pixmaps.

In this section we will start drawing into windows.

We have seen that changing the background color of a window is a troublesome operation, since the window must be unmapped and remapped, memory for a foreign structure allocated, and so on.

Instead, we can use some graphic operations to draw a rectangle over the window. We will latter manipulate the foreground, visible, color of this rectangle, that will become the new background of our window.

We can also use multiple rectangles with different dimension to decorate our window with a border, for instance. Later on we will print some text over these rectangles.

8.1 Drawing rectangles in a window

The client can request a number of graphic operations, such clearing an area, copying an area into another, drawing points, lines, rectangles, and text. Beside clearing, all operations are possible on all drawables, both windows and pixmaps.
Most requests for graphic operations include a graphic context, which is a structure that contains the parameters of the graphic operations. A graphic context includes the foreground color, the background color, the font of text, and other graphic parameters. When requesting a graphic operation, the client includes a graphic context.

In other words, as for setting window's attribute, we must use a foreign C structure to set parameters for graphic operations, and then we will feed this structure to the functions
that will perform these graphic operations.

There is one difference: instead of operating within a function that allocates memory and creates a pointer to the foreign structure, now we have to explicitly create the Graphic Context, and free it after having used it, with
createGC, the interface to XCreateGC, and
freeGC, the interface to
XFreeGC.

Be careful: if you create a graphic context without freeing it after use, you are going to end up with a sizeable memory leak!

The specific graphic functions we are going to need for drawing a rectangle into our window are:

The first two functions are needed to set the parameters in the Graphic Context. The third one will use this GC for filling a rectangle on the specified window. Just have a look to their type signatures:

As you see, now mkUnmanagedWindow sets a null border width and does not set any background color. Everything is easily done with rectangles.

8.2 Printing a string

Printing a string to a window requires operating with another foreign C structure, the XFontStruct, which contains all of the information regarding the metrics of font that the X server will use to display our string.

This structure will be used to perform some computations that are required for the correct placement of the text in the window.

As we have seen with the window's attributes and the Graphic Context, we need a function that returns a pointer to this foreign structure, pointer that must be freed after using it.

XLoadQueryFont, which requires the X connection and the font name, will perform two distinct operations: load the needed font and return its id (performed by XLoadFont() which doesn't have a Haskell interface) and query the font to retrieve the XFontStruct
(performed by
XQueryFont which does have a Haskell interface:
queryFont).

Given the FontStruct and the string to be printed, these functions
will provide some valuable information. The values returned by the
first one are related to font direction and vertical placement, while
the second one will return the total width of the string to be printed
with that specific font.

Finally we must remember to free the FontStruct with
freeFont the interface to
XFreeFont.

We can now write our function to print a string on a window, but first
we need to make some small modifications to our drawInWin function,
that will now take a string and will load and free the needed
FontStruct to be passed to the new printString function:

In the "let" part we use textWidth and textExtents to set vertical and horizontal alignment: this is done by calculating the x and y coordinates for drawImageString. In this example text will be vertically and horizontally centered (take into account that I have also enlarged the window, whose width now is 200 pixels).

For a reference of the meaning of font ascent and descent, and the origins of the rectangle drawn by drawImageString read the par. 8.6 (Drawing Text)
of the The Xlib Manual.

You may notice that printString takes a
Drawable, which can be either a window or a pixmap (see below).

We can now rewrite our example and finally see some text printed in our window:

"A pixmap is a region of memory that can be used for drawing. Contrary to windows, the contents of pixmaps are not automatically shown on the screen. However, the content of a pixmap (or a part of it) can be transferred to a window and vice versa. This allows for techniques such as double buffering.
Most of the graphical operations that can be done on windows can also be done on pixmaps. Windows and pixmaps are collectively named drawables, and their content data resides on the server."

This is not very difficult, and requires a very small change of our drawInWin function.

In order to create the pixmap we will use
createPixmap, the interface to
XCreatePixmap(), which takes the display connection, the drawable upon which the pixmap is created, the width, the height, and the depth of the screen.

We will then use
copyArea, the interface to
XCopyArea,
to copy the pixmap over the window.

10 Dealing with events

Run the clock, switch to a console (with Alt+Ctrl+F1) and come back to the X server where the clock is running.

What happened? The window disappeared, and came back after being redrawn by drawInWin.

The problem is that our application does not respond to the events the X server is sending to our window. If a window is covered or anyway no more visible on the screen, when the covered area becomes visible again the X server will send to that window an Expose event, so that the application using that window may redraw it. Since our clock doesn't listen for any event, the window will not be redrawn till a new call to drawInWin is done.

"Events are packets sent by the server to a client to communicate that something the client may be interested in has happened. For example, an event is sent when the user presses a key or clicks a mouse button.
Events are not only used for input: for example, events are sent to indicate the creation of new subwindows of a given window. Every event is relative to a window. For example, if the user clicks when the pointer is in a window, the event will be relative to that window. The event packet contains the identifier of that window."

The list of events a window will be reacting too is set as the
event mask attribute of that window, and so may be set at creation time, as we have seen for the background pixel, or with XChangeWindowAttributes, or by using
selectInput, the interface to
XSelectInput.

This is all we need to do in order to configure the window in such a way that it will receive Expose events.

Our problem is far bigger than that, unfortunately. Our problem, indeed, is that we must update (redraw) our window either in the case of an Expose event is received and when a given amount of time is elapsed. This second requirement was met by blocking our program execution with threadDelay. But when our program is blocked it cannot receive any event.

But if we start listening for events and no Expose event happens, after some time is elapsed we must update our window anyhow.

How can we achieve this?

Just to explain our problem with other words, if we change, in the last example, main and updateWin to listen to events we end up with something like this:

In main we added the selectInput call. Note that the event mask includes both Expose events, and mouse button press events (you may specify different types of events).

The second function, updateWin, required more modifications.

Now the sync call takes a True, and not a False any more. This means that when flushing the output buffer all events in the event queue will be discarded. This is necessary otherwise we are going to intercept previous events. For instance, if you change the Boolean to False, you will see a
[http://www.tronche.com/gui/x/xlib/events/exposure/graphics-expose-and-no-expose.html
NoExpose] event, that is the result of the application of XCopyArea in
drawInWin.

convert the event in a string with eventName (which requires X11-extras);

print the event name to the standard output: if we run our program from the command line, we can see the events received by our window.

I've also removed the threadDelay call. Guess why?

Just give it a run and you'll find out. If sync discards previous events, now nextEvent will block the program execution till an event occurs. If you don't press the mouse button over the window, or force an Expose event to occur, for instance by switching to a text console and back, the thread will be blocked in the safe foreign call to
[http://www.tronche.com/gui/x/xlib/event-handling/manipulating-event-queue/XNextEvent.html
XNextEvent], which, "if the event queue is empty, flushes the output buffer and
blocks until an event is received".