IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Étude et programmation du système X Window

Concepts et bibliothèques Xlib, Xt et Xm


précédentsommairesuivant

II. Programmation C de la Xlib

II-A. Display et fenêtres

La Xlib est une bibliothèque d'interface entre le programmeur et le protocole X11. Le protocole X est le seul langage compréhensible par le serveur X.

Cette bibliothèque est un ensemble de fonctions bas niveau simples (relativement) que le programmeur peut utiliser et dont les bibliothèques haut niveau (genre Xt et Motif) se servent.

Il est important de comprendre comment Xlib est conçue, pour pouvoir la programmer efficacement.

II-A-1. Environnement de programmation

II-A-1-a. Conventions concernant les fonctions Xlib

Toute fonction de la Xlib commence par la lettre X. On ne capitalise que les mots-clés. Exemple : XOpenDisplay(), XCopyColormapAndFree()

Certains appels comme DisplayPlanes() sont des macros et non pas des fonctions.

Toute fonction Xlib commence par l'argument display.

II-A-1-b. Compilation

Les programmes utilisant les fonctions de la Xlib doivent inclure :

 
Sélectionnez
#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>

Pour compiler un programme qui utilise les fonctions de la Xlib et faire l'édition de liens, on utilise les options suivantes :

 
Sélectionnez
~:> gcc main.c -I/usr/openwin/include

    -L/usr/openwin/lib/X11 -lX11
  • -I/usr/openwin/include : où le compilateur trouvera les includes X11 ;
  • -lX11 : on linke avec la bibliothèque X11 ;
  • -L/usr/openwin/lib/X11 : où le linker trouvera les bibliothèques X11.

II-A-2. Le display

Pour accéder à un display X11, il faut établir une connexion entre la machine locale et la machine distante (un socket Unix, de type TCP/IP).

 
Sélectionnez
Display * dpy;
char * display_name=":0";/* ou NULL */
dpy=XOpenDisplay(display_name);
..
/* ne pas oublier de le fermer ! */
XCloseDisplay(dpy);

Display * est un pointeur sur une structure dans la mémoire du client qui regroupe des informations sur les propriétés du display ouvert. Ces informations seront utilisées par la Xlib pour des vérifications sur la possibilité de certaines commandes avant leur envoi au display. Dans l'include Xlib.h est défini :

 
Sélectionnez
typedef struct _XDisplay
{
    XExtData *ext_data; /* hook for extension to hang data */
    struct _XFreeFuncs *free_funcs; /* internal free functions */
    int fd; /* Network socket. */
    int conn_checker; /* ugly thing used by _XEventsQueued */
    ..
    struct _XSQEvent *qfree; /* unallocated event queue elements */
    int (*savedsynchandler)(); /* user synchandler when Xlib usurps */
} Display;

La structure Display est générée au moment de la connexion par la fonction XOpenDisplay().

display_name peut être soit une chaîne spécifiant le nom du display, par exemple « foc.essi.fr:0.0 », ou bien NULL (0). Dans ce dernier cas, le nom du display sera celui contenu dans la variable d'environnement shell $DISPLAY.

Pour ouvrir une connexion sur un display local à partir d'une machine distante, il faut que le serveur local l'autorise :

 
Sélectionnez
local_host:> xhost +another_host

local_host:> rlogin another_host

Passwd:

another_host:> DISPLAY=local_host:0

another_host:> xclock...

La commande xhost et les autres commandes de sécurité de X seront détaillées dans un autre chapitre.

L'argument dpy sera utilisé dans toutes les requêtes X (en premier argument). Par exemple :

 
Sélectionnez
XDrawPoint(dpy, drawable, gc, x, y);

Il existe une série de macros concernant le display : plusieurs macros sont disponibles pour obtenir des informations sur le display, comme le nombre de couleurs, sa taille, l'ID de la root-window…

Le numéro de l'écran par défaut (souvent sollicité plus loin) peut être obtenu par :

 
Sélectionnez
DefaultScreen(display);

Exemples de macros :

 
Sélectionnez
int DisplayHeight(dpy, screen_number)
int DisplayWidth(dpy, screen_number)
int DisplayPlanes(dpy, screen_number)

II-A-3. Les fenêtres X

Une fenêtre est un espace graphique (pas forcément rectangulaire). À chaque fenêtre est affectée une structure descriptive stockée dans le serveur.

Les paramètres des fenêtres (taille, hauteur…) sont dans la mémoire du serveur(1). Du côté du programme client, on ne manipule que des identifieurs de fenêtres. C'est un nombre unique codé sur 32 bits (29 utilisés). Le type Window est donc un identifieur et en aucun cas un pointeur :

 
Sélectionnez
typedef unsigned long XID;
typedef XID Window;

Une fenêtre occupe environ 100 octets dans la mémoire du serveur : elle ne coûte rien ! Les applications courantes comportent plus d'une centaine de fenêtres (Xmh…).

