2

Préliminaires

 

 

 

2.1. Le lancement du serveur

                  La commande shell permettant de lancer le serveur est la commande xinit. La syntaxe de cette commande est la suivante :

xinit  [ [client] options ]     [ -- [server] [display] options ]

 

                  Cette commande lance deux processus. Un serveur X sur un certain dispositif d’affichage appelé display avec un certain nombre d’options (partie optionnelle de la commande qui figure après les deux tirets) et un premier client (première partie de la commande). Quand le premier client meurt, il provoque la terminaison du serveur. Cependant, sur de nombreux sites, l’envoi de cette commande est opaque à l’utilisateur qui exécute une commande différente écrite en shell — laquelle réalise un appel à xinit effectué avec un certain nombre d'options tenant compte de paramètres locaux[1].

 

Le fichier .xinitrc

 

                  Envoyée sans argument, xinit utilise un certain nombre de fichiers par défaut. Pour le premier client, xinit regarde s’il existe dans le répertoire principal de l’utilisateur un fichier appelé .xinitrc. Si ce fichier n’existe pas, xinit lance par défaut la commande xterm (l’émulateur de terminal) avec les options suivantes :

 

xterm -geometry +1+1 -n login -display unix:0

 

                 Si le fichier $HOME/.xinitrc existe, xinit lance un processus shell exécutant ce fichier. Le fichier .xinitrc permet donc à l’utilisateur de reconfigurer le serveur après lancement et de lancer un ou plusieurs clients en arrière‑plan. Ainsi, les lignes suivantes placées dans le fichier de démarrage $HOME/.xinitrc seront exécutées par xinit après lancement d’un serveur X sur l’écran[2] :

 

# pour reconfigurer le chemin d’accès des fontes et la vitesse de la souris

xset +fp /usr/local/lib/X11/fonts/X11R2  m 5 1

xset fp rehash

# on lance maintenant quelques clients en arrière-plan

xterm -fn 10x20 -fb 10x20 -sb &

xclock -geometry 150x100+700+5 -rv &

# et on exécute  le window manager twm

exec twm

 

                  Ici, on va d'abord exécuter deux appels à la commande xset, ensuite lancer un client, xterm, qui émule un terminal dans une fenêtre. (C'est depuis cette fenêtre que l'on pourra alors lancer d'autres commandes unix et d'autres programmes.) On lance ensuite une autre commande, xclock, en arrière-plan pour avoir l'heure de façon permanente tout en gardant la main. Puis on lance un exec du window manager twm. Pour quitter le serveur, il suffira alors de quitter twm car le shell .xinitrc sera alors terminé[3].

 

 

 

2.2. La compilation du programme

                  Pour utiliser la plupart des fonctions de la librairie, il faut inclure les fichiers suivants :

 

.h;.h;

#include <X11/Xlib.h>

#include <X11/Xutil.h>

#include <X11/keysym.h>

#include <X11/Xresource.h>

 

.h;.h;

                  Ces fichiers définissent des structures de données, des macros ou des symboles, et déclarent les types des fonctions[4]. Il faut toujours au moins inclure le fichier <X11/Xlib.h> qui contient la déclaration de la plupart des structures et fonctions. Le fichier <X11/Xutil.h> contient la déclaration de constantes utilisées par la librairie. On aura donc aussi fréquemment besoin de ce fichier (par exemple, pour analyser des constantes de retours).

 

                  Par contre, le fichier <X11/keysym.h> sert à la définition des noms symboliques des touches du clavier. Il n’est pas nécessaire de l’inclure si l’application n’utilise pas le clavier. De même le fichier  <X11/Xresource.h> ne sera inclus que pour les fonctions utilisant le gestionnaire de ressources.

.h;.h;

                  D'autres fichiers pourront également être inclus pour des fonctions particulières et seront indiqués au moment opportun. Il est par exemple recommandé d'inclure le fichier <X11/Xos.h> pour utiliser des appels systèmes, manipulations de chaînes ou autres. De même, on inclura <X11/Xatom.h> pour utiliser les propriétés prédéfinies permettant de communiquer entre clients.

 

                  Ces fichiers étant inclus en tête du fichier de programme fichier.c, on lancera simplement la commande suivante pour compiler :

 

