Found at: http://publish.ez.no/article/articleprint/87/

Xlib tutorial 1 - painting colors, graphics contexts and fonts



Have you ever wondered what is going on behind the scenes of X11 windowmanageres and toolkits? Here is a chance to get into native X programming with Xlib.


Hello world!
This first tutorial will get you into some of the basic elements of Xlib programming. We will create a small program that paints a big cross on the window and prints the famous "Hello world". Ofcourse all the X concepts will be explained along the way. If you want detail information about any of the functions, look at their respective man page (e.g "man XOpenDisplay").

The source code for this tutorial can be found on the bottom of the page.

Opening a display

Concept Display: A display is the physical connection to the X server. It is allways created with a call to XOpenDisplay which returns a Display structure holding information about the connection. Most X functions (those that require server interaction) take this structure as parameter.
Concept Screen: A screen denotes a physical monitor on your system. Under X it is represented by an integer value.

const char *display_name;
    if( ( display_name = (char *) getenv("DISPLAY") ) == NULL )
        display_name = "";

    /* Open a connection to the X server */
    Display *dpy = XOpenDisplay( display_name );
    if( !dpy ) 
    {
        // BSOD: we have no display.
        printf( "Cannot not open display %s\n", XDisplayName( display_name ) );
        exit( 1 );
    }

The call to XOpenDisplay takes one parameter, the display name. It is of the format "host:server.screen" and will ofcourse connect to the host given or localhost if it is ommited. Server is the running server on that computer. For most computers this will be 0 since you only run one instance of X at the time. (However you can start another with the -- :1 parameter to startx, which is very usefull for testing). The last part of the display name is the optional screen part. It is used for systems with more than one physical monitor and denotes which of the monitors will be the default one. This will be 0 for almost all modern computers.
If XOpenDisplay returns 0 we couldn't open a display. Usually this is fatal and so we kill the application.

The root window


Concept Window: Windows are the building blocks of X applications. A window can be painted upon and it can have subwindows. When something happens inside a window e.g a mouseclick or keypress an Event will be generated and sent to the client that created this window. Do not confuse a Window with an application window decorated by a window manager. An application will generally contain lots of Windows e.g buttons, scrollbars, menuitems. A window is represented by an integer ID.
Concept Root Window: The root window is the toplevel window that covers a whole screen. Your toplevel application window will use the Root Window as its parent.

 int screen = DefaultScreen( dpy );
    Window root = RootWindow( dpy, screen );

Before we can get the root window we need to know what screen we are using. This is done with the DefaultScreen macro which returns the screen given in the optional screen element of the DISPLAY environment variable. (See Opening a display). With the second macro RootWindow we fetch the ID of the root Window.

Creating our application window


Concept Colormap: A colormap holds the translation for a given color into the data sent to the monitor. In theory each window can have it's own colormap, however this is almost never applicable or desirable. Ofcourse a system with multiple screens will have one colormap for each screen.
Concept mapping: A window that is visible on the screen is known to be mapped. A non-visible window is unmapped.

    Colormap colmap = DefaultColormap( dpy, screen );
    
    XColor dummy, black, red;
    XAllocNamedColor( dpy, colmap, "black", &black, &dummy );
    XAllocNamedColor( dpy, colmap, "red", &red, &dummy );
    
    // create a window that we can draw on.
    Window win = XCreateSimpleWindow( dpy, root, 100, 100, 200, 200, 0, black.pixel, black.pixel );
    // map the window (that is, show it)
    XMapWindow( dpy, win );


We start off getting the Colormap for our screen using another macro. Then we ask for the colors red and black using the XAllocNamedColor function. XAllocNamedColor allocates a read only entry in the colormap with the closest RGB value to the value we specified. A copy of the entry is returned to us.
It's time to create our toplevel application window. There are two functions we can use for this, XCreateWindow and XCreateSimpleWindow. To keep things simple we will use XCreateSimpleWindow which creates a window that copies most of its properties from the parent. We create a window that uses root as it's parent, position it at coordinates 100,100 with size 200,200 where 0,0 is the topleft corner of the root window.
Finally it's time to tell X to show the window. This is done by sending a XMapWindow request to the server.

Preparing for painting


