C-Scene Issue #08
Introduction to the X11 library
Christian Sunesson

Introduction to the X11 library

Abstract

The X11 system provides a relatively low-level C library for writing X-clients. Those are the programs that use an X-display for their user interface. Xlib can draw simple solids, receive events, write text in various fonts, and also do primitive interprocess comunication. This document will try to give an overview over the concepts presented in Xlib, and other small notes. This article especially covers topics that are of interest for small grapic gems and games. This article also assumes that the reader has access to the X11 manpages, because functions will not be described in great depth.

Introduction

The design of X11 evolves around the fact that it's network transparent. This means that the program you're running doesn't necessarily have to run on the machine you're in front of. To accomplish this, all communication between the x display and the client uses a serial communication channel.

Display

The Display abstracts the connection to the unit the user is interacting with. Therefore all xlib functions that will do something with the display need to know the display connection. It is the mother of all xlib types, as you need it before you create any of the other.

The Screen number

When the display connection is established you have access to all the screens of the unit. Generally you should query the display with DefaultScreen() to find out which to use, as the user can specify the default with the display name used. Note that the screen is a regular integer stored as 'int'.

 
   DISPLAYNAME := [HOSTNAME] ':' DISPLAYNUM ['.' SCREEN]
   HOSTNAME    := the local host resolve name
   DISPLAYNUM  := which of the running servers
   SCREEN      := the screen of the server
Examples: "barney.bedrock.net:1.3", ":0.0", "bill.microsoft.com:0.0"

You should be aware that two connections to one display using the same screen can share resources. Resources in a display that do not share the same screen can _not_ share anything, as the two screens might have different bit depths color setups and so on (monocolor vs. indexed color vs. truecolor).

Window

The X11 windowing system applies a system called 2-and-a-half dimensional for drawing. You specify x, y, and a window to draw in. A window resides in a hierarchy where the top level window is called the root window. Sibling windows can obscure each other and descendant windows are clipped by their parents.

Some of the properties of a window are the position, size, mapped/unmapped state, bit depth, inout/out class, and event interest. The mapped state is whether the window is active to the display, the inout class can limit windows just to take input from the user and not output graphics (invisible). The input-only windows are only interested in knowing how the mouse moves and what keys are pressed when the window is 'in focus' (keyboard events). Of course the window must have asked for those events. There are a few more cosmetic properties of a window, but they aren't interesting to us quite yet.

XEvent

Most Xlib programs are event based. That means they just wait for something to happen and acts upon that then return to wait for another event. Each window you've created has it's own set of registered events. These events range from mouse-, and key-input, to exposures, and other more specialized things like inter-client communication and window management.

The XEvent type is a union of a bunch of different structs, all with with the first entries common to the XAnyEvent struct.

typedef struct {
   int type;
   unsigned long serial;    /* # of last request processed by server */
   Bool send_event;         /* true if this came from a SendEvent request */
   Display *display;        /* Display the event was read from */
   Window window;
} XAnyEvent;
For a complete list of the input event masks you might want to view the <X11/X.h> header, and <X11/Xlib.h> for the XEvent union's content. The function XSelectInput is used for registering interest in most of the different events. This is done by passing the bitwise OR of the event masks.

An example, it will open a window then live until a the window is clicked in.

Exposures

Here is a solution that most people find annoying at first because it is different from their previous graphics programming experience. The X-display does not remember the content of a window, so it needs to be redrawn by the client whenever it is exposed. We need the X display to tell us when the window needs to be redrawn. The event that notifies about it is called "Expose" and is selected with "ExposureMask".

typedef struct {
   int type;
   unsigned long serial;   /* # of last request processed by server */
   Bool send_event;        /* true if this came from a SendEvent request */
   Display *display;       /* Display the event was read from */
   Window window;
   int x, y;
   int width, height;
   int count;              /* if non-zero, at least this many more */
} XExposeEvent;
If you receive one expose event with a nonzero count, you're certain to get more. So you can wait until you've got one expose event with a count of zero until you redraw all of your window again. Or you might choose to just draw the parts that need exposure with the information contained about the location(x,y) and size(width, height) for each expose event.

The graphics context - GC

The concept is as annoying at first. A set of GCs can be thought of as a box of pencils. Each have a color, a width, and other properties. A GC is created with a Window as argument and can only be used with other windows that have the same bit-depth.