% cc -o executable fichier.c -lX11

 

                  Si l’on veut pouvoir utiliser un débogueur symbolique comme dbx, on compilera avec l’option -g avant de lancer le débogueur.

 

% cc -g -o executable fichier.c -lX11

% xdbx executable

 

      xdbx est la version graphique[5] du débogueur symbolique unix dbx. C'est un outil précieux qui permet d’éviter les fastidieux printf dans lesquels se perdent en général les programmeurs.

 

 

2.3. Le choix des noms et la présentation

                  Toutes les fonctions de la librairie commencent par un X majuscule ainsi que la plupart des types de structures. (Il y a cependant quelques exceptions pour des types comme Depth, Display, GC, et Visual.) Les noms de macros ne commencent normalement pas par X pour être facilement distingués des fonctions. Ainsi DefaultRootWindow est une macro et non pas une fonction. Quoi qu’il en soit, tous ces noms commencent par une majuscule et sont ensuite constitués de minuscules, les majuscules ne servant qu'à séparer les mots. C'est cette même convention qui est utilisée pour la définition des symboles[6].

                  Par contre, dans toutes les structures, les noms des champs figurent en minuscules et les mots composés sont séparés par des caractères soulignés (ex: le champ override_redirect de la structure XSetWindowAttributes).

 

En conséquence, pour éviter toute confusion avec des fonctions ou symboles de la librairie, un programmeur utilisant X-Window doit :

                  • Définir les symboles en majuscules

                  • Définir les variables en minuscules

                  • Ne doit pas définir de terme commençant par un X majuscule, voire éviter de mettre la première lettre de chaque mot en capital dans les noms de variables ou de fonctions (à cause des macros et des symboles X).

                  On peut par contre toujours séparer les noms des mots composés par des caractères soulignés puisque seuls les champs de structures ont cette forme dans la librairie.

 

                  Cependant, l’alternance majuscule/minuscule est très agréable et plus concise que les caractères soulignés pour séparer les mots. En outre, pour une programmation claire en C, il est parfois recommandé de suivre la convention selon laquelle les variables globales commencent par une majuscule et les locales par une minuscule.  Malheureusement, ces usages entrent en conflit avec les choix de la librairie et l'on essaiera dans la mesure du possible de respecter les règles que nous avons formulées ci-dessus[7].

                  Concernant ce manuel, nous avons décidé d'utiliser des noms conventionnels pour faciliter la compréhension des types des fonctions dans les programmes. Ainsi, une variable nommée dpy aura nécessairement le type Display *, de même une variable de nom pixmap, aura le type Pixmap, etc. Nous utiliserons souvent par la suite les déclarations implicites suivantes :

 

Display*       display, dpy;

Window         win, root, window;

Drawable       draw, d;

Screen *       screen_ptr;

int      screen_num, scr_num;

Pixmap         pixmap;

GC             gc;

XContext       context, xcontext;

XEvent         event, ev;

Colormap       cmap;

char *         string;

unsigned int   width, height;

 

                  Par ailleurs, on ne saurait trop recommander de choisir le nom des variables et des symboles de sorte que leur usage dans le programme soit immédiatement compréhensible. Cela facilite la maintenance et augmente la fiabilité des programmes. En particulier, le choix des noms de symboles est important. Pensez aussi à utiliser des noms comme bidon, vide, nil, etc., pour indiquer que le contenu des variables ainsi nommées n'est en réalité pas utilisé. Ainsi, dans

 

Window   fen_bidon;

int largeur, hauteur,

         nil;

   ...

XGetGeometry (dpy, win, &fen_bidon, &nil, &nil,

                    &largeur, &hauteur, &nil, &nil);

 

