[LUPG Home] [Tutorials] [Related Material] [Essays] [Project Ideas] [Send Comments]
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 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).
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:
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 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.
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.
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.
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.
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 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...
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 <X11/Xlib.h> /* 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);
}
XOpenDisplay()
function.
When the program finished its business and needs to close the connection
the X server, it does something like this:
XCloseDisplay(display);
exit()
function to do that.
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);
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
Window parent
int x
int y
unsigned int width
unsigned int height
unsigned int border_width
unsigned long border
unsigned long background
/* 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));
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 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).
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 assume "display" is a pointer
to a Display structure, and "win" is the ID of a previously created window):
/* this variable will contain the handle to the returned graphics context. */
GC gc;
/* these variables are used to specify various attributes for the GC. */
/* initial values for the GC. */
XGCValues values = CapButt | JoinBevel;
/* which values in 'values' to check when creating the GC. */
unsigned long valuemask = GCCapStyle | GCJoinStyle;
/* create a new graphical context. */
gc = XCreateGC(display, win, valuemask, &values);
if (gc < 0) {
fprintf(stderr, "XCreateGC: \n");
}
XCreateGC()
which attributes we want to set. This is what
the "valuemask" variable is for. We then use the "values" variable to specify
actual values for the attributes we defined in the "valuesmask". Thus, for
each constant used in "values", we'll use the matching constant in "valuesmask".
In this case, we defined a graphics context with two attributes:
Once we created a graphics context, we can use it in drawing functions. We can also modify its parameters using various functions. Here are a few examples:
/* change the foreground color of this GC to white. */
XSetForeground(display, gc, WhitePixel(display, screen_num));
/* change the background color of this GC to black. */
XSetBackground(display, gc, BlackPixel(display, screen_num));
/* change the fill style of this GC to 'solid'. */
XSetFillStyle(display, gc, FillSolid);
/* change the line drawing attributes of this GC to the given values. */
/* the parameters are: Display structure, GC, line width (in pixels), */
/* line drawing style, cap (line's end) drawing style, and lines */
/* join style. */
XSetLineAttributes(display, gc, 2, LineSolid, CapRound, JoinRound);
XCreateGC()
. We will use
just a few simple attributes in our tutorial, to avoid over-complicating it.
After we have created a GC
, we can draw on a window using this
GC, with a set of Xlib functions, collectively called "drawing primitives".
Without much fuss, lets see how they are used. We assume that "gc" is a
previously initialized GC
, and that 'win' contains the handle
of a previously created window.
/* draw a pixel at position '5,60' (line 5, column 60) of the given window. */
XDrawPoint(display, win, gc, 5, 5);
/* draw a line between point '20,20' and point '40,100' of the window. */
XDrawLine(display, win, gc, 20, 20, 40, 100);
/* draw an arc whose center is at position 'x,y', its width (if it was a */
/* full ellipse) is 'w', and height is 'h'. Start the arc at angle 'angle1' */
/* (angle 0 is the hour '3' on a clock, and positive numbers go */
/* counter-clockwise. the angles are in units of 1/64 of a degree (so 360*64 */
/* is 360 degrees). */
int x = 30, y = 40;
int h = 15, w = 45;
int angle1 = 0, angle2 = 2.109;
XDrawArc(display, win, gc, x-(w/2), y-(h/2), w, h, angle1, angle2);
/* now use the XDrawArc() function to draw a circle whose diameter */
/* is 15 pixels, and whose center is at location '50,100'. */
XDrawArc(display, win, gc, 50-(15/2), 100-(15/2), 15, 15, 0, 360*64);
/* the XDrawLines() function draws a set of consecutive lines, whose */
/* edges are given in an array of XPoint structures. */
/* The following block will draw a triangle. We use a block here, since */
/* the C language allows defining new variables only in the beginning of */
/* a block. */
{
/* this array contains the pixels to be used as the line's end-points. */
XPoint points[] = {
{0, 0},
{15, 15},
{0, 15},
{0, 0}
};
/* and this is the number of pixels in the array. The number of drawn */
/* lines will be 'npoints - 1'. */
int npoints = sizeof(points)/sizeof(XPoint);
/* draw a small triangle at the top-left corner of the window. */
/* the triangle is made of a set of consecutive lines, whose */
/* end-point pixels are specified in the 'points' array. */
XDrawLines(display, win, gc, points, npoints, CoordModeOrigin);
}
/* draw a rectangle whose top-left corner is at '120,150', its width is */
/* 50 pixels, and height is 60 pixels. */
XDrawRectangle(display, win, gc, 120, 150, 50, 60);
/* draw a filled rectangle of the same size as above, to the left of the */
/* previous rectangle. note that this rectangle is one pixel smaller than */
/* the previous line, since 'XFillRectangle()' assumes it is filling up */
/* an already drawn rectangle. This may be used to draw a rectangle using */
/* one color, and later to fill it using another color. */
XFillRectangle(display, win, gc, 60, 150, 50, 60);
XFillArc()
takes the same parameters as XDrawArc()
, but draws only the
inside of this arc (like XFillRectangle()
does to a rectangle
drawn using the XDrawRectangle()
function). There is also an
XFillPolygon()
function that fills the inside of a polygon.
It takes almost the same parameters as XDrawLines()
. However,
if the last point in the array has a different location than the first point
in the array, the XFillPolygon()
function automatically adds
another "virtual" lines, connecting these two points. Another difference
between the two functions, is that XFillPolygon()
takes
an additional parameters, shape, that is used to help the X server optimize
its operation. You can read about it in your manual pages. There are also
plural versions for these functions, namely XFillArcs()
and XFillRectangles()
.In an Xlib program, everything is driven by events. Event painting on the screen is sometimes done as a response to an event - an "expose" event. If part of a program's window that was hidden, gets exposed (e.g. the window was raised above other windows), the X server will send an "expose" event to let the program know it should repaint that part of the window. User input (key presses, mouse movement, etc.) is also received as a set of events.
After a program creates a window (or several windows), it should tell the X server what types of events it wishes to receive for this window. By default, no events are sent to the program. It may register for various mouse (also called "pointer") events, keyboard events, expose events and so on. This is done for optimizing the server-to-client connection (i.e. why send a program (that might even be running at the other side of the globe) an event it is not interested in?).
In Xlib, we use the XSelectInput()
function to register for
events. This function accepts 3 parameters - the display structure, an ID
of a window, and a mask of the event types it wishes to get. The window ID
parameter allows us to register for receiving different types of events for
different windows. Here is how we register for "expose" events for a window
whose ID is 'win':
XSelectInput(display, win, ExposureMask);
ExposureMask
is a constant defined in the "X.h" header file.
If we wanted to register to several event types, we can logically "or"
them, as follows:
XSelectInput(display, win, ExposureMask | ButtonPressMask);
This registers for "expose" events as well as for mouse button presses inside
the given window. You should note that a mask may represent several event
sub-types.
Note: A common bug programmers do is adding code to handle new event types in
their program, while forgetting to add the masks for these events in the
call to XSelectInput()
. Such a programmer then could sit down
for hours debugging his program, wondering "why doesn't my program notice
that i released the button??", only to find that they registered for button
press events, but not for button release events.
After we have registered for the event types we are interested in, we need to enter a loop of receiving events and handling them. There are various ways to write such a loop, but the basic loop looks like this:
/* this structure will contain the event's data, once received. */
XEvent an_event;
/* enter an "endless" loop of handling events. */
while (1) {
XNextEvent(display, &an_event);
switch (an_event.type) {
case Expose:
/* handle this event type... */
.
.
break;
default: /* unknown event type - ignore it. */
break;
}
}
XNextEvent()
function fetches the next event coming from
the X server. If no event is waiting, it blocks until one is received. When
it returns, the event's data is placed in the XEvent
variable
given to the function as the second parameter. After that, the "type" field of
this variable specifies what type of event we got. Expose
is
the event type that tells us there is a part of the window that needs to be
redrawn. After we handle this event, we go back and wait for the next
event to process. Obviously, we will need to give the user some way of
terminating the program. This is usually done by handling a special "quit"
event, as we'll soon see.
The "expose" event is one of the most basic events an application may receive. It will be sent to us in one of several cases:
When we get an "expose" event, we should take the event's data from the
"xexpose" member of the XEvent
structure (in our code
example we refer to it as "an_event.xexpose"). It contains several
interesting fields:
count
Window window
int x, y
int width, height
As an example, here is how we will draw a line across our window, whenever
we receive "expose" events. Assume this 'case' is part of the event loop's
switch
command.
case Expose:
/* if we have several other expose events waiting, don't redraw. */
/* we will do the redrawing when we receive the last of them. */
if (an_event.xexpose.count > 0)
break;
/* ok, now draw the line... */
XDrawLine(display, win, gc, 0, 100, 400, 100);
break;
User input traditionally comes from two sources - the mouse and the keyboard. Various event types exist to notify us of user input - a key being pressed on the keyboard, a key being released on the keyboard, the mouse moving over our window, the mouse entering (or leaving) our window and so on.
The first event type we'll deal with is a mouse button-press (or button
release) event in our window. In order to register to such an event type, we
would add one (or more) of the following masks to the event types we specify
for the XSelectInput()
function:
ButtonPressMask
ButtonReleaseMask
The event types to be checked for in our event-loop switch, are any of the following:
ButtonPress
ButtonRelease
The event structure for these event types is accessed as "an_event.xbutton", and contains the following interesting fields:
Window window
int x, y
int button
Button1
, Button2
, Button3
.Time time
As an example, here is how we will draw a black pixel at the mouse position,
whenever we receive "button press" events, with the 1st mouse button, and erase
that pixel (i.e. draw a white pixel) when the 2nd mouse button is pressed.
We assume the existence of two GCs, gc_draw with foreground color set to
black, and gc_erase, with foreground color set to white.
Assume that the following 'case' is part of the event loop's
switch
command.
case ButtonPress:
/* store the mouse button coordinates in 'int' variables. */
/* also store the ID of the window on which the mouse was */
/* pressed. */
x = an_event.xbutton.x;
y = an_event.xbutton.y;
the_win = an_event.xbutton.window;
/* check which mouse button was pressed, and act accordingly. */
switch (an_event.xbutton.button) {
case Button1:
/* draw a pixel at the mouse position. */
XDrawPoint(display, the_win, gc_draw, x, y);
break;
case Button2:
/* erase a pixel at the mouse position. */
XDrawPoint(display, the_win, gc_erase, x, y);
break;
default: /* probably 3rd button - just ignore this event. */
break;
}
break;
Similar to mouse button press and release events, we also can be notified
of various mouse movement events. These can be split into two families.
One is of mouse pointer movement while no buttons are pressed, and the
second is a mouse pointer motion while one (or more) of the buttons are
pressed (this is sometimes called "a mouse drag operation", or just
"dragging"). The following event masks may be added in the call to
XSelectInput()
for our application to be notified of such
events:
PointerMotionMask
ButtonMotionMask
Button1MotionMask
ButtonMotionMask
, but only when the 1st mouse button
is held pressed.Button2MotionMask
, Button3MotionMask
,
Button4MotionMask
, Button5MotionMask
The event types to be checked for in our event-loop switch, are any of the following:
MotionNotify
The event structure for these event types is accessed as "an_event.xbutton", and contains the following interesting fields:
Window window
int x, y
unsigned int state
Button1Mask
Button2Mask
Button3Mask
Button4Mask
Button5Mask
ShiftMask
LockMask
ControlMask
Mod1Mask
Mod2Mask
Mod3Mask
Mod4Mask
Mod5Mask
Time time
As an example, the following code handles a "draw mode" for a painting program,
that is, if the user moves the mouse while the 1st mouse button is being held
down, then we 'draw' on the screen. Note that this code has a flow: Since mouse
movement may generate many events, it might be that we won't get a mouse
motion event for each pixel the mouse moved over. Our program should
be able to cope with such a situation. One way to do that would be to remember
the last pixel the mouse was dragged over, and draw a line between that
position and the new mouse pointer position.
Assume that the following 'case' is part of the event loop's
switch
command.
case MotionNotify:
/* store the mouse button coordinates in 'int' variables. */
/* also store the ID of the window on which the mouse was */
/* pressed. */
x = an_event.xmotion.x;
y = an_event.xmotion.y;
the_win = an_event.xbutton.window;
/* if the 1st mouse button was held during this event, draw a pixel */
/* at the mouse pointer location. */
if (an_event.xmotion.state & Button1Mask) {
/* draw a pixel at the mouse position. */
XDrawPoint(display, the_win, gc_draw, x, y);
}
break;
Another type of event that applications might be interested at, is a mouse
pointer entering a window the program controls, or leaving such a window.
Some programs use these events to show the user that the application is now
in focus. In order to register for such an event type, we would add one
(or more) of the following masks to the event types we specify
for the XSelectInput()
function:
EnterWindowMask
LeaveWindowMask
The event types to be checked for in our event-loop switch, are any of the following:
EnterNotify
LeaveNotify
The event structure for these event types is accessed as "an_event.xcrossing", and contains the following interesting fields:
Window window
Window subwindow
int x, y
int mode
Button1
, Button2
, Button3
.Time time
unsigned int state
Button1Mask
Button2Mask
Button3Mask
Button4Mask
Button5Mask
ShiftMask
LockMask
ControlMask
Mod1Mask
Mod2Mask
Mod3Mask
Mod4Mask
Mod5Mask
Bool focus
True
if the window has the keyboard focus,
False
otherwise.There may be many windows on a screen, but only a single keyboard attached to them. How does the X server then know which window should be sent a given keyboard input? This is done using the keyboard focus. Only a single window on the screen may have the keyboard focus at a given time. There are Xlib functions that allow a program to set the keyboard focus to a given window. The user can usually set the keyboard focus using the window manager (often by clicking on the title bar of the desired window). Once our window has the keyboard focus, every key press or key release will cause an event to be sent to our program (if it registered for these event types...).
If a window controlled by our program currently holds the keyboard focus,
it can receive key press and key release events. In order to register
for such events, any of the following masks may be added to the call to
XSelectInput()
:
KeyPressMask
KeyPressMask
The event types to be checked for in our event-loop switch, are any of the following:
KeyPress
KeyRelease
The event structure for these event types is accessed as "an_event.xkey", and contains the following interesting fields:
Window window
unsigned int keycode
int x, y
Time time
unsigned int state
Button1Mask
Button2Mask
Button3Mask
Button4Mask
Button5Mask
ShiftMask
LockMask
ControlMask
Mod1Mask
Mod2Mask
Mod3Mask
Mod4Mask
Mod5Mask
As we mentioned, the key code is rather meaningless on its own, and is
affected by the specific keyboard device attached to the machine running
the X server. To actually use this code, we translate it into a key symbol,
which is standardized. We may use the XKeycodeToKeysym()
function
to do the translation. This function gets 3 parameters: a pointer to the
display, the key code to be translated, and an index (we'll supply '0' for
this parameter). Standard Xlib key codes are found in the include file
"X11/keysymdef.h". As an example for using the key press events together with
the XKeycodeToKeysym
function, we'll show how to handle key
presses of this sort: Pressing '1' will cause painting the pixel where
the mouse pointer is currently located. Pressing the DEL key will cause
to erase that pixel (using a 'gc_erase' GC). Pressing any of the letters (a
to z, upper case or lower case) will cause it to be printed to standard output.
Any other key pressed will be ignored.
Assume that the following 'case' is part of the event loop's
switch
command.
case KeyPress:
/* store the mouse button coordinates in 'int' variables. */
/* also store the ID of the window on which the mouse was */
/* pressed. */
x = an_event.xkey.x;
y = an_event.xkey.y;
the_win = an_event.xkey.window;
{
/* translate the key code to a key symbol. */
KeySym key_symbol = XKeycodeToKeysym(display, an_event.xkey.keycode, 0);
switch (key_symbol) {
case XK_1:
case XK_KP_1: /* '1' key was pressed, either the normal '1', or */
/* the '1' on the keypad. draw the current pixel. */
XDrawPoint(display, the_win, gc_draw, x, y);
break;
case XK_Delete: /* DEL key was pressed, erase the current pixel. */
XDrawPoint(display, the_win, gc_erase, x, y);
break;
default: /* anything else - check if it is a letter key */
if (key_symbol >= XK_A && key_symbol <= XK_Z) {
int ascii_key = key_symbol - XK_A + 'A';
printf("Key pressed - '%c'\n", ascii_key);
}
if (key_symbol >= XK_a && key_symbol <= XK_z) {
int ascii_key = key_symbol - XK_a + 'a';
printf("Key pressed - '%c'\n", ascii_key);
}
break;
}
}
break;
As an example for handling events, we will show the events.c program. This program creates a window, makes some drawings on it, and then enters an event loop. If it gets an expose event - it redraws the whole window. If it gets a left button press (or motion) event, it draws the pixel under the mouse pointer in black color. If it gets a middle button press (or motion) event, it draws the pixel under the mouse pointer in white color (i.e. erases this pixel). Some note should be taken as to how these picture changes are handled. It is not sufficient to just draw the pixel with the appropriate color. We need to make a note of this color change, so on the next expose event we will draw the pixel again with the proper color. For this purpose, we use a huge (1000 by 1000) array representing the window's pixels. Initially, all cells in the array are initialized to '0'. When drawing a pixel in black, we set the matching array cell to '1'. When drawing a pixel in white, we set the matching array cell to '-1'. we cannot just reset it back to '0', otherwise the original drawing we painted on the screen will always be erased. Finally, when the user presses on any on the keyboard, the program exits.
When running this program, you will note that motion events often skip pixels. If the mouse was moved from one point to another in a quick motion, we will not get motion events for each pixel the mouse pointer moved over. Thus, if it was our intention to handle these gaps properly, we would need to remember the location of the last motion event, and then perhaps draw a line between that location and the location of the next motion event. This is what painting programs normally do.
Besides drawing graphics on a window, we often want to draw text. Text strings have two major properties - the characters to be drawn, and the font with which they are drawn. In order to draw text we need to first request the X server to load a font. We then assign the font to a GC, and finally we draw the text in a window, using the GC.
In order to support flexible fonts, a font structure is defined, of type
XFontStruct
. This structure is used to contain information
about a font, and is passed to several functions that handle fonts selection
and text drawing.
As a first step to draw text, we use a font loading function, such as
XLoadQueryFont()
. This function asks the X server to load
data that defines a font with a given name. If the font is found, it is loaded
by the server, and an XFontStruct
pointer is returned. If the
font is not found, or the loading operation fails, a NULL value is returned.
Each font may have two names. One is a long string, that specifies the full
properties of the font (font family, font size,
italic/bold/underline attributes, etc.). The other is a short nickname for the
font, configured for the specific X server. As an example, we will try
to load a "*-helvetica-*-12-*" font (the '*' characters work the same as
wildcard characters in a shell):
/* this pointer will hold the returned font structure. */
XFontStruct* font_info;
/* try to load the given font. */
char* font_name = "*-helvetica-*-12-*";
font_info = XLoadQueryFont(display, fond_name);
if (!font_info) {
fprintf(stderr, "XLoadQueryFont: failed loading font '%s'\n", font_name);
}
After we load the font, we need to assign it to a GC. assuming that 'gc' is a variable of type GC that's already initialized, here is how this is done:
XSetFont(display, gc, font_info->fid);
The 'fid' field in an XFontStruct
is an identified used to
identify the loaded font in various requests.
Once we got our wanted font loaded and assigned to our GC, we can draw text
in our window, using a function such as XDrawString()
. This
function will draw a given text string at a given location on the window.
The location would be that of the lower-left corner of the drawn text string.
Here is how it is used:
/* assume that win_width contains the width of our window, win_height */
/* contains the height of our window, and 'win' is the handle of our window. */
/* some temporary variables used for the drawing. */
int x, y;
/* draw a "hello world" string on the top-left side of our window. */
x = 0;
y = 0;
XDrawString(display, win, gc, x, y, "hello world", strlen("hello world"));
/* draw a "middle of the road" string in the middle of our window. */
char* text_string = "middle of the road";
/* find the width, in pixels, of the text that will be drawn using */
/* the given font. */
int string_width = XTextWidth(font_info, text_string, strlen(text_string));
/* find the height of the characters drawn using this font. */
int fond_height = font_info->ascent + font_info->descent;
x = (win_width - string_width) / 2;
y = (win_width - font_height) / 2;
XDrawString(display, win, gc, x, y, "hello world", strlen("hello world"));
XTextWidth()
function is used to "predict" the width
of a given text string, as it will be drawn using a given font. This
may be used to determine where to draw the left end of the string
so it will look like it's occupying the middle of the window, for example.
[LUPG Home] [Tutorials] [Related Material] [Essays] [Project Ideas] [Send Comments]
This document is copyright (c) 1999-2002 by guy keren.
The material in this document is provided AS IS, without any
expressed or implied warranty, or claim of fitness for a
particular purpose. Neither the author nor any contributers shell
be liable for any damages incured directly or indirectly by using
the material contained in this document.
permission to copy this document (electronically or on paper, for
personal or organization internal use) or publish it on-line is
hereby granted, provided that the document is copied as-is, this
copyright notice is preserved, and a link to the original document
is written in the document's body, or in the page linking to the
copy of this document.
Permission to make translations of this document is also granted,
under these terms - assuming the translation preserves the meaning
of the text, the copyright notice is preserved as-is, and a link
to the original document is written in the document's body, or in
the page linking to the copy of this document.
For any questions about the document and its license, please
contact the author.