typedef struct {
   int function;            /* logical operation */
   unsigned long plane_mask;/* plane mask */
   unsigned long foreground;/* foreground pixel */
   unsigned long background;/* background pixel */
   int line_width;          /* line width (in pixels) */
   int line_style;          /* LineSolid, LineOnOffDash, LineDoubleDash */
   int cap_style;           /* CapNotLast, CapButt, CapRound, 
                               CapProjecting */
   int join_style;          /* JoinMiter, JoinRound, JoinBevel */
   int fill_style;          /* FillSolid, FillTiled, FillStippled, 
                               FillOpaqueStippled*/
   int fill_rule;           /* EvenOddRule, WindingRule */
   int arc_mode;            /* ArcChord, ArcPieSlice */
   Pixmap tile;             /* tile pixmap for tiling operations */
   Pixmap stipple;          /* stipple 1 plane pixmap for stippling */
   int ts_x_origin;         /* offset for tile or stipple operations */
   int ts_y_origin;
   Font font;               /* default text font for text operations */
   int subwindow_mode;      /* ClipByChildren, IncludeInferiors */
   Bool graphics_exposures; /* boolean, should exposures be generated *
   int clip_x_origin;       /* origin for clipping */
   int clip_y_origin;
   Pixmap clip_mask;        /* bitmap clipping; other calls for rects */
   int dash_offset;         /* patterned/dashed line information */
   char dashes;
} XGCValues;
Glancing through it, you see that a GC also keep information about font to use, different methods for filling solids, and bitmap controlled clipping. Plus of course a lot more that is hardly ever useful. :)

GCs are different from many other types in Xlib in that it isn't an ID for a resource on the display, but rather an opaque struct pointer that caches changes to commit them first when it need to (i.e. before drawing). This means that while you can share a gc between two clients, it is not recommended because they're not synced. And also Xlib makes it hard to do this by not having the interface to do it. (There is a possibility to get the resource ID of the GC with XGContextFromGC(), but you cannot build a GC from a GContext value.)

Drawing

Mastering the drawing functions isn't very hard. There is functions for drawing lines, like XDrawSegments, rectangles with XDrawRectangle. And there is the XDrawArc for circles, ellipses and arcs. If you want your primitives filled, then look at the XFill* functions.

The trouble is in modifying the GC to your needs, for example, changing color or font. And colors under X11 is a pain, because this is supposed to be a portable system and programs should work on black and white displays as well as truecolor ones. The easy way out is to avoid using anything else than black and white in your program. Which is often ok and will maintain functionality, but it will not impress your friends. Either learn about the color system in X or get new friends.

Visuals

A Visual keep information about the screen's capabilities. This can generally be ignored if colors aren't needed, then use DefaultVisual() and only the contrasting colors BlackPixel() and WhitePixel(). If there is a need of a certain bit depth or truecolor/indexed color mode, then find a visual with XMatchVisualInfo() and fail to the user's disappointment if there was no match. Note that you need the X11/Xutil.h header for these functions.

Color classes:

Colormap Type          Read/Write   Read-only
-----------------      ----------   ----------
Monochrome/Gray        GrayScale    StaticGray
Single index RGB       PseudoColor  StaticColor
Decomposed index RGB   DirectColor  TrueColor

Colors

To make as many applications as possible fit into using a single colormap Xlib increases the chances of applications to pick the same colors by naming the colors. This is of course only convenient for decoration kind of colors. XAllocNamedColor() is the simplest interface for this. This color is a shared color, you cannot modify it.

An image viewer is an easy to imagine application to aquire a lot of colors. It would do best by creating it's own colormap with XCreateColormap() and make sure to allocate all colors in there, then change them to fit the need with XStoreColors(). The function XAllocColorCells() will allocate modifiable entries in an existing colortable. Colortables are associated with windows, refer to XCreateWindow() or XSetWindowAttributes().

Second example. This time something colorful and graphic happens. Click in the window to terminate the program.

Writing text

Drawing text isn't much different from the basic primitives, as long as you stick with codepages that fit in 8bit strings. After that you have a dense jungle of i18n to wander through. But lets take what is easy first.

What you need is a Font to write with. There is a list available if you type the command 'xlsfonts'. A user configured program need to XListFonts(), XLoadFont() and optionally XQueryFont() before it can do something useful. The function XLoadQueryFont() encapsulates all of this if the font name is known to exist.

