Programmation Xlib

Le but de cet article est d'initier le lecteur à la programmation sous le système graphique X11. Dans ce document, nous allons décrire un exemple simple utilisant la librairie Xlib.

Cet article n'est pas un cours de programmation; son but est de permettre au lecteur d'écrire son premier programme en Xlib, tout en introduisant les concepts principaux de la programmation X. Il suppose une bonne connaissance du langage C.

1. Rappels sur X11

1.1 Principes de base

X11 est le système graphique le plus populaire du système d'exploitation UNIX. Sa large distribution, le fait qu'ils soit libre de tous droits de distribution et surtout ses qualités techniques exceptionnelles en ont fait un standard de l'industrie du logiciel. Ses caractéristiques principales sont :

- L'utilisation d'une architecture client/serveur et cela dans une totale hétérogénéité (le serveur et le client peuvent fonctionner sur des architectures totalement différentes). Le dialogue entre le serveur et les clients se fait conformément au protocole X, ce qui permet d'assurer la transparence du dialogue.

- La portabilité des librairies et des applications X11 (le même code tourne sur une grosse station de travail ou bien sur un petit PC sous Linux). En dépit des avantages de X11, il serait faux de croire que le développement d'une application X soit une chose simple. Les concepts utilisés sont complexes (programmation orientée objet, gestion d'événements...), le nombre de fonctions à connaître est important, la documentation titanesque et parfois difficile à aborder...

1.2 Structure des librairies

Les librairies constituant X sont organisées sous forme de couches en partant du plus bas niveau (la couche protocole) au plus haut (les toolkits). Le schéma ci-contre donne l'organisation de ces couches.

 
Toolkits (Xaw, Xm)
Xt  
Xlib
Protocole X