Une fenêtre (la structure mémoire et l'identifieur) est détruite si la connexion du display sur laquelle elle est ouverte est rompue.

II-A-3-a. Caractéristiques des fenêtres
II-A-3-a-i. Fenêtres mères et coordonnées

L'écran X11 se compose d'une root-window qui est en fait le fond de 1. Il est très important de comprendre ceci.

L'écran, et qui contient à son tour d'autres fenêtres qui sont toutes ses filles.

Image non disponible
Figure 1 : système de coordonnées sous X.

Une fenêtre possède une fenêtre mère, affectée lors de sa création. Une fenêtre peut sortir des limites de sa fenêtre mère et ses coordonnées lui sont relatives (Figure 1). Comme les filles sont clippées par leur fenêtre mère, les fenêtres peuvent sortir de l'écran. Chaque fenêtre se comporte comme un « petit » écran. Une fenêtre peut être de n'importe quelle taille. La largeur, la hauteur et la position d'une fenêtre forment sa géométrie.

Les fenêtres peuvent se superposer. Ainsi une fenêtre peut en cacher une autre.

La taille d'une fenêtre est mesurée en pixels et n'inclut pas la largeur des bordures. Si la taille de la bordure est 0, elle est invisible.

Une fenêtre n'est pas sensible aux entrées (souris, clavier) si le pointeur de la souris ne la désigne pas(2).

II-A-3-a-ii. Les caractéristiques Depth et Visual Type

La profondeur (Depth) est le nombre de bits par pixel qui va être disponible pour représenter les couleurs que l'on pourra utiliser pour dessiner dans la fenêtre.

En annexe est présenté un éclaircissement sur le codage des couleurs dans les cartes vidéo.

Le Visual Type représente la manière dont le serveur va gérer les couleurs. Il existe en effet différents moyens d'accéder à l'écran. Peu de hardwares supportent de multiples Visuals. Nous y reviendrons.

II-A-3-a-iii. Classe d'une fenêtre

Une fenêtre possède une classe qui diffère selon qu'elle est capable ou non d'afficher des résultats.

Les classes possibles sont :

  • InputOutput : la fenêtre peut recevoir des entrées et afficher des résultats ;
  • InputOnly : la fenêtre ne peut que recevoir des entrées.

Exemple : des boutons-poussoirs.

II-A-3-a-iv. Attributs des fenêtres

Les attributs d'une fenêtre contrôlent de nombreux aspects concernant l'apparence d'une fenêtre ainsi que la manière dont elle va réagir aux différents événements.

Exemple d'attributs :

  • couleur du fond, de la bordure ;
  • à quels événements la fenêtre va-t-elle réagir ? ;
  • la fenêtre autorise-t-elle le window-manager à la déplacer, la retailler…
II-A-3-a-v. Hiérarchie.

Les fenêtres sont arrangées en arbre (Figure 2). Elles ont toutes un ancêtre sauf la root-window (r). La root-window est créée lors du lancement du serveur X.

Image non disponible
Figure 2 : hiérarchie des fenêtres X11.

Les filles directes de la root-window sont spéciales, ce sont des top-level.

Une fenêtre top-level est généralement la première créée par une application.

Le stacking order intervient à deux niveaux : il règle la priorité entre les fenêtres top-level (une application passe entièrement devant une autre).

Ensuite, il règle la priorité entre les filles des top-level.

II-A-3-b. Programmation des fenêtres

Il existe plusieurs moyens pour créer une fenêtre.

XCreateWindow() est utilisée pour créer une fenêtre quelconque X, les paramètres sont passés dans la structure XSetWindowAttributes (voir plus loin).

Exemple :

 
Sélectionnez
Window win_des;
XSetWindowAttributes xswa;
int winX,winY,winW,winH;
int border_width;
int screen;

/* largeur, hauteur de la fenêtre */
winW=600;
winH=600;

/* largeur de la bordure */
border_width = 3;

/* Position de la fenêtre par rapport à la taille de la RootWindow */
winX=500;
winY=400;
screen=DefaultScreen(dpy);

/* Couleur de la bordure et du background */
xswa.border_pixel = BlackPixel(dpy,DefaultScreen(dpy));
win_des = XCreateWindow(display, DefaultRootWindow(dpy),
    winX, winY, winW, winH, border_width,
    DefaultDepth(dpy,screen), InputOutput,
    DefaultVisual(dpy,screen),
    CWBorderPixel, &xswa);

/* Affichage de la fenêtre à l'écran */
XMapWindow(dpy,win_des);

La fonction XMapWindow() est utilisée pour afficher une fenêtre et tous ses enfants. Une fenêtre peut être retirée de l'écran sans être détruite avec XUnmapWindow().

Il existe une manière plus simple pour créer une fenêtre. L'exemple précédent illustre bien la « lourdeur » et la complexité de la programmation sous Xlib. Heureusement, pour la création de fenêtres la Xlib propose une fonction plus facile à utiliser.

La fonction XCreateSimpleWindow() permet de créer plus rapidement une fenêtre.

L'appel de XCreateSimpleWindow() au lieu de XCreateWindow() fait que la fenêtre créée hérite de tous les attributs de la fenêtre mère.

Exemple :

 
Sélectionnez
win_des= XCreateSimpleWindow(display,
    RootWindow(display, screen),
    x, y,
    width, height,
    border_width,
    BlackPixel(display,screen),
    WhitePixel(display,screen));

Les attributs de la fenêtre peuvent être modifiés après la création à l'aide de fonctions spécialisées telles XSelectInput(), XResizeWindow()

Détaillons les différents paramètres de la fonction XCreateWindow().

 
Sélectionnez
#include <X11/X11.h>

Display *display;
Window window, parent_window;
int x, y;
unsigned int width, height;
unsigned int border_width;
int depth;
unsigned int class;
Visual *visual;
unsigned long valuemask;
XSetWindowAttributes*attributes;

window = XCreateWindow(display, parent_window, x, y, width, height, border_width, depth, class,
    visual, valuemask, attributes);

On rappelle que le type Window est un identificateur de la fenêtre dans la mémoire du serveur. Le premier paramètre est le display, retourné par la fonction XOpenDisplay(). Le second paramètre est l'identificateur de la fenêtre mère. Il est facile d'obtenir l'identificateur de la RootWindow grâce à la macro DefaultRootWindow(dpy).

Les troisième et quatrième paramètres (x et y) donnent la position désirée du coin en haut à gauche de la fenêtre (en pixels). Nous employons le terme « désirée », car le Window Manager peut arbitrairement modifier la position de la fenêtre lors de sa création. Les deux paramètres suivants (width et height) correspondent à la taille de la fenêtre en pixels. Le paramètre suivant (border_width) indique l'épaisseur souhaitée du cadre autour de la fenêtre. Ici aussi, si la fenêtre possède comme mère la RootWindow, le Window Manager peut affecter à la bordure une valeur différente de celle spécifiée dans le code.

Le paramètre depth correspond au nombre de bit-planes disponibles sur le display. Cette valeur peut être obtenue à l'aide de la macro DefaultDepth(display, screen_number), ou de la constante CopyFromParent.

Le paramètre class est une constante pouvant prendre une des valeurs suivantes : InputOutput, InputOnly, ou CopyFromParent. La majeure partie des fenêtres que l'on va manipuler seront de la classe InputOutput.

Le paramètre visual sert à décrire un modèle abstrait du hardware graphique dont est équipée la machine sur laquelle l'application devra être exécutée. Nous étudierons plus en détail ce paramètre lors dans le chapitre consacré à la couleur. En attendant, ne prenons pas de risque et utilisons soit la macro DefaultVisual(display, screen_number).

Les deux paramètres suivants, valuemask et attributes sont très liés. La structure XSetWindowAttributes contient 15 champs conditionnant l'aspect et le comportement de la fenêtre. La variable valuemask est un masque de bits qui sert à indiquer quels champs parmi les 15 on va spécifier dans la variable contenant les attributs. Le couple structure/masque n'est qu'un moyen de factoriser le passage de paramètre.

Structure XSetWindowAttributes (tirée du manuel) :

 
Sélectionnez
typedef struct
{
    Pixmap background_pixmap; /* background, None, or
    ParentRelative */
    unsigned long background_pixel; /* background pixel */
    Pixmap border_pixmap; /* border of the window */
    unsigned long border_pixel; /* border pixel value */
    int bit_gravity; /* one of bit gravity values */
    int bit_gravity; /* one of bit gravity values */
    int win_gravity; /* one of the window gravity values */
    int backing_store; /* NotUseful, WhenMapped, Always */
    unsigned long backing_planes; /* planes to be preserved */
    unsigned long backing_pixel; /* value to use in restoring planes */
    Bool save_under; /* should bits under be saved? (popups) */
    long event_mask; /* set of events that should be saved */
    long do_not_propagate_mask; /* set of events not propagate */
    Bool override_redirect; /* boolean value for override_redirect */
    Colormap colormap; /* color map to be associated with window */
    Cursor cursor; /* cursor to be displayed (or None) */
} XSetWindowAttributes;

Voici les 15 champs qui servent à spécifier quels membres de la structure on a remplis.

CWBackPixmap, CWBackPixel, CWBorderPixmap, CWBorderPixel, CWBitGravity, CWWinGravity, CWBackingStore, CWBackingPlanes, CWBackingPixel, CWOverrideRedirect, CWSaveUnder, CWEventMask, CWDontPropagate, CWColormap, CWCursor.

Les attributs de la fenêtre peuvent être modifiés après la création à l'aide de fonctions spécialisées telles :

 
Sélectionnez
XChangeWindowAttributes(Display *display,Window w,
    unsigned long valuemask, XSetWindowAttributes *attributes);

Un autre type de structure destinée à des paramètres :

 
Sélectionnez
XConfigureWindow(Display *display, Window w, unsigned int
    value_mask, XWindowChanges *values);

La structure de type XWindowChanges contient les champs :

 
Sélectionnez
typedef struct
{
    int x, y;
    int width, height;
    int border_width;
    Window sibling;
    int stack_mode;
} XWindowChanges;

Il faut préciser quels champs de la structure values on a renseigné à l'aide de ces masques : CWX, CWY, CWWidth, CWHeight, CWBorderWidth, CWSibling, CWStackMode.

D'autres fonctions concernant la géométrie sont encore plus spécialisées :

 
Sélectionnez
XResizeWindow(Display *display, Window w, unsigned int width,
    unsigned int height);
XMoveResizeWindow(Display *display, Window w, int x, int y,
    unsigned int width, unsigned int height);
XSetWindowBorderWidth(Display *display, Window w,
    unsigned int width);

II-B. Les événements

II-B-1. Description

Une application X est dirigée par les événements. Par événement on entend un message provenant du serveur, arrivant de manière asynchrone.

Ces événements peuvent être divisés en plusieurs catégories.

Viennent d'abord les événements correspondants à une action utilisateur comme la frappe d'une touche au clavier.

On trouve ensuite les événements générés par une action de windowmanager : exposition (lorsqu'on découvre une partie d'une fenêtre en déplaçant celle qui se trouvait dessus) ou changement de taille.

Enfin, on a les événements envoyés par d'autres applications, ce qui permet de disposer de primitives de base de communication.

La gestion des événements est assurée de façon repartie : le serveur X maintient une file d'attente (FIFO : premier entré, premier sorti) de tous les événements et les envoie aux différents clients.

Tout événement est rattaché à une fenêtre.

Il existe plusieurs types d'événements :

  • événements matériels (envoyés par le Window-Manager) ;
  • clic et mouvement souris ;
  • événements clavier ;
  • changement de fenêtres (envoyés par le Window-Manager) ;
  • entrée dans la fenêtre (focus) ;
  • sortie de la fenêtre (perte de focus) ;
  • événements rattachés à la géométrie de la fenêtre (envoyés par le Window-Manager) ;
  • destruction de la fenêtre ;
  • affichage de la fenêtre ;
  • exposition ;
  • changement de taille.

II-B-2. Programmation

Les clients demandent à recevoir des événements par des masques à l'aide de fonctions telles que

 
Sélectionnez
XSelectInput(Display * display,Window w, long event_mask)

Exemple :

 
Sélectionnez
XSelectInput(display, win, ExposureMask | KeyPressMask 
    | ButtonPressMask | StructureNotifyMask | PointerMotionMask);

La fenêtre sera sensible à un changement d'exposition, aux entrées clavier, aux clics souris, au changement de taille (structure), et aux déplacements du curseur.

On peut également spécifier le masque d'événement ci-dessus à la création de la fenêtre.

Le traitement des événements se fait à l'aide de routines de lecture spécialisées, celles-ci concernent la file des événements rattachés à une connexion sur un display.

La fonction suivante permet d'extraire un événement de la liste et de le copier dans une structure XEvent locale : XNextEvent(Display *, XEvent *).

XPeekEvent(Display *, XEvent *) permet quant à elle de copier l'événement, mais il n'est pas détruit dans la liste du display.

Pour traiter les événements concernant notre fenêtre, soit on écoute la liste des événements globale rattachée à la connexion du display et qui traite toutes les fenêtres que l'on a ouvertes (et dans ce cas on teste le champ XEvent->xany.window pour traiter seulement ceux qui concerne une fenêtre en particulier), soit on s'adresse à une fonction plus spécialisée :

 
Sélectionnez
XWindowEvent(Display *display, Window w, long event_mask, Xevent *event_return)

Cette dernière fonction ne traite que les événements dont le type correspond à un type spécifié dans le masque event_mask et dont la fenêtre propriétaire est celle passée en paramètre.

X-Window ne génère pour une fenêtre que les événements dont le type correspond à un type spécifié dans le masque lors de la création de la fenêtre ou avec XSelectInput(). On écoute ensuite avec la fonction XWindowEvent certains types de messages (dans ce dernier cas, il y a deux filtres de messages).

Le traitement des événements reçus par une fenêtre se fait dans une boucle de gestion d'événements : après la création et le mapping des fenêtres, les clients doivent boucler et agir en fonction des événements reçus :

Exemple d'une boucle de gestion d'événements.

 
Sélectionnez
XEvent report;
while(1)
{
    XNextEvent(display, &report); /* si un seule fenêtre */
    switch (report.type)
    {
        case Expose:
            /* traitement des événements de type Expose */
            break;
        case ButtonPress:
            /* traitement des événements de type ButtonPress */
            break;
        case KeyPress:
            /* traitement des événements de type KeyPress */
            break;
        case ConfigureNotify:
            /* traitement des événements de type ConfigureNotify */
            case /* ....*/ :
            /* .....*/
        break;
    }
}

La structure d'un événement (type XEvent) est une union entre différentes structures d'événements bien adaptés à chaque classe d'événement.

La structure ci-dessous, tirée des includes, vous permet de connaître tous les types d'événements. On accédera aux informations retournées par ces événements en accédant au champ correspondant de la structure union.

 
Sélectionnez
typedef union _XEvent {
    int type; /* must not be changed; first element */
    XAnyEvent xany;
    XKeyEvent xkey;
    XButtonEvent xbutton;
    XMotionEvent xmotion;
    XCrossingEvent xcrossing;
    XFocusChangeEvent xfocus;
    XExposeEvent xexpose;
    XGraphicsExposeEvent xgraphicsexpose;
    XNoExposeEvent xnoexpose;
    XVisibilityEvent xvisibility;
    XCreateWindowEvent xcreatewindow;
    XDestroyWindowEvent xdestroywindow;
    XUnmapEvent xunmap;
    XMapEvent xmap;
    XMapRequestEvent xmaprequest;
    XReparentEvent xreparent;
    XConfigureEvent xconfigure;
    XGravityEvent xgravity;
    XResizeRequestEvent xresizerequest;
    XConfigureRequestEvent xconfigurerequest;
    XCirculateEvent xcirculate;
    XCirculateRequestEvent xcirculaterequest;
    XPropertyEvent xproperty;
    XSelectionClearEvent xselectionclear;
    XSelectionRequestEvent xselectionrequest;
    XSelectionEvent xselection;
    XColormapEvent xcolormap;
    XClientMessageEvent xclient;
    XMappingEvent xmapping;
    XErrorEvent xerror;
    XKeymapEvent xkeymap;
    long pad[24];
} XEvent;

Le champ type de la structure XEvent permet de tester le type de chaque événement reçu.

Selon le type de l'événement, on peut indexer sur la structure mémoire concernée. Cela évite d'avoir une structure « fourre-tout » dans laquelle il y aurait tous les paramètres de tous les événements.

Par exemple, pour traiter les caractères entrés au clavier :

 
Sélectionnez
case KeyPress:
{
    char buffer;
    int bufsize=1;
    int charcount;
    charcount = XLookupString(&(report.xkey), &buffer, bufsize, NULL, NULL);
    switch(buffer) {
        case 'q':
            printf("Touche q pressée\n");
            break;
        default:
            printf("Touche non valide\n");
            break;
    }
}

Pour lire les coordonnées de la souris pendant un clic souris :

 
Sélectionnez
case ButtonPress:
{
    switch (report.xbutton.button)
    {
        case Button1 :
        {
            x = xevent.xbutton.x;
            y = xevent.xbutton.y;
            printf("Bouton 1 pressé en %d %d\n",x,y);
            break;
        }
        case Button3 :
        {
            /* ......*/
            break;
        }
    }
    break;
}

Dans l'exemple ci-dessus, le type de l'événement ButtonPress signifie l'enfoncement d'une touche de la souris.

Dans l'exemple suivant, on récupère la nouvelle taille de la fenêtre (sans faire appel à la fonction XGetGeometry()).

 
Sélectionnez
case ConfigureNotify:
{
    printf("Fenetre retaillee: width = %d height = %d\n",
    report.xconfigure.width, report.xconfigure.height);
    break;
}

Il existe de très nombreux événements, cependant nous ne nous servons généralement que des quatre ou cinq plus utiles.

Ne pas confondre :

  • le nom de la structure de l'événement. Exemple : XButtonEvent, XKeyEvent, XmotionEvent, XExposeEvent, XconfigureEvent ;
  • le nom des constantes associées qui vont figurer dans le switch de la boucle de gestion des événements : KeyPress, KeyRelease, ButtonPress, ButtonRelease, MotionNotify, Expose, ConfigureNotify ;
  • le nom des masques qui figurent dans l'appel à XSelectInput().

Par exemple, lors d'un mouvement de la souris, un événement du type MotionNotify est reçu (le fait que l'on veuille recevoir les événements du mouvement de la souris a été spécifié par PointerMotionMask).

II-C. Les fonctions graphiques

La Xlib fournit une panoplie assez complète de fonctions de dessin 2D.

Ces fonctions sont en général simples à utiliser :

  • tracés de points, lignes, rectangles, arcs, polygones et textes, creux ou pleins ;
  • requêtes simples ou multiples (tracer une ligne ou plusieurs lignes).

II-C-1. Graphic context

Pour dessiner, il est nécessaire de spécifier des choses telles que la largeur du trait, si l'on veut tracer des lignes en trait plein ou en pointillés, la couleur, les patterns pour le remplissage des formes géométriques…