il transparaît clairement que la procédure XGetGeometry, (qui permet de récupérer la taille et la position d'une fenêtre), n'est utilisée ici que pour récupérer la largeur et la hauteur de la fenêtre.

 

                  Dernier point concernant la présentation des programmes. Il est très important de bien indenter les lignes et en particulier d’aller à la ligne suivante pour mettre en relief les arguments des fonctions. En effet, beaucoup de fonctions de la librairie prennent un grand nombre d’arguments. De surcroît, certains arguments sont une combinaison de masques et prennent eux-même une grande place. Il ne faut donc pas hésiter à écrire un même argument sur plusieurs lignes si cela clarifie le texte du programme. En C en effet, les caractères blanc, tab et retour chariot sont ignorés du compilateur (sauf dans les chaînes[8]) et ne provoquent donc pas d’erreur de compilation. Ainsi, pour sélectionner les différents types d'événements intéressant une fenêtre, on écrira volontiers leurs masques de sélection sur plusieurs lignes :

XSelectInput (dpy, window, ButtonPressMask

                           | ButtonReleaseMask

                           | KeyPressedMask

                           | KeyReleaseMask

                           | PointerMotionMask

                           | StructureNotifyMask);

 

 

2.4. Connexion au serveur

                  Une fois le serveur lancé sur le terminal[9], les applications qui le souhaitent peuvent ouvrir une connexion à ce serveur. La connexion au serveur est réalisée en début de programme grâce à un appel à la fonction

 

Display *  XOpenDisplay (string)

 

                  Pratiquement toutes les requêtes de la librairie prendront ensuite en argument la variable (de type Display*) retournée par cette fonction. XOpenDisplay prend en argument une chaîne de caractères permettant d'identifier symboliquement un terminal, c'est-à-dire l'ensemble des éléments écran(s), clavier et souris, sur lesquels porteront la communication avec le serveur. Si un serveur X était effectivement actif sur ce terminal, la fonction retourne alors un pointeur sur une structure de type Display permettant d'identifier la connexion au serveur et un certain nombre d'éléments caractéristiques.

 

                  On peut appeler XOpenDisplay avec une chaîne de caractères comportant trois champs de la forme "machine:serveur.écran".  Cette chaîne pourra être lue en option au lancement du programme. Ainsi on peut lancer le programme émulateur de terminal xterm avec la commande

 

% xterm -display sun2:0.0

 

                  Le programme xterm effectuera alors un appel à  XOpenDisplay avec la chaîne "sun2:0.0" ce qui permettra d'afficher les résultats de l'exécution sur la console (serveur 0, écran 0) de la machine sun2. Certaines machines peuvent en effet avoir plusieurs terminaux graphiques qui leur soient connectés et c’est la raison pour laquelle on doit préciser le numéro du serveur[10].

 

                  On peut également appeler XOpenDisplay avec une chaîne vide

 

Display *dpy;

dpy = XOpenDisplay(NULL);

 

                  Par convention, la fonction consulte alors la variable d'environnement DISPLAY pour y lire une valeur. Il suffit donc d'affecter préalablement cette variable au niveau du shell avant de lancer l'exécution du programme. Par exemple, sous csh, on pourra indiquer :

 

% setenv DISPLAY sun3:0.0

 

                  On peut également lui affecter la valeur "unix:0.0" ou appeler directement XOpenDisplay avec cette chaîne. Le mot unix est alors interprété comme désignant la machine (unix) sur laquelle on a lancé le programme.

 

                  Pour imprimer un message d'erreur quand le dispositif d'affichage indiqué par l'utilisateur n'existe pas, on peut utiliser la fonction

 

char * XDisplayName (string)

char * string;

 

                  L'argument string contiendra bien entendu la même chaîne que celle passée en argument à XOpenDisplay. Si string est NULL, cette fonction retourne la chaîne que XOpenDisplay tente d'utiliser quand on l'appelle avec la chaîne vide.

 

                  Un programme ne fera en général qu'un seul appel à XOpenDisplay. On peut bien entendu réaliser des connexions à plusieurs serveurs s'occupant de dispositifs d'affichage différents (par exemple, pour réaliser un programme de bataille navale entre deux joueurs connectés à des machines différentes), mais si l'on appelle deux fois XOpenDisplay pour le même serveur X, le résultat est imprévisible et vide de sens.

 

 

Exemple de connexion au serveur

 

                  Un début typique de programme prenant en compte l’option -d (pour display) sera donc quelque chose comme :

 

#include <stdio.h>

#include <X11/Xlib.h>

#include <X11/Xutil.h>

 

char*          nom_de_station = NULL;

Display*       dpy;

 

/*

 * Une fonction pour imprimer des erreurs

 * et quitter le programme

 */

 

void erreur (message, a1, a2, a3) char* message;

{

   printf (message, a1, a2, a3);

   exit (1);

}

 

/*

 * La boucle principale

 */

 

main (argc, argv) char **argv;

{

   /*

    * Regarder si on a passé un argument au programme,

    * et si oui modifier l'argument nom_de_station

    */

  

   if (argc == 3 && !strcmp (argv [1], "-d"))

         nom_de_station = argv [2];

 

   /*

    * Connexion au serveur “nom_de_station”

    */

  

   dpy   = XOpenDisplay (nom_de_station);

   if (dpy == NULL)

         erreur ("%s: connexion %s impossible \n",

                     argv[0],                                          XDisplayName(nom_de_station));

   else printf("%s: connexion reussie\n",

                     argv[0]);

 

                 

La déconnexion sera réalisée au moyen de la requête XCloseDisplay(dpy). On veillera cependant à ce que cette requête soit bien la dernière du programme, car une référence ultérieure à la variable dpy, non correctement instanciée, provoquerait une erreur[11].

 

 

2.5. Initialisations diverses

                  Pour obtenir du serveur les valeurs nécessaires à la définition des fenêtres, on utilise généralement des macro-fonctions. On a vu que la connexion au serveur consistait en l'appel à la fonction XOpenDisplay, qui retournait un pointeur sur une structure de type Display, opaque à l'utilisateur. De nombreuses macros utiles sont de simples accès aux champs de la structure Display qui caractérise le terminal pour lequel on a ouvert une connexion. Un début typique de programme en noir et blanc sera par exemple :

 

int            screen_num;

Display* dpy; 

Window         root;

unsigned long foreground, background;

GC       gc;  

 

dpy      =     XOpenDisplay(NULL);

  

root           =     DefaultRootWindow(dpy);

screen_num     =     DefaultScreen(dpy)

background     =     BlackPixel(dpy,screen_num)

foreground     =     WhitePixel(dpy,screen_num)

gc       =     DefaultGC(dpy,screen_num);

 

                  Les fenêtres étant organisées en arbre à partir de la racine (root) constituant le fond d'écran, on a toujours besoin de connaître l'identificateur de cette fenêtre pour créer une première fenêtre. La macro DefaultRootWindow permet d'obtenir la racine (fenêtre fond d’écran) à partir de la variable dpy caractérisant la connexion. Il y a en fait une deuxième macro RootWindow qui retourne la racine en fonction du numéro d’écran car certains dispositifs d’affichage possèdent plusieurs écrans. Le numéro d'écran par défaut est retourné par la macro DefaultScreen.

 

                  De même, les valeurs des pixels[12] blanc et noir dépendent des machines. On les obtiendra à partir de dpy avec les macros BlackPixel et WhitePixel. On verra dans quelques chapitres qu'une variable de type GC (pour graphic context) est également nécessaire dès que l'on veut dessiner dans une fenêtre. La macro DefaultGC permettra de récupérer un contexte graphique par défaut.

 

                  Signalons aussi la présence de macros fournissant la taille de l'écran, ses capacités de traitement couleur, le contexte graphique, la palette de couleurs par défaut, etc., à partir du pointeur sur la structure Display (récupéré avec XOpenDisplay) et du numéro de l'écran (récupéré par DefaultScreen) :

     

DisplayWidth ;(dpy, screen_num) 

DisplayHeight (dpy, screen_num)

DefaultVisual  (dpy, screen_num)

DefaultDepth (dpy, screen_num)  

DefaultColormap (dpy, screen_num)

DefaultGC (dpy, screen_num)

RootWindow (dpy, screen_num)

 

                  Cette liste n'est pas exhaustive et nous recommandons au lecteur de consulter les macros associées au display s'il cherche à récupérer des informations sur l'écran, le serveur ou les capacités graphiques de la machine. Ces définitions se trouvent dans le fichier <X11/Xlib.h> et sont introduites dans le premier chapitre de la documentation MIT ([GET 91]).

                  D'autres macros permettent encore d'obtenir des renseignements à partir d'un pointeur sur le display et d'un pointeur sur l’écran. Pour obtenir un pointeur sur l'écran, on utilisera d'abord la fonction

 

Screen * XScreenOfDisplay (dpy,screen_num)

 

et on pourra alors accéder à toute une série de macros fournissant d'autres caractéristiques du serveur ou de l'écran, comme par exemple :

 

DoesBackingStore (screen_ptr)

DoesSaveUnder (screen_ptr)

DefaultColormapOfScreen (screen_ptr)

 

 On prendra garde à ne pas confondre les deux types de dénotation d'écran, le type pointeur, Screen *screen_pt, et le type entier, int screen_num.

 

 

 

 

       Les fonctions importantes

 

          Display *  XOpenDisplay (string)

 

          char * XDisplayName (dpy_name)

 

          Window DefaultRootWindow (dpy)

          int DefaultScreen (dpy)

          unsigned long BlackPixel (dpy,screen_num)

          unsigned long WhitePixel (dpy,screen_num)

   GC DefaultGC (dpy, screen_num)

          int DisplayWidth (dpy, screen_num)

          int DisplayHeight (dpy, screen_num)

           

          On notera aussi l'exemple de connexion au serveur donné section 2.4.

 

 

 



[1] Ces options permettent en particulier d’indiquer des chemins d’accès à des répertoires.

[2] Attention, par défaut xinit lance un serveur sur la console de la machine. Il faut donc se trouver sur la console pour lancer xinit, sinon, on risque de créer une mauvaise surprise au collègue qui  travaille sur la console.

[3] Du fait de la commande exec, le processus en cours disparaît au profit de l'exécution de twm. Or le processus en cours, i.e. le shell-script .xinitrc est le premier client du serveur et sa fin entraîne aussi la terminaison du serveur. C'est donc maintenant l'interruption de ce nouveau processus (twm) qui provoquera une terminaison du serveur. Il ne faut donc pas  tuer twm  (à moins que l'on ne souhaite quitter le serveur). Si l'on veut pouvoir tuer le window manager il faudra inverser l’ordre de lancement des commandes xterm et twm :

                  twm &

       exec xterm -fn 10x20 -fb 10x20 -sb -name console

On quittera alors le serveur en quittant la pseudo-console xterm.

 

      On pourrait également lancer tous les clients en arrière-plan et terminer le shell .xinitrc par la commande wait. Mais on ne pourrait plus alors quitter aisément le serveur en quittant l'un des clients.

[4] La plupart des types utilisés ainsi par le programmeur lui sont en fait opaques. Par exemple, le type Window n’est jamais adressé directement et l’utilisateur ignore les champs de la structure qui correspond réellement aux fenêtres. L’utilisateur n’accède aux fenêtres qu’au travers des fonctions de la librairie. Ainsi, pour affecter les attributs des fenêtres, il faut passer par l’intermédiaire d'une structure : la structure XSetWindowAttributes.

[5] Cette commande ne fait pas partie des commandes standards mais c’est une contribution au système facile à se procurer.

[6] Citons à titre d'exemple, les masques CWBackPixel et ExposureMask. Seuls les atomes font exception à la règle, mais ils commencent tous par XA_ pour être aisément distingués des constantes définies par l’utilisateur.

[7] Un compromis est néanmoins possible : si vous souhaitez utiliser l’alternance majuscule/minuscule, vous pouvez redoubler systématiquement la première majuscule, ce qui n’est pas très beau, mais reste néanmoins lisible, ou bien choisir un préfixe qui débutera toutes vos fonctions. Le choix de noms français diminue en outre le risque de confusion. On peut donc s'autoriser de temps à autre une entorse à la règle en prenant soin d'utiliser dans ce cas un mot français bien choisi, par exemple

                  Window FenetrePrincipale, BoiteDeDialogue;

[8] Si l’on souhaite aller à la ligne à l’intérieur d’une chaîne de caractères, on utilise en C le meta caractère  \ qui permet d’indiquer au préprocesseur qu’il faut ignorer le caractère de fin de ligne qui le suit. Ainsi par exemple, on écrira:

                  printf("Ceci est une très longue chaîne qui ne sera pas \

coupée a l'impression");

[9] Se renseigner auprès de l'administrateur du site sur les chemins d'accès à indiquer dans la variable d'environnement PATH (habituellement /usr/bin/X11) et la commande à lancer si xinit ne marchait pas.

[10] Il y a un serveur par ensemble clavier-souris. En outre, dans certaines configurations, un même serveur peut avoir plusieurs écrans (reliés cependant à la même souris).

[11] On a effectivement toute chance de référencer encore la variable dpy car elle est passée en premier argument à la plupart des requêtes.

[12] Pixel = numéro (ou indice) de couleur dans une table codant les valeurs d'intensité associées à cette couleur.