With the XFontStruct obtained the client can measure the size of a string locally with XTextExtents(), XQueryExtents() would let the server do the measurement and that causes more network traffic. From the XCharStruct filled out the text height is ascent+descent. Remember that the x,y position when text is drawn is measured from the baseline of the text. The upper left corner will then be [x, y-ascent].

Internationalized text makes this worse, because it involves the whole locale subsystem of X11. You need to XSetLocaleModifiers(), create a resource-db with XrmGetFileDatabase(), XOpenOM(), and finally XCreateOC(). The latter have a particularly ugly interface. The OC (output context) is interchangeable with FontSets, so you can use it as argument to XmbDrawString/XwcDrawString. The first function is for multibyte and the second is wide-character encoding of unicode.

Text input

Feeding your program with input has the problem that not all keyboards are the same. This is solved by mapping keycodes to so called KeySym values. And some keysyms are in turn mapped to strings. Both of these mappings are found with XLookupString().

Internationalized input, now here we got a sweetie. As with output you need to have a correct locale set, called XOpenIM() and XCreateIC(). Then you have XwcLookupString() to do the same as XLookupString(). What adds greatly to the complexity is that not all methods of typing in glyphs give you the glyph directly. A single glyph could consist of a few keystrokes that the user wants to be able to edit. Or the input method could even supply a whole dictionary of all glyphs and provide means to point and click your way to the correct glyph. In the last case the input method want it's own window, this window needs a registered eventmask and a way to receive it's events. For this reason XFilterEvent() needs to be called after XNextEvent() to decide what events went to the input method and which did not.

As it seems internationalization adds a lot of complexity to a program. And it does, no doubt about it. But when it is done, and done good, it will seamlessly be able to understand American, Hebrew, Futhark, or Klingon for those with an inkling. I18n is simply a too technologically superior thing to be passed of by ignorance of other cultures.

Third example. The funny graphics is replaced with some text input-output. I18n avoided thank you. Click and type in the window. Escape terminates the program.

Efficiency Considerations

Generally always keep the network traffic to a minimum. Second is not to acquire too many resources on the X display (windows, GCs, ...). Speed seems to come for free if you've got the first two. But then again it might be because the application was written with functionality in mind and not visual appearance.

Buffered windows

If the picture in a window is complex you can ask the X display to buffer the window if resources exist. This is called backing store. Either it is turned on when the window is created with XCreateWindow(), or it is toggled with XSetWindowAttributes(). Sometimes it is better to make an obscuring window save what is underneath, like menues and the like do.

Another way is to create a so called pixmap that is drawn into with the same functions as with regular windows. Then paste this into windows with XCopyArea(). Pixmaps also have uses as in window background images and pattern for filling solids.

Client side images

When the images are very complex and difficult to render then using XDraw*/XFill* routines turns proportionally awkward. Also the speed might not be all that impressive. This is when all good men, err beings, turn to client side images. Set bit and byteorder in the XImage struct to host endian then Create the image with XCreateImage().

XImages have a basic putpixel interface for manipulation, XPutPixel() and XGetPixel(). This is slow but very portable to all the possible formats you might meet. If more juice is required, be ready to write a set of custom functions for each image configuration, like, format, depth and everything else the XImage struct holds. XPutImage() and XGetImage() pastes and copies part or whole images to/from the display.

Shared memory images and pixmaps

The top notch efficiency upgrader. Only works if server and client runs on the same machine. The client writes into shared memory, the server reads it. The concept is simple. This have been successfully been used for games like xkoule, xdoom, and xquake. Shared images is an extension called MIT-SHM, it requires libXext. Proper documentation can be found at ftp://ftp.x.org/pub/R6.4/xc/doc/hardcopy/Xext/mit-shm.PS.gz.

  1. Create the shared image with XShmCreateImage()
  2. Require the SysV shared memory segment.
  3. Command the server to attach the segment with XShmAttach()
  4. Continue almost as with a normal XImage.
Fourth example and last. This time XImages will be used for sending complex images to the server. The extension MIT-SHM will be used where available. A little hack is required to find out if using MIT-SHM is possible.

That's all folks!


Copyright Christian Sunesson - nossenus - cel98csn@mds.mdh.se - 1999 March