Avec la Xlib, on passe ces informations aux fonctions de dessin grâce à un identifieur de type GC. Une telle structure doit être créée et initialisée avant l'appel des fonctions de dessin. Plusieurs structures de ce type peuvent être créées, rendant le dessin dans différents styles simples à réaliser. Le GC est un des paramètres obligatoires de toutes les fonctions de dessin de la Xlib.

II-C-2. Programmation du contexte graphique

Utilisation : créer un GC par type d'opération graphique, par exemple un pour chaque fonte de caractères utilisée. On se servira des différents GC selon que l'on désire écrire les textes à l'écran en italique, en gras, en gros, en petit…

Pour créer un GC il faut utiliser la fonction

 
Sélectionnez
XcreateGC(Display *display, Drawable d, unsigned long valuemask, XGCValues *values)

qui accepte quatre paramètres :

  • un pointeur (Display *) sur le Display ;
  • un pointeur (XID) sur un Drawable (fenêtre ou pixmap) ;
  • un pointeur sur un objet de type XGCValues ;
  • un masque spécifiant quels champs de la structure XGCValues on a renseignés.

On peut créer le GC soit en passant des champs dans la structure avant l'appel de la fonction XCreateGC(), soit ne pas s'en occuper et configurer le contexte graphique par la suite à l'aide de fonctions spécifiques comme XSetForeground().

Un masque sur les champs du paramètre précédent. Si ce paramètre est NULL alors, le GC est initialisé avec les valeurs par défaut. Sinon, on peut l'initialiser comme bitmask et indiquer quels champs de la structure XGCValues sont déjà initialisés.

La structure XGCValues contient des éléments tels que foreground, background, line_width, line_style que l'on peut initialiser. Le masque est initialisé en utilisant des valeurs comme GCForeground, GCBackground, GCLineStyle

La Xlib fournit des macros très utiles lorsque l'on initialise un contexte graphique. Parmi les plus souvent utilisées, BlackPixel() et WhitePixel() renvoient la valeur par défaut des couleurs noir et blanc pour un display donné. Ces valeurs sont prises dans la table de couleurs (colormap) par défaut.

Deux exemples de création de GC :

initialisation du GC par défaut, ensuite on peut le modifier :

 
Sélectionnez
GC gc;
..
/* Ouvrir le display, créer les fenêtres, etc. */
..
gc = XCreateGC(display, RootWindow(display, screen), 0, NULL);
XSetForeground(display, gc, BlackPixel(display, screen));
XSetBackground(display, gc, WhitePixel(display, screen));
/* maintenant on peut utiliser ce GC dans avec les fonctions de
dessin */
..

initialisation du GC par défaut, ensuite on peut le modifier :

 
Sélectionnez
GC gc;
XGCValues values;
unsigned long valuemask;
/* Ouvrir le display, créer les fenêtres, etc. */
values.foreground = BlackPixel(display, screen);
values.background = WhitePixel(display, screen);
gc = XCreateGC(display, RootWindow(display, screen), (GVForeground | GCBackground), &values);
/* Maintenant, on peut utiliser ce GC dans avec les fonctions de
dessin */

Finalement, la mise à jour des paramètres d'un contexte graphique se fait de la même manière que pour les fenêtres, on doit utiliser des fonctions spécifiques pour mettre à jour des champs de structure dans la mémoire du serveur dont on ne possède que des identifieurs (XID).

II-C-3. Les fonctions graphiques

Ces fonctions travaillent sur des Drawables : Pixmaps ou Windows. Le résultat dépend du GC.

Les fonctions de dessin :

 
Sélectionnez
XDrawPoint(display, drawable, gc, x, y)

Probablement la plus simple des fonctions graphiques. Elle dessine un point avec la couleur courante décrite dans le GC à la position (x, y).

 
Sélectionnez
XDrawPoints(display, drawable, gc, XPoint *pts, n, mode)

Fonction similaire à la précédente si ce n'est qu'un tableau de n éléments de type XPoint est dessiné. Le paramètre mode (un int) vaut soit CoordModeOrigin soit CoordModePrevious selon que les coordonnées des points sont relatives à l'origine ou aux coordonnées du dernier point.

 
Sélectionnez
XDrawLine(display, drawable, gc, x1, y1, x2, y2)

Dessine une ligne entre (x1, y1) et (x2, y2).

 
Sélectionnez
XDrawLines(display, drawable, gc, XPoints *pts, n, mode)

Dessine un ensemble de lignes connectées, en prenant des paires de points dans la liste. Voir la fonction XDrawPoints() pour le paramètre mode.

 
Sélectionnez
XDrawRectangle(display, drawable, gc, x, y, width, height)

Dessine un rectangle dont le coin en haut à gauche est en (x, y) et de taille (width, height).

 
Sélectionnez
XFillRectangle(display, drawable, gc, x, y, width, height)

Dessine un rectangle plein. L'attribut fill_style du GC spécifie la manière dont on effectue le remplissage (couleur pleine, pattern…)

 
Sélectionnez
XFillPolygon(display, drawable, gc, XPoint *pts, n, shape, mode)

Semblable à XDrawLines sauf que le polygone dessiné est rempli. Le paramètre shape sert d'aide au serveur pour optimiser le remplissage du polygone.

Les valeurs possibles sont :

  • Complex : les côtés du polygone peuvent s'intercepter ;
  • Nonconvex : les côtés du polygone ne s'interceptent pas, mais il n'est pas convexe ;
  • Convex : le polygone est convexe, le dessin sera nettement plus rapide.
 
Sélectionnez
XDrawArc(display, drawable, gc, x, y, width, height, angle1, angle2)

Les paramètres x, y, width et height définissent une boite (Bounding Box) contenant l'arc de cercle. Le centre de l'arc est le centre de la boite. Les paramètres angle1 et angle2 spécifient la zone du cercle qui sera dessiné. Ces angles sont en 1/64 de degrés, mesurés dans le sens trigonométrique. L'angle de valeur zéro est à la position trois heures. Le paramètre angle2 est mesuré relativement à angle1.

Ainsi, pour dessiner un cercle entier, il faudra positionner width et height égaux au diamètre, et choisir angle1 = 0 et angle2 = 360*64 =23 040.

 
Sélectionnez
XFillArc(display, drawable, gc, x, y, width, height, angle1, angle2)

Idem à la fonction précédente sauf que l'arc de cercle est plein. Les fonctions de tracé de texte seront étudiées ultérieurement. Les fonctions de tracé de bitmaps également.

Effacement d'une fenêtre :

 
Sélectionnez
XClearWindow(display, drawable)

Efface le contenu d'une fenêtre ou d'un Pixmap.

II-C-4. Les drawables

On appelle Drawable un objet Xlib dans lequel on peut dessiner. En d'autres termes, un Drawable est un identificateur vers une structure qui va contenir des graphiques : une fenêtre (Window) ou une zone mémoire (Pixmap). On peut utiliser la fonction XCopyArea() de l'un à l'autre.

II-C-5. Le texte sous X

II-C-5-a. Description

Un texte peut être imprimé à l'écran en spécifiant une chaîne de caractères et une police. Une police est un ensemble de caractères de même esthétique. Une fonte est un ensemble de polices de la même famille. On appelle abusivement une police, une fonte.

La métrique d'une fonte regroupe cinq paramètres :

  • la hauteur ;
  • la profondeur ;
  • la largeur ;
  • l'approche ;
  • l'incrément.

Voir sur la figure suivante la signification de ces paramètres.

Image non disponible
Figure 3 : métrique d'une fonte X.

Le principe de fonctionnement est toujours le même. La fonte doit être dans la mémoire du serveur pour que son accès y soit rapide. La structure mémoire descriptive de la fonte est chargée quand on en fait la requête. La seule chose vue par le programme est un identifieur unique (comme pour les fenêtres ou les colormaps).

Avant d'utiliser une police de caractères, il faut l'avoir demandée au serveur. Le serveur possède des fontes par défaut (en ROM pour les terminaux X). En général elle est lue sur disque et envoyée au serveur ; cependant, si une autre application est déjà en train de l'utiliser elle sera partagée dans la mémoire du serveur X.

On peut consulter les différentes polices (fontes) de caractères disponibles : les noms des différentes polices peuvent être consultés à l'aide du programme xlsfonts.

Une police sera visualisée à l'aide du programme xfd -fn font_name.

Un autre logiciel plus interactif permet de visualiser et choisir les différentes polices en même temps : xfontsel.

II-C-5-b. Programmation de l'affichage de texte

Plusieurs manières de charger une police de caractères :

  • manière simple : le chargement d'une police se fait à l'aide de la fonction Font *XloadFont(display, font_name). Cette fonction charge la police et retourne un ID de la fonte (de type Font). Cet ID peut être ensuite utilisé par la fonction XSetFont(display, gc, font_ID) pour associer la police à un Contexte Graphique (GC) ;
  • manière un peu plus compliquée : il est très difficile de calculer la hauteur et la longueur d'une chaîne de caractères quelconque sans connaître les caractéristiques détaillées de la police utilisée (surtout si celle-ci n'est pas proportionnelle).

Si une police a déjà été chargée par un appel à XLoadFont(), on peut utiliser la fonction XFontStruct *XQueryFont(display, font_ID) qui fournit des informations très détaillées sur la police passée en paramètre. Ces informations se trouvent dans la structure XFontStruct.

Remarque : il est possible de faire XLoadFont() et XQueryFont() en une seule fois à l'aide de la fonction

 
Sélectionnez
XFontStruct *XloadQueryFont(display, font_name)

Exemple de code pour la lecture d'une fonte de caractères :

 
Sélectionnez
XFontStruct *font_info;
char *fontname = "9x15";
if((*font_info = XLoadQueryFont(display,fontname)) == NULL)
{
    fprintf(stderr,"Police de caractères %s non trouvée \n",fontname);
    exit (-1);
}

Ne pas oublier de libérer la mémoire allouée : lorsque vous n'avez plus besoin d'une police de caractères, libérez-la en appelant la fonction

 
Sélectionnez
XFreeFont(display, font_struct).

Largeur et hauteur d'une chaîne de caractères :

Chaque caractère d'une police donnée possède trois attributs qui sont utiles pour le calcul de la hauteur et de la largeur d'un texte :

  • une baseline : ligne horizontale qui coupe le caractère en deux, au-dessous de laquelle se trouve le jambage ;
  • un ascent : indique le nombre de pixels au-dessus de la baseline ;
  • un descent : indique le nombre de pixels au-dessous de la baseline.

La hauteur d'un texte s'obtient à l'aide des champs ascent et descent du champ max_bounds (de type XCharStruct) de la structure XFontStruct.

La largeur d'un texte s'obtient à l'aide de la fonction XTextWidth().

Cet exemple montre comment procéder :

 
Sélectionnez
#define STRING "toto"
{
    XFontStruct *fontstruct;
    fontstruct = XLoadQueryFont(display,fontname);
    height_font=
        fontstruct.max_bounds.ascent+fontstruct.max_bounds.decent;
    width_font = XTextWidth(fontstruct, STRING, strlen(STRING));
}

Pour afficher le texte (enfin), il existe trois fonctions principales.

 
Sélectionnez
XDrawString(display, drawable, gc, x, y, string, length)

Affiche une chaîne de caractères dans un Drawable.

x et y représentent les coordonnées de la baseline de la chaîne.

Rappel : la baseline d'une chaîne est une ligne horizontale en dessous de laquelle les caractères possèdent un « jambage ».

 
Sélectionnez
XDrawImageString(display, drawable, gc, x, y, string, length)

Similaire à la fonction précédente sauf que la boite qui contient la chaîne est remplie avec la couleur de background spécifié dans le contexte graphique passé en paramètre.

En inversant les champs background et foreground du GC, on peut facilement afficher du texte en inverse vidéo à l'aide de cette fonction.

 
Sélectionnez
XDrawText(display, drawable, gc, x, y, items, nitems)

Cette fonction permet d'afficher une ou plusieurs chaînes de caractères à la fois. Chaque item est un objet de type XTextItem qui contient la chaîne de caractères, la police de caractères, un espacement horizontal à partir de la fin de l'item précédent…

