Basic Graphics Programming With The Xlib
Library
1. Preface
2. The Client And Server Model Of The X Window System
3. GUI programming - the Asynchronous Programming Model
4. Basic Xlib Notions
1. The X Display
2. The GC - Graphics Context
3. Object Handles
4. Memory Allocation For Xlib Structures
5. Events
5. Compiling Xlib-Based Programs
6. Opening And Closing The Connection To An X Server
7. Checking Basic Information About A Display
8. Creating A Basic Window - Our "hello world" Program
9. Drawing In A Window
1. Allocating A Graphics Context (GC)
2. Drawing Primitives - Point, Line, Box, Circle...
10.X Events
1. Registering For Event Types Using Event Masks
2. Receiving Events - Writing The Events Loop
3. Expose Events
4. Getting User Input
1. Mouse Button Click And Release Events
2. Mouse Movement Events
3. Mouse Pointer Enter And Leave Events
4. The Keyboard Focus
5. Keyboard Press And Release Events
5. X Events - A Complete Example
11.Handling Text And Fonts
1. The Font Structure
2. Loading A Font
3. Assigning A Font To A Graphics Context
4. Drawing Text In A Window
12.Windows Hierarchy
1. Root, Parent And Child Windows
2. Events Propagation
13.Interacting With The Window Manager
1. Window Properties
2. Setting The Window Name And Icon Name
3. Setting Preferred Window Size(s)
4. Setting Miscellaneous Window Manager Hints
5. Setting An Application's Icon
14.Simple Window Operations
1. Mapping And UN-Mapping A Window
2. Moving A Window Around The Screen
3. Resizing A Window
4. Changing Windows Stacking Order - Raise And Lower
5. Iconifying And De-Iconifying A Window
6. Getting Info About A Window
15.Using Colors To Paint The Rainbow
1. Color Maps
2. Allocating And Freeing Color Maps
3. Allocating And Freeing A Color Entry
4. Drawing With A Color
16.X Bitmaps And Pixmaps
1. What Is An X Bitmap? An X Pixmap?
2. Loading A Bitmap From A File
3. Drawing A Bitmap In A Window
4. Creating A Pixmap
5. Drawing A Pixmap In A Window
6. Freeing A Pixmap
17.Messing With The Mouse Cursor
1. Creating And Destroying A Mouse Cursor
2. Setting A Window's Mouse Cursor
Preface
This tutorial is the first in a series of "would-be" tutorials about graphical programming in the X
window environment. By itself, it is useless. A real X programmer usually uses a much higher level of
abstraction, such as using Motif (or its free version, lesstiff), GTK, QT and similar libraries. However,
we need to start somewhere. More than this, knowing how things work down below is never a bad idea.
After reading this tutorial, one would be able to write very simple graphical programs, but not
programs with a descent user interface. For such programs, one of the previously mentioned libraries
would be used.
The Client And Server Model Of The X Window System
The X window system was developed with one major goal - flexibility. The idea was that the way
things look is one thing, but the way things work is another matter. Thus, the lower levels provide the
tools required to draw windows, handle user input, allow drawing graphics using colors (or black and
white screens), etc. To this point, a decision was made to separate the system into two parts. A client
that decides what to do, and a server that actually draws on the screen and reads user input in order to
send it to the client for processing.
This model is the complete opposite of what one is used to when dealing with clients and servers. In
our case, the user seats near the machine controlled by the server, while the client might be running on
a remote machine. The server controls the screen, mouse and keyboard. A client may connect to the
server, request that it draws a window (or several windows), and ask the server to send it any input the
user sends to these windows. Thus, several clients may connect to a single X server - one might be
running an email software, one running a WWW browser, etc. When input it sent by the user to some
window, the server sends a message to the client controlling this window for processing. The client
decides what to do with this input, and sends the server requests for drawing in the window.
The whole session is carried out using the X message protocol. This protocol was originally carried
over the TCP/IP protocol suite, allowing the client to run on any machine connected to the same
network that the server is. Later on the X servers were extended to allow clients running on the local
machine more optimized access to the server (note that an X protocol message may be several hundreds
of KB in size), such as using shared memory, or using Unix domain sockets (a method for creating a
logical channel on a Unix system between two processes).
GUI programming - the Asynchronous Programming Model
Unlike conventional computer programs, that carry some serial nature, a GUI program usually uses an
asynchronous programming model, also known as "event-driven programming". This means that that
program mostly sits idle, waiting for events sent by the X server, and then acts upon these events. An
event may say "The user pressed the 1st button mouse in spot x,y", or "the window you control needs
to be redrawn". In order for the program to be responsive to the user input, as well as to refresh
requests, it needs to handle each event in a rather short period of time (e.g. less than 200 milliseconds,
as a rule of thumb).
This also implies that the program may not perform operations that might take a long time while
handling an event (such as opening a network connection to some remote server, or connecting to a
database server, or even performing a long file copy operation). Instead, it needs to perform all these
operations in an asynchronous manner. This may be done by using various asynchronous models to
perform the longish operations, or by performing them in a different process or thread.
So the way a GUI program looks is something like that:
1. Perform initialization routines.
2. Connect to the X server.
3. Perform X-related initialization.
4. While not finished:
1. Receive the next event from the X server.
2. handle the event, possibly sending various drawing requests to the X server.
3. If the event was a quit message, exit the loop.
5. Close down the connection to the X server.
6. Perform cleanup operations.
Basic Xlib Notions
In order to eliminate the needs of programs to actually implement the X protocol layer, a library called
'Xlib' was created. This library gives a program a very low-level access to any X server. Since the
protocol is standardized, A client using any implementation of Xlib may talk with any X server. This
might look trivial these days, but back at the days of using character mode terminals and proprietary
methods of drawing graphics on screens, this looked like a major break-through. In fact, you'll notice
the big hype going around thin-clients, windows terminal servers, etc. They are implementing today
what the X protocol enabled in the late 80's. On the other hand, the X universe is playing a catch-up
game regarding CUA (common user access, a notion made by IBM to refer to the usage of a common
look and feel for all programs in order to ease the lives of the users). Not having a common look and
feel was a philosophy of the creators of the X window system. Obviously, it had some drawbacks that
are evident today.
The X Display
The major notion of using Xlib is the X display. This is a structure representing the connection we have
open with a given X server. It hides a queue of messages coming from the server, and a queue of
pending requests that our client intends to send to the server. In Xlib, this structure is named 'Display'.
When we open a connection to an X server, the library returns a pointer to such a structure. Later, we
supply this pointer to any Xlib function that should send messages to the X server or receive messages
from this server.
The GC - Graphics Context
When we perform various drawing operations (graphics, text, etc), we may specify various options for
controlling how the data will be drawn - what foreground and background colors to use, how line edges
will be connected, what font to use when drawing some text, etc). In order to avoid the need to supply
zillions of parameters to each drawing function, a graphical context structure, of type 'GC' is used. We
set the various drawing options in this structure, and then pass a pointer to this structure to any drawing
routines. This is rather handy, as we often needs to perform several drawing requests with the same
options. Thus, we would initialize a graphical context, set the desired options, and pass this GC
structure to all drawing functions.
Object Handles
When various objects are created for us by the X server - such as windows, drawing areas and cursors -
the relevant function returns a handle. This is some identifier for the object that actually resides in the
X server's memory - not in our application's memory. We can later manipulate this object by supplying
this handle to various Xlib functions. The server keeps a mapping between these handles and the actual
objects it manages. Xlib provides various type definitions for these objects (Window, Cursor, Colormap
and so on), which are all eventually mapped to simple integers. We should still use these type names
when defining variables that hold handles - for portability reasons.
Memory Allocation For Xlib Structures
Various structure types are used in Xlib's interface. Some of them are allocated directly by the user.
Others are allocated using specific Xlib functions. This allows the library to initialize properly these
structures. This is very handy, since these structures tend to contain a lot of variables, making it rather
tedious for the poor programmer to initialize. Remember - Xlib tries to be as flexible as possible, and
this means it is also as complex as it can get. Having default values will enable a beginner X
programmer to use the library, without interfering with the ability of a more experienced programmer
to tweak with these zillions of options.
As for freeing memory, this is done in one of two ways. In cases where we allocated the memory - we
free it in the same manner (i.e. use free() to free memory allocated using malloc()). In case we
used some Xlib function to allocate it, or we used some Xlib query method that returns dynamically
allocated memory - we will use the XFree() function to free this memory block.
Events
A structure of type 'XEvent' is used to pass events received from the X server. Xlib supports a large
amount of event types. The XEvent structure contains the type of event received, as well as the data
associated with the event (e.g. position on the screen where the event was generated, mouse button
associated with the event, region of screen associated with a 'redraw' event, etc). The way to read the
event's data depends on the event type. Thus, an XEvent structure contains a C language union of all
possible event types (if you're not sure what C unions are, it is time to check your proffered C language
manual...). Thus, we could have an XExpose event, an XButton event, an XMotion event, etc.
Compiling Xlib-Based Programs
Compiling Xlib-Based programs requires linking them with the Xlib library. This is done using a
compilation command like this:
cc prog.c -o prog -lX11
If the compiler complains that it cannot find the X11 library, try adding a '-L' flag, like this:
cc prog.c -o prog -L/usr/X11/lib -lX11
or perhaps this (for a system with release 6 of X11):
cc prog.c -o prog -L/usr/X11R6/lib -lX11
On SunOs 4 systems, the X libraries are placed in /usr/openwin/lib:
cc prog.c -o prog -L/usr/openwin/lib -lX11
and so on...
Opening And Closing The Connection To An X Server
An X program first needs to open the connection to the X server. When we do that, we need to specify
the address of the host running the X server, as well as the display number. The X window system can
support several displays all connected to the same machine. However, usually there is only one such
display, which is display number '0'. If we wanted to connect to the local display (i.e. the display of the
machine on which our client program runs), we could specify the display as ":0". To connect to the first
display of a machine whose address is "simey", we could use the address "simey:0". Here is how the
connection is opened:
#include /* defines common Xlib functions and structs. */
.
.
/* this variable will contain the pointer to the Display structure */
/* returned when opening a connection. */
Display* display;
/* open the connection to the display "simey:0". */
display = XOpenDisplay("simey:0");
if (display == NULL) {
fprintf(stderr, "Cannot connect to X server %s\n", "simey:0");
exit (-1);
}
Note that is common for X programs to check if the environment variable 'DISPLAY' is defined, and if
it is, use its contents as the parameter to the XOpenDisplay() function.
When the program finished its business and needs to close the connection the X server, it does
something like this:
XCloseDisplay(display);
This would cause all windows created by the program (if any are left) to be automatically closed by the
server, and any resources stored on the server on behalf of the clients - to be freed. Note that this does
not cause our client program to terminate - we could use the normal exit() function to do that.
Checking Basic Information About A Display
Once we opened a connection to an X server, we should check some basic information about it: what
screens it has, what is the size (width and height) of the screen, how many colors it supports (black and
white? grey scale? 256 colors? more?), and so on. We will show a code snippet that makes few of these
checks, with comments explaining each function as it is being used. We assume that 'display' is a
pointer to a 'Display' structure, as returned by a previous call to XOpenDisplay().
/* this variable will be used to store the "default" screen of the */
/* X server. usually an X server has only one screen, so we're only */
/* interested in that screen. */
int screen_num;
/* these variables will store the size of the screen, in pixels. */
int screen_width;
int screen_height;
/* this variable will be used to store the ID of the root window of our */
/* screen. Each screen always has a root window that covers the whole */
/* screen, and always exists. */
Window root_window;
/* these variables will be used to store the IDs of the black and white */
/* colors of the given screen. More on this will be explained later. */
unsigned long white_pixel;
unsigned long black_pixel;
/* check the number of the default screen for our X server. */
screen_num = DefaultScreen(display);
/* find the width of the default screen of our X server, in pixels. */
screen_width = DisplayWidth(display, screen_num);
/* find the height of the default screen of our X server, in pixels. */
screen_height = DisplayHeight(display, screen_num);
/* find the ID of the root window of the screen. */
root_window = RootWindow(display, screen_num);
/* find the value of a white pixel on this screen. */
white_pixel = WhitePixel(display, screen_num);
/* find the value of a black pixel on this screen. */
black_pixel = BlackPixel(display, screen_num);
There are various other macros to get more information about the screen, that you can find in any Xlib
reference. There are also function equivalents for some of these macros (e.g. XWhitePixel, which does
the same as WhitePixel).
Creating A Basic Window - Our "hello world" Program
After we got some basic information about our screen, we can get to creating our first window. Xlib
supplies several functions for creating new windows, one of which is XCreateSimpleWindow().
This function gets quite a few parameters determining the window's size, its position, and so on. Here
is a complete list of these parameters:
Display* display
Pointer to the Display structure.
Window parent
The ID of an existing window that should be the parent of the new window.
int x
X Position of the top-left corner of the window (given as number of pixels from the left of the
screen).
int y
Y Position of the top-left corner of the window (given as number of pixels from the top of the
screen).
unsigned int width
Width of the new window, in pixels.
unsigned int height
Height of the new window, in pixels.
unsigned int border_width
Width of the window's border, in pixels.
unsigned long border
Color to be used to paint the window's border.
unsigned long background
Color to be used to paint the window's background.
Lets create a simple window, whose width is 1/3 of the screen's width, height is 1/3 of the screen's
height, background color is white, border color is black, and border width is 2 pixels. The window will
be placed at the top-left corner of the screen.
/* this variable will store the ID of the newly created window. */
Window win;
/* these variables will store the window's width and height. */
int win_width;
int win_height;
/* these variables will store the window's location. */
int win_x;
int win_y;
/* calculate the window's width and height. */
win_width = DisplayWidth(display, screen_num) / 3;
win_height = DisplayHeight(display, screen_num) / 3;
/* position of the window is top-left corner - 0,0. */
win_x = win_y = 0;
/* create the window, as specified earlier. */
win = XCreateSimpleWindow(display,
RootWindow(display, screen_num),
win_x, win_y,
win_width, win_height,
win_border_width, BlackPixel(display, screen_num),
WhitePixel(display, screen_num));
The fact that we created the window does not mean it will be drawn on screen. By default, newly
created windows are not mapped on the screen - they are invisible. In order to make our window
visible, we use the XMapWindow() function, as follows:
XMapWindow(display, win);
To see all the code we have gathered so far, take a look at the simple-window.c program. You'll see two
more function not explained so far - XFlush() and XSync(). The XFlush() function flushes all
pending requests to the X server - much like the fflush() function is used to flash standard output.
The XSync() function also flushes all pending requests to the X server, and then waits until the X
server finishes processing these requests. In a normal program this will not be necessary (you'll see
why when we get to write a normal X program), but for now we put it there. Try compiling the program
either with or without these function calls to see the difference in its behavior.
Drawing In A Window
Drawing in a window can be done using various graphical functions - drawing pixels, lines, circles,
rectangles, etc. In order to draw in a window, we first need to define various general drawing
parameters - what line width to use, which color to draw with, etc. This is done using a graphical
context (GC).
Allocating A Graphics Context (GC)
As we said, a graphical context defines several attributes to be used with the various drawing functions.
For this, we define a graphical context. We can use more than one graphical context with a single
window, in order to draw in multiple styles (different colors, different line widths, etc.). Allocating a
new GC is done using the XCreateGC() function, as follows (in this code fragment, we