Concept Graphics context (GC): A ghraphics context is a structure that defines the attributes of a painting operation. It is kept on the server (to minimize traffic) and controls attributes like pen width, font type, filling style and the painting colors.

    XFontStruct *font = XLoadQueryFont( dpy, "fixed" );
    if( !font )
    {
        printf("Frekko: Error, couldn't load font\n" );
        exit( 1 );
    }

    XGCValues gv;
    gv.function = GXcopy;  // paint with src color only.
    gv.line_width = 3;
    gv.foreground = red.pixel; // foreground is the color reds pixel value.
    gv.background = red.pixel; // background is the color reds pixel value.
    gv.font = font->fid;

    GC gc = XCreateGC( dpy, root, GCFunction | GCLineWidth | GCForeground | GCBackground | GCFont, &gv );


Before we can start and paint on our newly created window we need to set up graphics context (hereafter called GC) as all painting operations take a GC as parameter. First off we start by loading a font. Fonts are a mess under X and is a possible target for another tutorial. We'll go easy and just load the "fixed" font which is ugly, but available on all systems. (We still check though. In case something is wrong crash with an error and not with a segfault when painting). The call to XLoadQueryFont returns a pointer to a XFontStruct which holds information about the font we just loaded.
Initializing a new GC is done through the XGCValues struct. The first attribute we set is the function attribute. This controls how the "paint" is applied to already existing "paint". GXcopy simply takes no account to what's already there and paints right over it. Another usefull function is GXinvert which inverts the existing paint (nice for the resize rubberband feature used in many window managers).
gv.line_width controls the width of any lines drawn, foreground and background control the colors drawn and are set to the pixel values of the colors we created and finaly the font is set to the freshly loaded font.
Lots of X calls take an attribute structure as parameter. Common for most of them is that you don't need to specify all attributes. This also holds for XCreateGC and we specify which elements to use by ORing the component mask bits together.

Painting galore


    // get the geometry of our window, the windowmanager might have changed it.
    XWindowAttributes attr;
    XGetWindowAttributes( dpy, win, &attr );
    int w = attr.width, h = attr.height;

    // Draw a big fat cross that covers the whole window.
    XDrawLine( dpy, win, gc, 0, 0, w, h );
    XDrawLine( dpy, win, gc, 0, h, w, 0 );

    const char *text = "Hello world";

    // fetch the height of the font, and the width of the text.
    int fontHeight = font->ascent + font->descent; // max height above and below baseline
    int textWidth = XTextWidth( font, text, strlen( text ) );
    
    // center the text at the top of the window
    XDrawString( dpy, win, gc, (w - textWidth)/2, fontHeight, text, strlen( text ) );
    
    XFlush( dpy );

One little detail remains before we start painting. We have to fetch the window size again. Why do we need to do this you may ask, we set the window size ourselves. This is easy to answer: the windowmanager may have (for any reason) decided to resize our window before showing it. We use the simplest method of obtaining window attributes, namely with the XWindowAttributes function which returns all the attributes in a nice structure.

Once everything is set up painting is actually pretty easy since only really primitive painting functions are available in Xlib. Note that all the painting functions take a GC as parameter and that the coordinates given are in window space. (0,0 is the topleft corner of the window that is beeing painted).

Finally we send an XFlush command. This is to ensure that the paint commands are actually sent to the server. This has to do with the client/server modell of X which will be explained further in tutorial2.

Make it run


    while(true)
    {
        // sleep here, so we won't use 100% cpu.
        sleep(10);
        break; // quit after 10 seconds
    }

    XFreeFont( dpy, font );
    XFreeGC( dpy, gc );
    XCloseDisplay( dpy );


All X programs need an event loop which processes events from the X server until the program wants to quit. As events will be covered in tutorial 2, a fake eventloop will do the trick. It let's the program sleep for 10 seconds then exits our "event loop".

Finally we clean up the resources we have allocated. Usually most located X resources are freed with XFree(). Fonts and GC's are exceptions and are freed with XFreeFont and XFreeGC. XCloseDisplay disconnects us from the X server and frees the resources used by the Display struct. Naturaly you can't use resources after they have been freed.


Final words


This tutorial has shown some basic features of the Xlib library. However a more object oriented approach is preferable when dealing with larger applications. I have enhanced the tutorial code to include a painter and a font class which hides some of the X details from the user. The next tutorial will be based on the object oriented version.
Also worth noting is that repainting after resize or covering does not work. This will be handled in the second tutorial.

The latest version of this tutorial is part of the frekko project and can always be found here.

Attached files:

tutorial1-obj-tar.gz
tutorial1.tar.gz

| Back to normal page view |