II-D. Gestion de la couleur sous X11

II-D-1. Concepts de base, vocabulaire incontournable

Jusqu'à présent nous avons spécifié les couleurs au travers des champs foreground et background d'un GC. Pour cela, nous avons utilisé les macros BlackPixel() et WhitePixel(), car nous nous étions limités au noir et blanc. Il est cependant possible de mettre dans ces champs une valeur correspondant à une véritable couleur.

Parce que X a été conçu pour supporter une large variété d'architectures hardware, les mécanismes de gestion de la couleur proposés par la Xlib sont relativement complexes.

La plupart des écrans couleur aujourd'hui utilisent le modèle RGB pour le codage des couleurs.

Un écran couleur utilise plusieurs bits par pixels (multiple bit planes screen) pour coder la couleur de ce pixel (pixel value).

Une colormap est utilisée pour transformer la valeur numérique d'un pixel (pixel value) vers la couleur effectivement visible à l'écran. Une colormap est en réalité une table située dans la mémoire du serveur ; la valeur numérique d'un pixel est un index dans cette table (pixel value). À une pixel value de 16 correspond une couleur spécifiée par le seizième élément de la colormap. Un élément de la colormap est appelé un colorcell.

La sensibilité des couleurs est donc très fine : chaque composante varie de 0 à 65 535. Une couleur sous X est représentée par :

 
Sélectionnez
typedef struct {
    unsigned long pixel;/* pixel value */
    unsigned short red, green, blue;/* rgb values */
    char flags; /* DoRed, DoGreen, DoBlue */
    char pad;
} XColor;

En général, chaque colorcell contient trois valeurs codées sur 16 bits, correspondant aux intensités de chacune des composantes RGB.

Si on utilise 8 bits (sur les 16) pour coder chaque composante, une colorcell peut coder 2563 couleurs (16 millions).

Le nombre de couleurs pouvant être affichées simultanément à l'écran dépend du nombre de bitplanes utilisés pour coder la pixel value. Un système à 8 bitplanes pourra indexer 28 colorcells (256 couleurs). Un système avec 24 bitplanes pourra afficher 16 millions de couleurs.

II-D-1-a. Colormap

X propose le concept de colormap virtuelle ou colormap privée, qui permet de gérer plusieurs colormaps, chacune proposant une palette différente ; mais une seule peut être active à un moment donné. Ces colormaps sont swappées (échangées avec la colormap hardware) par le Window Manager lorsque le focus passe d'une fenêtre à l'autre. Conséquence de cette limitation : les fenêtres présentes à l'écran voient leurs couleurs s'altérer lorsque le focus est dans une fenêtre possédant une colormap différente.

La colormap est une structure de données stockée dans le serveur X. On n'a pas accès directement aux valeurs. On procède à des requêtes. Un identifieur de colormap est un identifieur unique par display :

 
Sélectionnez
typedef unsigned long XID;
typedef XID Colormap;
II-D-1-a-i. Allocation de couleurs

Si l'on veut dessiner en bleu, il faut allouer une case dans la colormap qui contient les composantes (0, 0, 255). L'allocation renvoie l'indice de cette couleur dans la colormap (par exemple : 37). Dessiner dans le frame-buffer avec la valeur de pixel égale à 37 produira des graphiques bleus.

Mais si 15 clients allouent la couleur bleue, il est dommage de dupliquer 15 fois l'entrée dans la colormap. X propose le partage des couleurs.

Un client désirant utiliser une couleur donnée ne spécifie pas explicitement la pixel value et la couleur de la colorcell correspondant à cette pixel value. À la place, il demande l'accès à une colorcell dans la colormap (gérée par le serveur X), et une pixel value lui est retournée.

On appelle ce mécanisme l'allocation de couleur : lorsqu'un client désire utiliser la couleur bleue, c'est comme s'il posait au serveur la question :

  • « Quelle colorcell dois-je utiliser pour dessiner en bleu ? », et le serveur lui répond :
  • « Tu peux utiliser la colorcell correspondant à cette pixel value ! »

C'est ça l'allocation des couleurs !

II-D-1-a-ii. Partage des couleurs

Dans les modes DirectColor, GrayScale et PseudoColor, on peut éditer la colormap. Une entrée de la colormap est nommée colorcell.

Il en existe de deux types :

  • en lecture seule (RO) ;
  • en lecture/écriture (RW).
II-D-1-a-iii. Cellules RO