Comme nous le voyons sur le schéma, la couche utilisable la plus basse (en l'occurrence, la Xlib) est accessible à partir des couches de plus haut niveau (manipulant des classes et des objets : boutons, menus, etc.).

2. Pourquoi connaître la Xlib ?

Si l'on fait une analogie entre X et les langages de programmation, on peut considérer la Xlib comme une sorte de langage assembleur et les couches supérieures (Xt, toolkits - Xaw ou Motif, GTK, Qt) comme des langages évolués. On peut donc se demander s'il est encore nécessaire de connaître aujourd'hui un langage de bas niveau alors que des outils puissants permettent de construire une application directement à la souris (pour ne citer que certains produits disponibles sous Linux comme par exemple BX de ICS ou IlogViews de ILOG). Cependant, je pense qu'il est difficilement envisageable de réaliser (et de maintenir) une application complexe sous X sans avoir une connaissance des couches inférieures, y compris la Xlib. En plus de cela, la Xlib offre des performances à l'exécution inégalées (et inégalables) par un toolkit (c'est le propre d'un assembleur).

3. Structure d'un programme X

Un programme X peut grossièrement se diviser en 3 parties :

- l'ouverture d'une connexion à un serveur X (serveur local ou bien terminal) ;

- les initialisations des fenêtres initiales (cas de la Xlib) ou des objets initiaux (cas d'un toolkit) ;

- l'attente d'événements (souris, clavier ou autres...) dans une boucle infinie dont on ne sortira qu'à la fin du programme.

La réception des événements déclenchera les actions décrites à la deuxième partie (initialisation).

4. Les concepts de base

4.1 Le display

Le display définit la connexion de l'application à un serveur X. Une fois initialisée, la valeur du display sera utilisée dans tous les appels aux fonctions X. Le display est calculé grâce à la fonction :

Display *XOpenDisplay (nom_du_display)

char *nom_du_display;

Dans le cas où le paramètre est égal au pointeur NULL, la fonction lit la valeur de la variable d'environnement DISPLAY. Dans le cas où l'on passe un nom de display, il devra respecter la même syntaxe que la variable DISPLAY, soit en général :

nom_du_display:0.0 pour un terminal X (exemple: Xpierre:0)

:0.0 pour un serveur local

4.2 L'écran (screen)

Pour un display donné, on peut avoir plusieurs screens (unités d'affichage en fait). En pratique, la valeur de l'écran est toujours de 0.

4.3 Les fenêtres

La fenêtre est un des concepts les plus importants sous X. Tout objet affiché à l'écran (quelle que soit sa complexité) est décomposé en fenêtres. Les fenêtres sont en général rectangulaires mais la release 4 (X11R4) a introduit les fenêtres rondes, qui sont cependant peu utilisées (voir les clients xeyes ou xman). Les caractéristiques principales d'une fenêtres sont :

- la fenêtre parent,

- la position par rapport au parent (x, y en pixels)

- les dimensions (largeur/hauteur en pixels)

- la couleur du fond

- la couleur du bord

- l'épaisseur du bord

Le type X correspondant à la fenêtre est le type Window. Pour créer simplement une fenêtre, on pourra utiliser la fonction :

Window XCreateSimpleWindow (display, parent, x, y, width, height,

border_width, border_color, background)

Display *display;

Window parent;

int x, y;

unsigned int width, height, border_width;

unsigned long border_color, background;

Pour afficher la fenêtre, on utilisera la fonction :

XMapWindow (display, w)

Display *display;

Window w;

Et pour l'effacer, on utilisera :

XUnmapWindow (display, w)

Display *display;

Window w;

Le système des fenêtres a une structure arborescente avec une fenêtre particulière qui est la mère de toutes les autres fenêtres : la Root Window (fond de l'écran).

4.4 Les Pixmaps

Un Pixmap est une fenêtre cachée permettant de faire des tracés en arrière-plan (par exemple, sauvegarde d'une partie d'écran pour un ré-affichage ultérieurement). Ils ne seront pas utilisés dans l'exemple qui suit.

4.5 Les événements

Les événements sont à la base du fonctionnement d'un programme sous X (on parle de programmation événementielle). Les événements peuvent être d'origine externe (clic/déplacement de souris, touche du clavier...) ou bien interne (dégagement d'une partie de fenêtre obscurcie).

Les événements sont identifiés grâce aux types d'événement. En voici quelques exemples :

ButtonPress appui sur le bouton de la souris

ButtonRelease relâchement du bouton souris

KeyPress touche appuyée

KeyRelease touche relâchée

Expose dégagement d'une fenêtre obscurcie (ou bien

premier affichage de la fenêtre)

Les types d'événements sont rassemblés en masques d'événement ou event mask. Par exemple, le masque ButtonPressMask correspond à l'événement ButtonPress, ExposureMask correspond à l'événement Expose.

Les événements sont reçus par les fenêtres à condition que l'on ait indiqué l'intérêt de la fenêtre pour cet événement. Ceci se fait avec la fonction :

XSelectInput(display, w, event_mask)

Display *display;

Window w;

long event_mask;

Le paramètre event_mask est un masque indiquant la liste des événements intéressant la fenêtre

(exemple : ButtonPressMask | KeyPressMask). Si un événement reçu n'intéresse pas la fenêtre, il est en général passé à la fenêtre père de la fenêtre courante.

Comme nous l'avons déjà indiqué, un programme X a une structure de boucle infinie de traitement des événements reçus. La lecture des événements est souvent bloquante; elle est réalisée par la fonction :

XNextEvent (display, event_return)

Display *display;

XEvent *event_return;

Le type XEvent correspond à une union C pouvant contenir tous les événements possibles. La sélection se fait sur le premier champ de l'union qui correspond au type de l'événement :

typedef union _XEvent {

int type;

XAnyEvent xany;

XKeyEvent xkey;

XButtonEvent xbutton;

XMotionEvent xmotion;

XCrossingEvent xcrossing;

XFocusChangeEvent xfocus;

...

};

Pour une variable xev du type XEvent, l'accès aux différents champs, suivant la valeur du type, se fait donc par xev.xbutton (pour un ButtonPress ou un ButtonRelease), xev.xexpose (pour Expose), etc.

4.6 Le contexte graphique (GC)

Le contexte graphique ou graphic context (GC) permet de stocker dans une structure tous les attributs d'affichage (couleur de fond, couleur de dessin, fonte, type d'affichages - Normal, XoR...). Une variable de type GC est toujours passée en paramètre des fonctions de tracé. Par exemple, pour afficher un texte dans une fenêtre, on utilisera la fonction :

XDrawString(display, d, gc, x, y, string, length)

Display *display;

Drawable d;

GC gc;

int x, y;

char *string;

int length;

Le type Drawable peut représenter une Window ou bien un Pixmap (en fait, un type permettant le dessin, d'où le nom de Drawable)

5. Un exemple simple commenté

Ce petit exemple permet d'afficher la chaîne Hello, world dans une fenêtre X :

/*

* xsimple : Affiche une chaine dans une fenetre X...

*/

#include <stdio.h>

#include <X11/Xlib.h>

En premier lieu, on inclut le fichier stdio.h (ça peut toujours servir) et surtout le fichier X11/Xlib.h, qui contient les définitions des types et constantes utilisées par la Xlib.

GC gc;

Display *display;

int screen;

Window win, root;

unsigned long white_pixel, black_pixel;

On déclare les variables globales utilisées par le programme (contexte graphique, display, screen, fenêtres et couleurs noir et blanc).

/*

* fonction associee a l'evenement EXPOSE

*/

void expose ()

{

XDrawString (display, win, gc, 10, 30, "Hello, world !", 14);

}

Cette fonction est appelée lorsque la fenêtre win reçoit l'événement Expose. Ceci arrive dans deux cas seulement :

- lorsque la fenêtre est affichée à l'écran par un XMapWindow

- lorsqu'une portion de la fenêtre précédemment couverte par une autre redevient visible (et que le serveur X ne fait pas de gestion interne des recouvrement - ou backing-store)

ATTENTION : on ne peut pas écrire dans une fenêtre AVANT qu'elle n'ait reçu son premier événement Expose.

Lors de la réception de l'évènement Expose, on affiche la chaîne Hello, world aux coordonnées (10, 30) de la fenêtre win.

/*

* programme principal

*/

main(ac, av)

int ac;

char **av;

{

char *dpyn = NULL;

/* selection du display en ligne */

if (ac == 3) {

if (strcmp (&av[1][1], "display") == 0) {

dpyn = av[2];

}

else {

fprintf (stderr, "Usage: xsimple [-display display_name]\n");

exit (1);

}

}

if ((display = XOpenDisplay (dpyn)) == NULL) {

fprintf (stderr, "Can't open Display\n");

exit (1);

}

Dans le programme principal, on ouvre tout d'abord une connexion avec un serveur X (donné par le contenu de la variable d'environnement DISPLAY). On peut également passer la valeur du display en paramètre sur la ligne de commande (par convention, c'est l'option -display). Il suffit ensuite d'appeler la fonction XopenDisplay, qui renvoie un pointeur de Display, s'il existe.

gc = DefaultGC (display, screen);

screen = DefaultScreen (display);

root = RootWindow (display, screen);

white_pixel = WhitePixel (display, screen);

black_pixel = BlackPixel (display, screen);

On récupère les valeurs par défaut des différentes variables (contexte graphique, screen...). Pour cela, on utilise des macros-instructions définies dans X11/Xlib.h .

win = XCreateSimpleWindow (display, root, 0, 0, 100, 90, 2, black_pixel, white_pixel);

XSelectInput (display, win, ExposureMask);

XStoreName (display, win, "xsimple");

XMapWindow (display, win);

On crée ensuite la fenêtre win par la fonction XCreateSimpleWindow. Cette fenêtre a pour parent la root-window, ses dimensions sont 100 sur 90 pixels, 2 pixels d'épaisseur pour le bord, fond blanc, bord noir. L'appel à XSelectInput indique que la fenêtre ne s'intéresse qu'à l'événement Expose (ExposureMask).

La fonction XStoreName permet de nommer la fenêtre pour le window-manager (dans notre cas, le nom xsimple s'affichera dans le bandeau du window-manager).

Enfin, la fonction XMapWindow affiche la fenêtre à l'écran.

for (;;) {

XEvent ev;

char c;

XNextEvent (display, &ev);

switch (ev.type) {

case Expose :

expose ();

break;

default :

break;

}

}

}

Voici la partie active du programme. C'est une boucle infinie de lecture des événements X (appel à la fonction XNextEvent). Les événements sont ensuite traités suivant leur type (ev.type) dans un switch (dans notre cas, seul l'événement Expose est associé à une fonction).

Pour compiler ce programme sous l'environnement de développement X, on pourra utiliser le fichier Imakefile suivant :

LDLIBS = $(XLIB)

SimpleProgramTarget(xsimple)

avec la séquence suivante :

- xmkmf pour générer le Makefile à partir de l'Imakefile

- make depend pour générer les dépendances des fichiers à la fin du Makefile

- make all pour compiler le programme

Conclusions

La programmation en Xlib est loin d'être simple et il est peu concevable de réaliser aujourd'hui une application complète sans utiliser de toolkit (OSF-Motif, GTK, Qt) ou même d'outil de prototypage d'applications. La compréhension des mécanismes de base de la Xlib reste cependant une assurance pour la fiabilité des applications développées.

 

Pierre Ficheux (pficheux@com1.fr)


© Copyright 2000 Diamond Editions/Linux magazine France. - Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1or any later version published by the Free Software Foundation; A copy of the license is included in the section entitled "GNU Free Documentation License".