Une cellule RO positionnée par un client (s'il reste de la place dans la colormap) peut être partagée avec les autres clients qui sont lancés après.

Dans ce cas, la couleur sera libérée après que le dernier client qui l'utilise soit fermé.

II-D-1-a-iv. Cellules RW

Une cellule RW positionnée par un client (s'il reste de la place dans la colormap) ne pourra pas être exploitée par un autre client. Comme on l'alloue en lecture/écriture, on pourrait changer son contenu et les autres clients qui l'exploitent changeraient d'apparence. Cela est contraire aux principes de X.

II-D-1-b. Private vs. Public

Un client X doit aussi pouvoir allouer ses 256 couleurs (par exemple une LUT de gris). Dans la colormap système standard, certaines colorcells sont RO (environ une quinzaine quand on démarre X). On peut donc allouer au plus 240 couleurs.

Dans ce cas, on peut déclarer qu'une fenêtre d'un client possède une colormap privée. Cette colormap sera installée sur l'écran dès que le focus concernera cette fenêtre.

Pour programmer la mise en place d'une colormap privée, on alloue une colormap et on l'affecte à une fenêtre. Elle sera installée dès que le focus entrera dans la fenêtre. Il suffit ensuite de remplir la colormap avec les entrées souhaitées.

 
Sélectionnez
Display *Dpy;
Window Win_desc;
XSetWindowAttributes xswa;
XColor xcol;
int Scr, n;
Visual *visual;
Colormap Cmap;

Scr = DefaultScreen(Dpy);
Win_desc = XCreateSimpleWindow(Dpy, RootWindow(Dpy, Scr), 100, 100, 640, 480, 3,
    WhitePixel(Dpy, Scr), BlackPixel(Dpy, Scr));
visual = DefaultVisual(Dpy, Scr);
Cmap=XCreateColormap(Dpy,DefaultRootWindow(Dpy), visual, AllocAll);
xswa.colormap = Cmap;
XChangeWindowAttributes(Dpy, Win_desc, CWColormap, &xswa);

for (n = 0; n < 256; n++)
{
    xcol.pixel = (unsigned long) n;
    xcol.red = n << 8;
    xcol.green = n << 8;
    xcol.blue = n << 8;
    xcol.flags = DoRed | DoGreen | DoBlue;
    XStoreColor(Dpy, Cmap, &xcol);
}

Dans l'exemple précédent, on a rempli la colormap de niveaux de gris du noir au blanc.

Les fonctions XQueryColor() et XQueryColors() permettent de connaître les valeurs RGB de chaque colorcell. Pour mettre des colorcells à jour, on utilise les fonctions XStoreColor() et XStoreColors().

II-D-1-c. Allocation de couleurs partagées

Lorsqu'on peut s'en contenter, pour les raisons précédemment citées, il est fortement conseillé d'allouer des couleurs en Read-Only.

La pixel value retournée par les fonctions d'allocation de couleur servira à positionner les champs foreground ou background d'un GC ou bien les attributs background_pixel ou border_pixel d'une fenêtre.

II-D-1-c-i. Fonctions d'allocation de couleurs en Read-Only

XAllocColor() renvoie la pixel value (indice de colorcell dans la colormap) qui contient la valeur RGB demandée, ou qui contient la valeur RGB la plus proche disponible sur cet écran. XAllocNamedColor() renvoie la pixel value (indice de colorcell dans la colormap) qui contient la valeur RGB correspondant au nom passé en paramètre (qui doit être dans la base de données des couleurs standards), ou qui contient la valeur RGB la plus proche disponible sur cet écran. L'utilisation de cette fonction est recommandée, car les couleurs ont plus de chances d'être partagées.

XParseColor() parse le nom d'une couleur ou la spécification hexadécimale d'une couleur et renvoie les valeurs RGB correspondantes. On l'utilise en général en tandem avec XAllocColor(). L'utilisation de ces deux fonctions au lieu de XAllocNamedColor() permet de repérer de manière plus fine les erreurs de syntaxe dans la spécification de la couleur.

Il n'est pas possible de savoir si une colorcell est Read/Write ou Read-Only !

La seule manière de savoir combien de colorcells sont disponibles pour l'allocation consiste à allouer N couleurs à l'aide de la fonction XAllocColorCells(), avec une grande valeur de N, puis à recommencer en diminuant la valeur de N jusqu'à ce que l'allocation ait réussi.

II-D-1-c-ii. Base de données de couleurs standards

X propose une base de données de couleurs standards, qui associe des noms de couleurs à des valeurs RGB. Cette base de données standard encourage le partage des couleurs par les applications clients. Elle se trouve dans /usr/lib/X11/rgb.txt (/usr/openwin/lib/rgb.txt sous OpenWindows) et possède près de 300 entrées.

Le partage des couleurs ne peut intervenir que si deux applications allouent une même couleur read-only, c'est-à-dire une couleur possédant les mêmes valeurs des composantes R, G et B, et ne pouvant être modifiée (c'est pourquoi on l'appelle read-only). À l'aide de cette base de données, il y a plus de chances pour que les clients partagent les couleurs que s'ils les allouent eux-mêmes en les prenant parmi les 248 combinaisons RGB possibles.

Il est à noter que le serveur X n'utilise pas directement le fichier /usr/lib/X11/lib/rgb.txt, mais une version compilée de celui-ci.

II-D-1-c-iii. Nommage hexadécimal des couleurs

Il est également possible, même si cela est déconseillé, de spécifier une couleur en hexadécimal.

Quatre formats possibles :

 
Sélectionnez
#RGB (4 bits pour chaque composante)
#RRGGBB (8 bits " " " " )
#RRRGGGBBB (12 bits " " " " )
# RRRRGGGGBBBB (16 bits " " " " )

Chaque lettre correspond à un chiffre hexadécimal. #3a7 et #3000a0007000 sont équivalents.

II-D-1-d. Création et installation de colormaps

Nous avons parlé de colormaps hardware et de colormaps privées ou colormaps virtuelles. Nous allons étudier ici comment manipuler ces objets.

Une colormap hardware est un objet physique dont le contenu (valeurs RGB) est lu par le hardware vidéo de l'écran pour afficher les couleurs. La plupart des stations de travail possèdent une seule colormap hardware.

Certaines machines haut de gamme en possèdent plusieurs, ce qui permet à plusieurs fenêtres d'avoir chacune leur propre colormap hardware.

En général, on peut modifier les valeurs RGB des colorcells de la colormap hardware, ou bien on peut swapper l'ensemble des valeurs de la colormap hardware avec celles d'une colormap située dans la mémoire de l'ordinateur (colormap virtuelle ou privée). Les visuals DirectColor, GrayScale et PseudoColor ne sont disponibles que sur des écrans ayant ces possibilités.

Jusqu'à présent, nous n'avons alloué que des couleurs dans la colormap par défaut (créée lors du lancement du serveur). Sur des écrans limités à 256 couleurs, il arrive souvent que des clients gourmands en couleurs, ou qui nécessitent des couleurs précises pour fonctionner correctement (comme XV, Netscape, Xemacs), allouent la totalité des colorcells disponibles en Read/Write.

Dans ce cas, la solution pour une application en manque de couleurs consiste à utiliser une colormap privée.

Le Window Manager se chargera de faire l'échange entre la colormap hardware et cette colormap privée lorsque le focus sera dans une fenêtre de l'application.

Lorsqu'une application crée une colormap privée, elle doit positionner l'attribut colormap de sa top-level window avec l'identificateur de cette colormap, afin que le Window Manager sache quelle colormap installer. Le WM ne peut dialoguer qu'avec les top-level windows.

II-D-1-e. Fonctions de manipulation des colormaps
  • XCreateColormap() : crée une colormap correspondant au visual passé en paramètre, avec toutes les colorcells allouées en Read/Write, ou sans allocation de colorcells.
    Si on n'alloue pas les colorcells lors de l'appel, on pourra par la suite les allouer indépendamment en Read-Only ou en Read/Write à l'aide des fonctions de manipulation de colorcells déjà étudiées. Si on alloue les colorcells lors de l'appel, il suffit ensuite de les initialiser à l'aide de XStoreColors() par exemple.
  • XfreeColormap() : libère les ressources occupées par une colormap privée. Envoie un événement ColormapNotify à toutes les fenêtres qui utilisaient cette colormap.
  • XlistInstalledColormaps() : liste les colormaps installées.
  • XCopyColorMapAndFree() : permet de copier une colormap dans une autre, et de libérer l'ancienne. Ceci est utile lorsque l'allocation des colorcells échoue après que certaines colorcells ont été allouées avec succès. Cela évite de recréer une colormap et de recommencer l'allocation des colorcells depuis le début.
  • XSetWindowColormap() : positionne l'attribut colormap d'une window.
  • XInstallColormap() : non étudiée. Utile si on veut écrire un Window Manager.
  • XUninstallColormap() : non étudiée. Utile si on veut écrire un Window Manager.
II-D-1-f. Exemple de code allouant des couleurs en Read-Only
 
Sélectionnez
/*
* Copyright 1989 O'Reilly and Associates, Inc.
* See ../Copyright for complete rights and liability information.
*/
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xos.h>
#include <stdio.h>

extern Display *display;
extern int screen_num;
extern Screen *screen_ptr;
extern unsigned long foreground_pixel, background_pixel, border_pixel;
extern char *progname;

#define MAX_COLORS 3

static char *visual_class[] = {
    "StaticGray",
    "GrayScale",
    "StaticColor",
    "PseudoColor",
    "TrueColor",
    "DirectColor"
};

get_colors()
{
    int default_depth;
    Visual *default_visual;
    static char *name[] = {"Red", "Yellow", "Green"};
    XColor exact_def;
    Colormap default_cmap;
    int ncolors = 0;
    int colors[MAX_COLORS];
    int i = 5;
    XVisualInfo visual_info;
    
    /* Try to allocate colors for PseudoColor, TrueColor,
    * DirectColor, and StaticColor. Use black and white
    * for StaticGray and GrayScale */
    
    default_depth = DefaultDepth(display, screen_num);
    default_visual = DefaultVisual(display, screen_num);
    default_cmap = DefaultColormap(display, screen_num);
    
    if (default_depth == 1) {
        /* must be StaticGray, use black and white */
        
        border_pixel = BlackPixel(display, screen_num);
        background_pixel = WhitePixel(display, screen_num);
        foreground_pixel = BlackPixel(display, screen_num);
        return(0);
    }
    
    while (!XMatchVisualInfo(display, screen_num, default_depth,
        /* visual class */i--, &visual_info));
        
    printf("%s: found a %s class visual at default_depth.\n",
    progname, visual_class[++i]);
    
    if (i < 2) {
        /* No color visual available at default_depth.
        * Some applications might call XMatchVisualInfo
        * here to try for a GrayScale visual
        * if they can use gray to advantage, before
        * giving up and using black and white.
        */
        
        border_pixel = BlackPixel(display, screen_num);
        background_pixel = WhitePixel(display, screen_num);
        foreground_pixel = BlackPixel(display, screen_num);
        return(0);
    }
    /* otherwise, got a color visual at default_depth */
    
    /* The visual we found is not necessarily the
    * default visual, and therefore it is not necessarily
    * the one we used to create our window. However,
    * we now know for sure that color is supported, so the
    llowing code will work (or fail in a controlled way).
    * Let's check just out of curiosity: */
    
    if (visual_info.visual != default_visual)
        printf("%s: PseudoColor visual at default depth is not default
            visual!\nContinuing anyway...\n", progname);
            
    for (i = 0; i < MAX_COLORS; i++) {
        printf("allocating %s\n", name[i]);
        if (!XParseColor (display, default_cmap, name[i], &exact_def)) {
            fprintf(stderr, "%s: color name %s not in database",
            progname, name[i]);
            exit(0);
        }
        
        printf("The RGB values from the database are %d, %d, %d\n",
            exact_def.red, exact_def.green, exact_def.blue);
            
        if (!XAllocColor(display, default_cmap, &exact_def)) {
            fprintf(stderr, "%s: can't allocate color: all colorcells
            allocated and no matching cell found.\n", progname);
            exit(0);
        }
        
        printf("The RGB values actually allocated are %d, %d, %d\n",
        exact_def.red, exact_def.green, exact_def.blue);
        colors[i] = exact_def.pixel;
        ncolors++;
    }
    
    printf("%s: allocated %d read-only color cells\n", progname, ncolors);
    
    border_pixel = colors[0];
    background_pixel = colors[1];
    foreground_pixel = colors[2];
    return(1);
}
II-D-1-g. Quand allouer des couleurs privées ?

Plusieurs cas se présentent :

  • l'application dessine avec certaines couleurs qui doivent pouvoir être modifiées sans devoir redessiner (cyclage de couleur, modification du contraste, de la luminosité, etc.). Si on change les valeurs RGB d'une colorcell, tous les points à l'écran possédant une pixel value correspondant à cette colorcell verront leur couleur changer ;
  • l'application doit utiliser des techniques d'overlays. En gros, dessiner des graphiques « par-dessus » d'autres graphiques, et pouvoir les effacer facilement, à la manière d'un calque qu'on enlève (on verra plus tard comment ça marche) ;
  • l'application est très gourmande en couleurs (affichage d'image, synthèse d'image).

REMARQUE : l'allocation de couleurs privées n'est pas possible avec des visuals TrueColor ou StaticColor.

II-D-1-h. Fonctions d'allocation/manipulation de couleurs privées
  • XallocColorCells() : cette fonction permet d'allouer des colorcells privées, dont on peut par la suite changer dynamiquement les valeurs RGB. Pour allouer simplement quelques colorcells, il suffit de positionner le paramètre ncolors à la valeur désirée, et de mettre le paramètre nplanes à 0. Toutes les pixel values seront retournées dans le tableau pixels. On positionnera les valeurs RGB des colorcells allouées à l'aide des fonctions XstoreColor(), XStoreColors() ou XstoreNamedColor().
  • XallocColorPlanes() : cette fonction ne sera pas étudiée dans l'immédiat.
  • XstoreColor() : permet de modifier la colorcell READ/Write correpondant à la pixel value passée en paramètre, en lui attribuant les valeurs RGB les plus proches de celles passées en paramètre dans la structure XColor. Ne pas oublier de positionner les flags DoRed, Dogreen et DoBlue correspondants aux composantes RGB qui doivent être modifiées.
  • XstoreColors() : idem, mais permet de modifier plusieurs colorcells d'un coup.
  • XstoreNamedColors() : très semblable à XstoreColor() sauf que la valeur RGB que va prendre la colorcell correspond à un nom de couleur (qui doit être dans la base de données des couleurs standards).
II-D-1-i. Exemple de code comprenant l'allocation de couleurs privées

Ci-dessous un exemple d'allocation des couleurs dans une colormap privée.

 
Sélectionnez
/*
* Copyright 1989 O'Reilly and Associates, Inc.
* See ../Copyright for complete rights and liability information.
*/
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xos.h>
#include <stdio.h>

extern Display *display;
extern int screen_num;
extern unsigned long foreground_pixel, background_pixel, border_pixel;

#define MAX_COLORS 3

get_colors()
{
    int default_depth;
    Visual *default_visual;
    static char *name[] = {"Red", "Yellow", "Green"};
    XColor exact_defs[MAX_COLORS];
    Colormap default_cmap;
    int ncolors = MAX_COLORS;
    int plane_masks[1];
    int colors[MAX_COLORS];
    int i;
    XVisualInfo visual_info;
    int class;
    
    class = PseudoColor;
    default_depth = DefaultDepth(display, screen_num);
    default_visual = DefaultVisual(display, screen_num);
    default_cmap = DefaultColormap(display, screen_num);
    
    if (default_depth == 1) {
        /* must be StaticGray, use black and white */
        border_pixel = BlackPixel(display, screen_num);
        background_pixel = WhitePixel(display, screen_num);
        foreground_pixel = BlackPixel(display, screen_num);
        return(0);
    }
    
    if (!XMatchVisualInfo(display, screen_num, default_depth,
      PseudoColor, &visual_info)) {
        if (!XMatchVisualInfo(display, screen_num, default_depth,
          DirectColor, &visual_info)) {
          
            /* No PseudoColor visual available at default_depth.
            * Some applications might try for a GrayScale visual
            * here if they can use gray to advantage, before
            * giving up and using black and white.
            */
            border_pixel = BlackPixel(display, screen_num);
            background_pixel = WhitePixel(display, screen_num);
            foreground_pixel = BlackPixel(display, screen_num);
            return(0);
        }
    }
    /* got PseudoColor visual at default_depth */
    
    /* The visual we found is not necessarily the
    * default visual, and therefore it is not necessarily
    * the one we used to create our window. However,
    * we now know for sure that color is supported, so the
    * following code will work (or fail in a controlled way).
    */
    /* allocate as many cells as we can */
    ncolors = MAX_COLORS;
    while (1) {
        if (XAllocColorCells (display, default_cmap, False,
          plane_masks, 0, colors, ncolors))
            break;
            
        ncolors--;
        if (ncolors = 0)
            fprintf(stderr, "basic: couldn't allocate read/write colors\n");
        exit(0);
    }
    
    printf("basic: allocated %d read/write color cells\n", ncolors);
    
    for (i = 0; i < ncolors; i++) {
        if (!XParseColor (display, default_cmap, name[i],
          &exact_defs[i])) {
            fprintf(stderr, "basic: color name %s not in database", name[i]);
            exit(0);
        }
        
        /* set pixel value in struct to the allocated one */
        exact_defs[i].pixel = colors[i];
    }
    
    /* this sets the color of read/write cell */
    XStoreColors (display, default_cmap, exact_defs, ncolors);
    border_pixel = colors[0];
    background_pixel = colors[1];
    foreground_pixel = colors[2];
}

Le programme principal qui appelle cette fonction get_colors() contient un appel à XQueryColor() pour connaître les valeurs RGB des colorcells allouées par get_colors() (fichiers séparés).

Le main fait également appel à XStoreColor() pour modifier dynamiquement la couleur du texte qui est affiché dans la fenêtre sans avoir à le redessiner.

 
Sélectionnez
void main(argc, argv)
int argc;
char **argv;
{
    XColor color;
    unsigned short red, green, blue;
    .
    .
    .
    /* open display, etc... */
    color.pixel = foreground_pixel;
    XQueryColor(display, DefaultColormap(display, screen_num), &color);
    printf("red is %d, green is %d, blue is %d\n",
    color.red, color.green, color.blue);
    
    while (1) {
        XNextEvent(display, &report);
        switch (report.type) {
            .
            .
            .
            case ButtonPress:
                color.red += 5000;
                color.green -= 5000;
                color.blue += 3000;
                printf("red is %d, green is %d, blue is %d\n",
                color.red, color.green, color.blue);
                XStoreColor(display, DefaultColormap(display, screen_num),
                &color);
                break;
            .
            .
            .
        }
    }
II-D-1-j. Exemple d'utilisation des couleurs nommées et partagées

On se propose d'allouer des couleurs dans la colormap système. On alloue la couleur rouge. Pour que la fenêtre en cours hérite de la colormap système, il faut qu'à l'initialisation, rien ne soit spécifié concernant la colormap.

Exemple :

 
Sélectionnez
XcreateSimpleWindow(Dpy,RootWindow(Dpy,Scr),
    100,100,500,400,3, 
    BlackPixel(Dpy,Scr),WhitePixel(Dpy,Scr))

Avec la fonction XCreateSimpleWindow() la colormap est héritée de la fenêtre mère (ici, la RootWindow). Pour la fonction XCreateWindow(), la colormap est héritée de la fenêtre mère si aucune colormap n'est spécifiée dans la structure XSetWindowAttributes passée en dernier paramètre.

On doit récupérer l'identifieur de la colormap héritée :

 
Sélectionnez
Colormap cmap;
XGetWindowAttributes(Dpy, RootWindow, &attr);
Cmap = attr.colormap;

Deux façons de procéder :

premier exemple, on spécifie la couleur par les trois composantes

 
Sélectionnez
xcol.red = (255) << 8;
xcol.green = (0) << 8;
xcol.blue = (0) << 8;

if (!XAllocColor(Dpy, Cmap, &xcol))
    pixel = xcol.pixel;
else
    printf("Table de couleur système pleine\n");

la deuxième façon d'allouer la couleur est de l'appeler par son nom avec la fonction XAllocNamedColor(), où l'on spécifie une entrée du fichier /usr/lib/X11/lib/rgb.txt.

II-D-2. Visuals et codage vidéo

X est conçu pour manipuler des frame-buffers sous forme bitmap. Dans le plus simple des displays, le noir et blanc, il n'y a qu'un bit par pixel. Pour gérer les displays couleur ou monochrome (nuances de gris), plusieurs bits sont nécessaires pour coder un pixel. Il existe trois façons d'interpréter la valeur d'un pixel.

II-D-2-a. Codages du frame-buffer
II-D-2-a-i. Codage en composantes statiques

Dans les cartes graphiques capables d'afficher 16 millions de couleurs (24 bits sont nécessaires), la valeur du pixel est composée de trois composantes R, V, B, c'est la méthode appelée TrueColor. Chaque composante s'étend de 0 à 255 (un octet non signé, donc 3*8 bits=24 bits).

Image non disponible
Figure 4 : codage truecolor.

Les cartes graphiques monochromes à nuances de gris codent la luminosité du point. Il n'y a qu'une seule composante (mode StaticGray).

II-D-2-a-ii. Codage couleur par colormap

Exiger trois octets pour chaque pixel demande une quantité de mémoire Frame-buffer assez grande pour la résolution des moniteurs courants. On exploite souvent la deuxième solution qui consiste à n'utiliser que huit bits (c'est un exemple) par pixel. Dans ce cas, la valeur d'un pixel n'exprime pas directement une couleur. C'est un index dans un tableau où sont stockées des couleurs. Par exemple pour un display huit bits, le frame-buffer est un champ 2D d'indices dans une table de couleurs.

Image non disponible
Figure 5 : codage par colormap.

Cette table porte le nom de colormap ou encore LUT (Loo Up Table).

Dans le dernier cas, on limite volontairement le nombre des couleurs disponibles (256 couleurs à trois composantes dans le cas d'un display 8 bits), mais on a besoin de trois fois moins de mémoire. Si l'on a le droit de changer les entrées (les trois composantes) de la colormap, on est en présence d'un visuel PseudoColor. Si les entrées sont fixes (fixées par le hardware), c'est un visuel StaticColor.

II-D-2-a-iii. Codage par composantes remappées

Ce codage est très proche de celui présenté au chapitre II.D.2.a.iCodage en composantes statiques. Sa nécessité tient essentiellement à deux faits :

  • on peut vouloir recalibrer les composantes logiciellement ;
  • on veut pouvoir faire un inverse vidéo sur un écran rapidement.

Pour l'inverse vidéo, le codage par palette est bien adapté : il suffit d'inverser les composantes de chaque entrée dans la colormap (256 opérations pour un display 8 bits). Avec le codage en composantes statiques, il est nécessaire d'inverser la valeur de tous les pixels (millions d'opérations).

Le codage par composantes remappées propose trois composantes par pixel. Mais chaque composante est un index dans une table. Ce n'est pas une table de couleurs, mais en fait trois tables séparées.

Image non disponible
Figure 6 : mode DirectColor.

Finalement, les trois tables de mapping forment une seule colormap (avec des entrées à trois composantes), mais chaque composante indexe seulement son canal. C'est le mode DirectColor.

Dans le cas d'un display à niveau de gris (une composante et une table monocomposante), ce mode se nomme GrayScale.

II-D-2-a-iv. Résumé pour les modes sans colormap

En ce qui concerne les modes sans colormap, on trouve le mode monochrome (nuances de gris) qui code la luminosité sur 1 (noir et blanc), 2 (4 niveaux), 4 (16 niveaux) ou 8 bits (256 niveaux). Le mode truecolor gère la couleur sans colormap. La précision du codage se fait généralement entre 12 (16 niveaux par composantes R, G et B) et 24 bits (8 bits soit 256 niveaux par composante). Le 24 bits est le plus répandu. Une autre déclinaison est également utilisée, c'est le 16 bits qui code deux composantes sur 5 bits (32 niveaux) et la troisième sur 6 bits. Il est équivalent au fameux mode 65 535 couleurs de Windows.

II-D-2-a-v. Résumé pour les modes avec colormap

La taille de la LUT est définie par le nombre de bitplanes disponibles.

Généralement on a 8 bits et 256 couleurs. Les couleurs de la LUT sont définies sur trois composantes R,V et B si l'on est en couleur (8 bits pour chacune généralement), mais ce peut être une seule composante si l'on travaille en monochrome.

II-D-2-a-vi. Résumé des résumés : les visuals

On sent bien avec ces exemples, la complexité de la gestion de la couleur pour X. On attribue des noms précis à ces classes de gestion de la couleur.

Colormap Lecture/Écriture Lecture seule
Monochrome/Gris GrayScale StaticGray
Colormap PseudoColor StaticColor
Décomposition DirectColor TrueColor
II-D-2-b. Comment X décrit le support couleur avec les Visuals

Un visual décrit les caractéristiques d'une colormap destinée à être utilisée sur un type d'écran donné. Pratiquement, il s'agit d'un pointeur sur une structure de type Visual qui contient des informations concernant un moyen d'utiliser un écran donné.

Il est nécessaire de spécifier un visual lors de la création d'une colormap ou de la création d'une fenêtre, et le même visual doit être utilisé lors de ces deux créations si la colormap va être associée à la fenêtre. La plupart des fenêtres héritent du visual ; bien souvent, lors de la création d'une fenêtre on utilise la macro DefaultVisual() qui retourne le visual de la RootWindow, et donc on utilise la colormap par défaut. Une fenêtre créée avec XCreateSimpleWindow() utilise la colormap par défaut.

La structure Visual est opaque. On ne peut accéder aux informations qu'elle renferme directement.

Rappel : un visual décrit une manière d'utiliser la couleur sur un écran, mais un écran peut supporter plusieurs visuals.

Par exemple, sur un système couleur, on peut utiliser à la fois un visual monochrome et un visual couleur. Obtenir des informations concernant les visuals supportés par un écran.

Deux fonctions sont disponibles : XMatchVisualInfo() et XGetVisualInfo(). Elles renvoient une structure de type XVisualInfo qui est publique (contrairement à la structure Visual qui est opaque).

Le champ class de la structure XVisualInfo correspond à une des six classes possibles : DirectColor, GrayScale, PseudoColor, StaticColor, StaticGray et TrueColor.

On peut connaître les visuals supportés par un écran donné à l'aide du programme xdpyinfo.

En pratique, on utilisera le plus souvent le DefaultVisual() qui est celui qui correspond le plus souvent aux besoins usuels et qui exploite au mieux le hardware dont on dispose.

Les visuals DirectColor, GrayScale et PseudoColor ont des colormaps modifiables (Read/Write).

Les visuals StaticColor, StaticGray et TrueColor ont des colormaps immuables (Read-Only).

Les colormaps modifiables (Read/Write) ont deux types de colorcells :

  • Read/Write : la couleur de la colorcell peut être changée à n'importe quel moment par le client qui l'a allouée. Elle ne peut être partagée ;
  • Read-Only : allouée une fois par un client, peut être partagée, mais plus modifiée.

Avantages des colormaps immuables :

  • elles peuvent être partagées ;
  • le calcul direct de la pixel value est possible, sans passer par le serveur, puisque la correspondance entre la pixel value et la couleur est prévisible (il suffit de connaître la colormap).

Désavantages des colormaps immuables :

  • la couleur désirée n'est peut-être pas disponible. Dans ce cas, impossible de la créer : les colorcells en Read-Only uniquement.

Avantages des colormaps modifiables :

  • on peut avoir à la fois des colorcells immuables et modifiables ;
  • c'est ce qui rend les visuals PseudoColor et DirectColor les plus utiles.

Désavantages des colormaps modifiables :

  • les colorcells en Read/Write ne sont pas partageables.

REMARQUE : la colormap par défaut contient des colorcells en Read-Only et en Read-Write. Si une application ne trouve pas dans cette colormap toutes les couleurs dont elle a besoin, elle pourra modifier, si elles sont disponibles, les colorcells en Read/Write. Il faut néanmoins remarquer que ces colorcells ne seront plus disponibles pour les autres applications tant qu'elles ne seront pas libérées. Dans ce cas, la seule solution pour l'application en manque de couleurs consiste à allouer une nouvelle colormap (une colormap privée). C'est ce que font certaines applications comme XV.

II-D-3. Les pixmaps

II-D-3-a. Introduction

Nous avons déjà parlé dans les premiers chapitres de ce cours de Pixmaps. Il s'agit en effet de « l'autre » drawable (avec les objets de type Window) dans lequel on peut utiliser les fonctions de dessin de la Xlib comme

 
Sélectionnez
XDrawLine(display, drawable, gc, x1, y1, x2, y2)

Un pixmap est un buffer mémoire qui peut être assimilé à une fenêtre graphique virtuelle. On peut dessiner dedans, copier son contenu dans une fenêtre, dans un autre pixmap, effacer son contenu, etc.

Les pixmaps ont plusieurs utilités :

  • optimiser la gestion des événements Expose. Si on écrit une application qui affiche des images, il suffit de lire l'image dans un Pixmap et copier le contenu du Pixmap dans une fenêtre pour afficher l'image. Mieux, X11 permet d'éviter de redessiner totalement une fenêtre si seulement une partie de celle-ci doit être rafraîchie : chaque événement Expose contient les coordonnées du rectangle à redessiner. Il suffit dans ce cas de recopier la zone correspondante du pixmap vers la fenêtre ;
  • faire de l'animation avec double buffer, sans scintillements. On dessine dans le Pixmap, on recopie dans la fenêtre, on redessine, on recopie, etc. C'est comme au cinéma ! ;
  • créer une fenêtre ayant un motif en fond (attribut background_pixmap des fenêtres) ;
  • faire du remplissage avec des motifs. Nous allons voir qu'à l'aide des Pixmaps et des contextes graphiques il est possible de remplir un rectangle contenant par exemple un damier (avec la fonction XfillRectangle(display, drawable, gc, x, y, width, height) tout simplement) ;
  • sauvegarder sur disque le contenu d'une fenêtre.
II-D-3-b. Mettre un bitmap en fond d'une fenêtre

Rappel : un bitmap est en noir et blanc, un pixmap peut comporter de nombreuses couleurs.

Pour créer un bitmap, le plus simple est d'utiliser le programme bitmap.

Ce programme permet de dessiner à l'aide d'outils très simples des motifs ou des curseurs et de les sauvegarder dans un fichier.

Image non disponible
Figure 7 : le programme bitmap.

Par la suite on pourra soit lire directement le fichier à l'aide de la fonction XReadBitmapFile() soit l'inclure directement dans le code avec un #include (déconseillé). Dans tous les cas, pour les manipuler, il faudra stocker leur contenu dans une variable de type Pixmap.

La Xlib ne fournit pas de fonction permettant de lire des motifs comportant plus de deux couleurs. Pour cela il faudra recourir à des bibliothèques spécialisées comme la bibliothèque Xpm ou la bibliothèque ImageMagick.

II-D-3-b-i. Premier exemple : le bitmap est lu dans un fichier
 
Sélectionnez
#include <stdio.h>

#include <X11/Xlib.h>
#include <X11/Xutil.h>

Display *dpy;
int screen;
Window root;
Visual *visual;
int depth;
int fg, bg;

main()
{
    Window win;
    XSetWindowAttributes xswa;
    XEvent report;
    
    GC gc;
    XGCValues gcvalues;
    
    Pixmap motif;
    Pixmap motif_8bits;
    int val;
    
    int motif_width, motif_height,x_hot, y_hot;
    
    if((dpy=XOpenDisplay(NULL))==NULL) {
        fprintf(stderr,"fenetre de base: ne peut pas se connecter ");
        fprintf(stderr,"au serveur %s\n",XDisplayName(NULL));
        exit(-1);
    }
    
    /* Initialisation des variables standards */
    screen = DefaultScreen(dpy);
    root = DefaultRootWindow(dpy);
    visual = DefaultVisual(dpy, screen);
    depth = DefaultDepth(dpy,screen);
    fg = BlackPixel(dpy, screen);
    bg = WhitePixel(dpy, screen);
    
    /* Lecture du motif de 1 bit de profondeur (bitmap) */
    val = XReadBitmapFile(dpy, root,
        "motif.xbm", /* Nom du fichier */
        &motif_width, &motif_height, /* largeur et hauteur */
        &motif, /* le pixmap */
        &x_hot, &y_hot); /* le "hot spot */
    switch(val) {
        case BitmapFileInvalid:
            fprintf(stderr, "le fichier motif.xbm n'est pas un bitmap valide\n");
            break;
        case BitmapOpenFailed:
            fprintf(stderr, "le fichier motif.xbm n'a pu être lu\n");
            break;
        case BitmapNoMemory:
            fprintf(stderr, "Pas assez de mémoire pour lire le fichier motif.xbm\n");
            break;
        case BitmapSuccess:
            fprintf(stderr, "Fichier motif.xbm lu avec succes. Taille = %d %d, hotspot = %d %d\n",
            motif_width, motif_height, x_hot, y_hot);
            break;
    }
    
    /* Création d'un second pixmap de la même profondeur */
    motif_8bits = XCreatePixmap(dpy,
        root,
        motif_width, motif_height,
        depth);
    
    /* La fonction suivante a besoin d'un GC */
    gcvalues.foreground = fg;
    gcvalues.background = bg;
    gc = XCreateGC(dpy, RootWindow(dpy, screen),
        (GCForeground | GCBackground), &gcvalues);
    
    /* On copie le bitmap dans le pixmap_8 bits */
    val = XCopyPlane(dpy, motif, motif_8bits, gc, 0, 0, motif_width,
        motif_height, 0, 0, 1);
    /* attributs de la fenêtre. */
    /* Le motif en fond. INCOMPATIBLE avec background_pixel !!!*/
    xswa.background_pixmap = motif_8bits;
    /* Les événements */
    xswa.event_mask = ExposureMask | ButtonPressMask | 
        ButtonReleaseMask | PointerMotionMask | KeyPressMask;
    /* Couleur de la bordure */
    xswa.border_pixel = fg;
    win = XCreateWindow(dpy, root,
        100, 100, 500, 500, 3,
        depth,
        InputOutput,
        visual,
        CWEventMask|CWBorderPixel|CWBackPixmap,
        &xswa);
    XMapWindow(dpy,win);
    
    /* On libère les pixmaps car maintenant ils sont dans la mémoire du serveur X11 */
    XFreePixmap(dpy, motif);
    XFreePixmap(dpy, motif_8bits);
    
    while(1) {
        XNextEvent(dpy, &report);
        ...
    }
}

Ce qu'il faut retenir de ce petit exemple :

  • Un bitmap = deux couleurs. Pour le lire, il faut utiliser la fonction XReadBitmapFile() qui alloue et initialise un Pixmap d'un bit de profondeur. Pour pouvoir l'utiliser comme fond d'une fenêtre n'ayant pas la même profondeur, il faut recopier ce pixmap dans un autre pixmap ayant la même profondeur que la fenêtre. On utilise pour cela la fonction XCopyPlane() en positionnant le dernier paramètre à 1 (un seul bitplane à copier).
  • Ne pas oublier de libérer les Pixmaps alloués si on n'en a plus besoin. Sitôt la fenêtre créée, ses attributs sont dans la mémoire du serveur. On libère donc les Pixmaps juste après à l'aide de XfreePixmap().

Si vous positionnez en plus l'attribut background_pixel de la fenêtre, l'attribut background_pixmap ne sera pas pris en compte. Ces deux attributs sont exclusifs, mais rien ne vous empêche de les positionner. Faites attention, car c'est le background_pixel qui gagne !

II-D-3-b-ii. Deuxième exemple : le bitmap est inclus dans le source
 
Sélectionnez
#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include "motif.xbm"
...
main()
{
    ...
    if((dpy=XOpenDisplay(NULL))==NULL) {
        fprintf(stderr,"fenetre de base: ne peut pas se connecter ");
        fprintf(stderr,"au serveur %s\n",XDisplayName(NULL));
        exit(-1);
    }
    
    /* Initialisation des variables standards */
    screen = DefaultScreen(dpy);
    ...
    motif = XCreateBitmapFromData(dpy, root,
        motif_bits, motif_width, motif_height);
    
    /* Creation d'un second pixmap de la meme profondeur */
    motif_8bits = XCreatePixmap(dpy,
        root,
        motif_width, motif_height,
        depth);

Quelques remarques cependant :

  • motif_bits, motif_width et motif_height sont maintenant des macros qu'il ne faut pas déclarer. Elles proviennent du fichier motif.xbm qui est inclus. Voici à quoi il ressemble :
 
Sélectionnez
static unsigned char motif_bits[] = {
    0xff, 0x01, 0xff, 0x01, 0xff, 0x01, 0xff, 0x01, 0xff, 0x01, 0xff, 0x01,
    0xff, 0x01, 0xff, 0xff, 0xff, 0xff, 0x80, 0xff, 0x80, 0xff, 0x80, 0xff,
    0x80, 0xff, 0x80, 0xff, 0x80, 0xff, 0x80, 0xff
};
  • l'intérêt principal de cette méthode est qu'elle facilite la distribution de programmes compilés. Je la déconseille dans tous les autres cas.
II-D-3-c. Utiliser un bitmap comme motif de remplissage

Une fois le pixmap alloué et initialisé (s'assurer qu'il a la même profondeur que la fenêtre, comme dans les deux exemples précédents), il est très facile de l'utiliser comme motif pour dessiner. Il suffit pour cela de créer un GC avec les attributs suivants positionnés :

  • fill_style doit valoir FillTiled (d'autres subtilités sont disponibles en le positionnant avec les valeurs FillStippled ou FillOpaqueStippled, voir le manuel) ;
  • tile doit être initialisé avec l'ID du pixmap.

… ou de modifier un GC existant à l'aide des fonctions XsetFillStyle() et XSetTile().

II-D-3-c-i. Exemple d'utilisation d'un motif pour dessiner
 
Sélectionnez
#include <stdio.h>

#include <X11/Xlib.h>
#include <X11/Xutil.h>

...

main()
{
    ...
    /* Création d'un second pixmap de la même profondeur */
    motif_8bits = XCreatePixmap(dpy,
        root,
        motif_width, motif_height,
        depth);
    gcvalues.foreground = fg;
    gcvalues.background = bg;
    
    gc = XCreateGC(dpy, RootWindow(dpy, screen),
        (GCForeground | GCBackground), &gcvalues);
        
    /* On copie le bitmap dans le pixmap_8 bits */
    val = XCopyPlane(dpy, motif, motif_8bits, gc, 0, 0, motif_width,
        motif_height, 0, 0, 1);
    ...
    /* Création de la fenêtre, etc. */
    ...
    
    XSetFillStyle(dpy, gc, FillTiled);
    XSetTile(dpy, gc, motif_8bits);
    XMapWindow(dpy,win);
    XFreePixmap(dpy, motif);
    XFreePixmap(dpy, motif_8bits);
    
    while(1) {
        XNextEvent(dpy, &report);
        switch (report.type) {
            case Expose:
                /* traitement des événements de type Expose */
                break;
            case ButtonPress:
                /*Quand on clique on dessine un rectangle rempli avec le motif */
                XFillRectangle(dpy, win, gc, report.xbutton.x,
                report.xbutton.y, 100, 100);
                break;
            case KeyPress:
                XCloseDisplay(dpy);
                exit(0);
                /* traitement des événements de type KeyPress */
                break;
            case ConfigureNotify:
                /* traitement des événements de type ConfigureNotify */
                break;
        }
    }
}
II-D-3-d. Utilisation d'un pixmap pour faire du double buffering

Pas grand-chose à expliquer, car la méthode est très simple : on dessine dans un pixmap (et non pas dans la fenêtre), puis on copie d'un coup le pixmap dans la fenêtre.

Le petit exemple ci-dessous montre un exemple d'utilisation d'un Pixmap en tant que double buffer. Il permet en outre de comparer la même animation (cent cercles concentriques qui avancent en diagonale à partir de la position cliquée) avec et sans double buffer.

Image non disponible
Figure 8 : le programme d'exemple du double buffer.
II-D-3-d-i. Programme d'exemple
 
Sélectionnez
#include <stdio.h>

#include <X11/Xlib.h>
#include <X11/Xutil.h>

#include "motif.xbm"

Display *dpy;
int screen;
Window root;
Visual *visual;
int depth;
int fg, bg;

main()
{
    Window win;
    int width = 500, height = 500;
    XSetWindowAttributes xswa;
    XEvent report;
    
    GC gc;
    XGCValues gcvalues;
    
    Pixmap db;
    int i,j;
    
    if((dpy=XOpenDisplay(NULL))==NULL) {
        fprintf(stderr,"fenetre de base: ne peut pas se connecter ");
        fprintf(stderr,"au serveur %s\n",XDisplayName(NULL));
        exit(-1);
    }
    
    /* Initialisation des variables standards */
    screen = DefaultScreen(dpy);
    root = DefaultRootWindow(dpy);
    visual = DefaultVisual(dpy, screen);
    depth = DefaultDepth(dpy,screen);
    fg = BlackPixel(dpy, screen);
    bg = WhitePixel(dpy, screen);
    
    /* Création du double buffer */
    db = XCreatePixmap(dpy, root, width, height, depth);
    gcvalues.foreground = fg;
    gcvalues.background = bg;
    gc = XCreateGC(dpy, RootWindow(dpy, screen),
        (GCForeground | GCBackground), &gcvalues);
        
    /* Attributs de la fenêtre. */
    /* Le motif en fond */
    xswa.background_pixel = bg;
    
    /* Les événements */
    xswa.event_mask = ExposureMask | ButtonPressMask | ButtonReleaseMask |
        PointerMotionMask | KeyPressMask;
        
    /* Couleur de la bordure et du background */
    xswa.background_pixel = bg;
    xswa.border_pixel = fg;
    win = XCreateWindow(dpy, root,
        100, 100, 500, 500, 3,
        depth,
        InputOutput,
        visual,
        CWEventMask|CWBorderPixel|CWBackPixel,
        &xswa);
        
    XMapWindow(dpy,win);
    
    while(1) {
        XNextEvent(dpy, &report);
        switch (report.type) {
            case Expose:
                /* traitement des événements de type Expose */
                break;
            case ButtonPress:
            switch(report.xbutton.button) {
                case Button1:
                    /* Animation avec double buffer */
                    for(j=0; j < 100; j++) {
                        XSetForeground(dpy, gc, bg);
                        XFillRectangle(dpy, db, gc, 0, 0, width, height);
                        XSetForeground(dpy, gc, fg);
                        
                        for(i = 0; i < 400; i+=4)
                            XDrawArc(dpy, db, gc, report.xbutton.x+j, report.xbutton.y+j,
                                i,i, 0, 23040);
                                
                        XCopyArea(dpy, db, win, gc, 0, 0, width, height, 0, 0);
                        /* on force le rafraîchissement de l'écran */
                        XFlush(dpy);
                    }
                    break;
                case Button2:
                    /* Animation sans double buffer */
                    for(j=0; j < 100; j++) {
                        XClearWindow(dpy, win);
                        
                        for(i = 0; i < 400; i+=4)
                            XDrawArc(dpy, win, gc, report.xbutton.x+j, report.xbutton.y+j,
                                i,i, 0, 23040);
                    }
                    break;
            }
            break;
            case KeyPress:
                XCloseDisplay(dpy);
                exit(0);
                /* traitement des événements de type KeyPress */
                break;
            case ConfigureNotify:
                /* traitement des événements de type ConfigureNotify */
                break;
        }
    }
}
II-D-3-d-ii. Étude de l'exemple
  • Pour réaliser une animation en double buffer, on commence par effacer le contenu du double buffer en dessinant sur la totalité du pixmap un rectangle de la couleur du fond. Ensuite, on effectue le dessin proprement dit, non pas dans la fenêtre, mais directement dans le pixmap :
 
Sélectionnez
XDrawArc(dpy, db, gc, report.xbutton.x+j, report.xbutton.y+j, eport.xbutton.y+j i, i, 0, 23040);
  • On recopie le pixmap dans la fenêtre :
 
Sélectionnez
XCopyArea(dpy, db, win, gc, 0, 0, width, height, 0, 0);
  • On force le serveur X11 à « flusher », c'est-à-dire à exécuter toutes les requêtes qui sont dans sa pile. N'oublions pas qu'habituellement les requêtes sont bufferisées. C'est l'équivalent du fflush() de la bibliothèque standard stdio ;
 
Sélectionnez
XFlush(dpy);
  • Dans le cas normal, on efface la fenêtre, et on dessine directement dans la fenêtre. Le dessin étant très long, on a presque le temps de voir les cercles se dessiner. L'effet est désastreux.
II-D-3-e. Optimisation des Expose

Si on dessine directement dans un Pixmap cela permet de gérer facilement le rafraîchissement de la fenêtre. En effet, si cette dernière est recouverte par une autre fenêtre puis découverte, il est très facile de la rafraîchir puisqu'on dispose d'une copie de son contenu dans le pixmap.

II-D-3-e-i. Méthode « tout-en-un »

Si l'on n'est pas à la recherche de performances ou si l'application n'est pas très gourmande en ressources, le plus simple est de recopier intégralement le contenu du pixmap dans la fenêtre, comme nous l'avons fait pour dans l'exemple du double buffer.

 
Sélectionnez
...
while(1) {
    XNextEvent(dpy, &report);
    switch (report.type) {
        case Expose:
            /* On efface tous les Expose de la pile */
            while(XCheckTypedEvent(dpy, Expose, &report));
            XCopyArea(dpy, db, win, gc, 0, 0, width, height, 0, 0);
            /* On force le rafraîchissement de l'écran */
            XFlush(dpy);
            break;
        case ButtonPress:
            ...

Puisqu'on recopie l'intégralité du pixmap dans la fenêtre, il faut purger les événements Expose qui traînent encore dans la pile. Inutile de redessiner plusieurs fois la même chose !

Rappelons que plusieurs événements de type Expose peuvent être générés dans le cas d'une modification de la taille de la fenêtre ou d'un recouvrement partiel multiple, un événement Expose est généré pour chaque partie rectangulaire à redessiner.

Pour supprimer des événements d'un type donné de la pile des événements, on utilise la fonction XCheckTypedEvent().

II-D-3-e-ii. Méthode « normale »

On traite les Expose un à un en ne redessinant que les parties rectangulaires correspondantes.

 
Sélectionnez
...
while(1) {
    XNextEvent(dpy, &report);
    switch (report.type) {
            case Expose:
            XCopyArea(dpy, db, win, gc,
            report.xexpose.x, report.xexpose.y,
            report.xexpose.width, report.xexpose.height,
            report.xexpose.x, report.xexpose.y);
            break;
        case ButtonPress:
            ...

C'est effectivement plus simple, mais n'oublions pas que dans ce cas, on va passer plusieurs fois dans le case dans le cas d'exposition multiple.

II-E. Les images

II-E-1. Introduction

Les images sont représentées sous X11 par une structure de type Ximage :

 
Sélectionnez
typedef struct _XImage
{
    int width, height; /* size of image */
    int xoffset; /* number of pixels offset in X direction */
    int format; /* XYBitmap, XYPixmap, ZPixmap */
    char *data; /* pointer to image data */
    int byte_order; /* data byte order, LSBFirst, MSBFirst */
    int depth; /* depth of image */
    ...
    int bytes_per_line; /* accelarator to next line */
    int bits_per_pixel; /* bits per pixel (ZPixmap) */
    int (*put_pixel)();
    struct _XImage *(*sub_image)();
} XImage;

Il ne faut pas confondre Pixmap et XImage. Le premier est un identifieur d'une zone mémoire dans la mémoire du serveur (l'équivalent d'une fenêtre), alors qu'une XImage est une zone de mémoire dans le client où sont stockées des informations.

À la différence des Pixmaps qui se trouvent dans la mémoire du serveur, les XImages sont dans la mémoire du client (ou mieux, nous allons voir !).

Ainsi, on peut les manipuler directement à coup de memset(), memcopy() et autres *ximage++ !

Rappelons que les Pixmaps ne peuvent être manipulés qu'au travers de fonctions Xlib et donc au travers du protocole X11.

On peut allouer une XImage dans une zone de mémoire partagée directement visible par le serveur X.

II-E-2. Programmation

Pour visualiser une XImage il faut recopier son contenu dans un drawable (Pixmap, Window). Xlib fournit deux fonctions d'interfaçage entre drawables et Images : XGetImage() et XPutImage().

Une XImage est un champ 2D de valeurs. Dans le cas des visuels à 256 couleurs, ces valeurs sont des indices dans la colormap.

Il existe trois formats de Ximage :

  • XYBitmap : la zone de mémoire décrite par une Ximage de ce type est en fait un plan de bits (bitplane). L'état (allumé ou éteint) du pixel est donné par la position du bit concerné ;
  • XYPixmap : la zone mémoire de ce mode est une concaténation de plusieurs bit-planes (8), ceci ressemble au codage des frame-buffers ;
  • ZPixmap : le plus couramment utilisé, c'est en fait un mode chunky-pixel (voir annexes). Un octet de la zone mémoire est un index dans la colormap.

Concrètement, il faut allouer une XImage avant de l'utiliser. Pour ceci on utilise la fonction :

 
Sélectionnez
XImage *XCreateImage( Display *display, Visual *visual, 
    unsigned int depth, int format, int offset, 
    char *data, unsigned int width, unsigned int height, 
    int bitmap_pad, int bytes_per_line)

Un exemple :

 
Sélectionnez
XImage *ximage;
char * Data;
int nppl=512, nl=512;
/*
Data=malloc(nppl*nl);
fread(data,…,file);/* chargement d'une image brute */
ximage = XCreateImage(Dpy, visual, 8,ZPixmap,0,Data,nppl, nl,8, 0);

Les arguments Dpy et visual sont classiques. Des renseignements sur la taille et la profondeur de l'image sont spécifiés. Attardons-nous sur l'argument Data. Data pointe sur une zone de mémoire linéaire (de taille nppl*nl). La structure XImage est donc une structure de contrôle du buffer d'octets (Data) que l'on passe en paramètre.

Cette image est affichée (mémoire copiée de la mémoire du client vers la zone mémoire de la fenêtre dans l'espace mémoire du serveur). La fonction XPutImage() réalise ce transfert.

II-E-2-a. Autres fonctions d'exploitation des XImages
  • XGetImage(…) et XgetSubImage(…) remplissent une XImage à partir d'un drawable (window ou pixmap).
  • XPutImage(display, image, drawable, x_src, y_src, x_dest, y_dest, width, height) : recopie une XImage dans un drawable. Nécessaire pour rendre une XImage visible.
  • XDestroyImage(ximage) : libère la mémoire allouée pour l'XImage. Attention, si la XImage a été créée avec XCreateImage, XGetImage ou XGetSubImage, cette fonction libère à la fois le buffer et la structure d'encapsulation.
  • XGetPixel(ximage, x, y) et XPutPixel(ximage, x, y, pixel_value) : macros permettant de lire ou d'écrire un pixel dans une XImage. XGetPixel() renvoie la pixelvalue du point passé en paramètre, XPutPixel() permet de colorer un pixel avec une pixel_value.
II-E-2-b. Exemples
II-E-2-b-i. Allocation d'une image
 
Sélectionnez
/* Alloue une XImage de width pixels de large * height pixels de haut */
char *buffer = NULL;
XImage *XIma = NULL;
/* On alloue donc le buffer qui va contenir les graphiques manuellement à l'aide d'un malloc
*/
if (buffer=(char *) malloc(width * height*sizeof(char))
{
    perror("malloc failed \n");
    exit(0);
}
/* Allocation d'une XImage à partir du buffer, plus rapide pour le dessin et l'affichage qu'un
Pixmap */
XIma=XCreateImage(display, visual ,8, ZPixmap ,0 ,(char *) buffer, width,height,8,0);
...
/* On remplit avec la pixel value 0 le point (x, y) */
buffer[x*y+x] = 0;
/* On affiche le buffer dans une fenêtre */
XPutImage(dpy, win, gc, XIma, 0, 0, 0, 0, width, height);
II-E-2-b-ii. Commentaire

Dans cet exemple, on alloue d'abord un buffer standard puis on l'encapsule dans une XImage. Le buffer est utilisé directement pour dessiner :

 
Sélectionnez
buffer[x*y+x] = 0;

On aurait pu aussi bien utiliser la fonction XPutPixel() de la manière suivante.

 
Sélectionnez
XPutPixel(XIma, x, y, 0);

Je pense que les deux écritures sont strictement équivalentes, cependant il est avantageux d'attaquer directement le buffer dans le cas d'utilisation de fonctions de type memset() ou memcpy(), utiles pour remplir des zones de mémoire rapidement.

Enfin, l'affichage se fait en recopiant la XImage dans une fenêtre :

 
Sélectionnez
XPutImage(dpy, win, gc, XIma, 0, 0, 0, 0, width, height);

II-F. Annexes

II-F-1. Codage de Frame-Buffer

II-F-1-a. Mode bitmap

Dans le mode bitmap, la valeur du pixel est exprimée par un empilement de bitplanes dans le frame-buffer.

Image non disponible
Figure 9 : codage d'un pixel en mode bitmap.

Ce mode bitmap possède plusieurs atouts. D'une part, il est complètement dynamique du point de vue de l'allocation de la mémoire vidéo. En effet, si on veut disposer de seulement 32 couleurs, 5 plans suffisent. Un octet dans un bitplane code une partie des valeurs de 8 pixels contigus sur une ligne. D'autre part, plusieurs effets sont possibles sans trop d'efforts (suppression de certains plans, mélange de plans).

Dans l'exemple de la Figure 9, pour coder un mode truecolor, il faudrait 24 bitplanes.

Néanmoins, une image vidéo composée de bitplanes a forcément une largeur multiple de 8 (très petit inconvénient).

Le deuxième inconvénient est le fait que pour accéder à la valeur d'un point, il faut aller consulter 1 bit dans plusieurs bitplanes.

Ce codage de frame-buffer est le plus couramment utilisé.

II-F-1-b. Mode chunky-pixel

Ce mode de codage des valeurs de frame-buffer est utilisé dans les consoles de jeux vidéo (très peu paramétrables).

Ici, le frame buffer est un champ 2D de valeurs. Alors qu'en mode bitmap, un octet codait une partie de 8 pixels contigus, le mode chunky-pixel propose un octet/un pixel.

Cela peut paraître plus naturel, mais ce mode est peu répandu.

Image non disponible
Figure 10 : codage chunky-pixel.

L'inconvénient de ce mode réside dans le fait que si l'on n'a pas un display 8 bits (un octet pour un pixel), une valeur peut être « à cheval » sur deux octets physiques en mémoire.


précédentsommairesuivant
Il est très important de comprendre ceci.
Comportement géré par le window-manager.

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2005 Michel Buffa, Franck Diard, François Abram. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.