Construire un widget

 

Cet exemple présente l'essentiel pour construire un 'widget' tk nommé 'demo', avec le C et la Xlib. Ce widget présente un texte, un rectangle et une image.


Le script tcl

 package require Img
 load libdemo.so
 frame .fr
 demo .fr.d -text "me voici"
 pack .fr .fr.d -fill both -expand 1
 set img [image create photo -file map.xpm ]
 .fr.d configure -image $img
 .fr.d configure -text "me voici avec une image" -couleur #0088FF

Le code

 #include <stdio.h>
 #include <stdlib.h>
 #include <tcl.h>
 #include <tk.h>

Inutile d'inclure <X11/Xlib.h>, même pour utiliser directement les fonctions de la Xlib

 // caractéristiques du 'widget'.
 typedef struct {
   Tk_Window tkwin;		/* Fenêtre du widget. */
   Display *display;		/* canal d'affichage. */
   Tcl_Interp *interp;		/* Interpréteur associé au widget. */
   Tk_OptionTable optionTable;	/* options  */
   Tcl_Obj *imagePtr;		/* Valeur de l'option -image  */
   Tk_Image image;		/* Dérivé de imagePtr par appel à Tk_GetImage */
   XColor *couleur;		/* Valeur de l'option -couleur */
   Tcl_Obj *textePtr;            /* Valeur de l'option -text  */
 } Demo;

Les caractéristiques de chaque instance d'un même widget sont mémorisées dans cette structure et passées dans les fonctions par l'argument 'clientData'.

 // table des options
 static Tk_OptionSpec optionSpecs[] = {
  {TK_OPTION_STRING, "-text", "text", "Text",
     "Bonjour",  Tk_Offset(Demo, textePtr), -1,0, 0, 0},
  {TK_OPTION_COLOR, "-couleur", "couleur", "Couleur",
     "#00AA00", -1, Tk_Offset(Demo, couleur), 0, 0, 0},
  {TK_OPTION_STRING, "-image", "image", "Image",
      (char *) NULL, Tk_Offset(Demo, imagePtr), -1, TK_OPTION_NULL_OK, 0, 0},
  {TK_OPTION_SYNONYM, "-img", (char *) NULL, (char *) NULL,
      (char *) NULL, 0, -1, 0, (ClientData) "-image"},
  {TK_OPTION_END}  /* fin de la table d'options */
 };

Cette table permet de définir chaque option. Chaque élément contient:

 // Déclaration des fonctions
 int creerWidget (ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[] );
 static void DemoAffiche(ClientData clientData);
 static void DemoObjEventProc(ClientData clientData, XEvent *eventPtr);
 static int DemoWidgetObjCmd( ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj * CONST objv[]);
 static void DemoDeletedProc(ClientData clientData);

 ///////////////////////////////////////////
 int Demo_Init (Tcl_Interp *interp) {
   Tcl_CreateObjCommand (interp, "demo", creerWidget, (ClientData) NULL, (Tcl_CmdDeleteProc *) NULL);
   return TCL_OK;
   }

La bibliothèque est nommée 'libdemo.so' et la fonction Demo_Init est appelée au chargement de celle-ci. Cette fonction crée une nouvelle commande tcl 'demo' correspondant au nom du widget, et à cette commande est associée la fonction 'creerWidget'. On peut utiliser dans le script tcl

demo .fr.d

et cette commande entrainera l'exécution de la fonction 'creerWidget' présentée ci dessous.

 ////////////////////////////////////////////////////////////
 int creerWidget (ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[] )
 //-------------------------------------------------------
  {
  Tcl_Obj *r;
  Tk_Window tkwin;
  Demo *demoPtr;
  Tk_OptionTable optionTable;

  // vérifie présence du 'nom' donné à demo.
  if ( objc < 2) {
    Tcl_WrongNumArgs (interp, 1, objv, "nom ?options?");
    return TCL_ERROR;
    }
  // création de la fenêtre
  tkwin = Tk_CreateWindowFromPath (interp, Tk_MainWindow (interp), Tcl_GetString (objv[1]), NULL);
  Tk_GeometryRequest(tkwin, 250, 150);

  optionTable = Tk_CreateOptionTable(interp, optionSpecs);

  // initialisation du widget
  demoPtr = (Demo *) ckalloc(sizeof(Demo));
  memset((void *) demoPtr, 0, (sizeof(Demo)));
  demoPtr->tkwin = tkwin;
  demoPtr->display = Tk_Display(tkwin);
  demoPtr->interp = interp;
  demoPtr->optionTable	= optionTable;
  demoPtr->imagePtr = NULL;
  demoPtr->image = NULL;

  if (Tk_InitOptions(interp, (char *) demoPtr, optionTable, tkwin) != TCL_OK) {
    Tk_DestroyWindow(demoPtr->tkwin);
    ckfree((char *) demoPtr);
    return TCL_ERROR;
    }

  if (Tk_SetOptions(interp, (char *) demoPtr, optionTable, objc - 2,
	    objv + 2, tkwin, NULL, (int *) NULL) != TCL_OK) {
    Tk_DestroyWindow(demoPtr->tkwin);
    return TCL_ERROR;
    }

  // Pour creer maintenant la fenetre sans attendre la création differée
  Tk_MakeWindowExist (tkwin);

  // Création d'une commande ayant le nom de l'instance du widget
  Tcl_CreateObjCommand(interp, Tk_PathName(demoPtr->tkwin), DemoWidgetObjCmd,
  	    (ClientData) demoPtr, DemoDeletedProc);

  // événements
   Tk_CreateEventHandler(demoPtr->tkwin, ExposureMask|StructureNotifyMask,
	    DemoObjEventProc, (ClientData) demoPtr);

  r = Tcl_DuplicateObj (objv[1]);
  Tcl_SetObjResult (interp, r);
  return TCL_OK;
  }

Cette fonction est appelée à l'exécution de la commande tcl :

demo .fr.d

'objc' donne le nombre d'arguments et 'objv' ces arguments. Dans notre exemple, on a objc=2 objv[0]=demo et objv[1]=.fr.d

- Création de la fenêtre avec Tk_CreateWindowFromPath qui est la procédure normalement utilisée pour un nouveau widget. Le nom de la fenêtre est ici .fr.d

- Initialisation du widget : Les caractéristiques du widget sont placés dans 'demoPtr'. Le canal d'affichage sert à communiquer avec le serveur X. Il est obtenu avec la fonction Tk_Display. La table des options est placée dans la champ 'optionTable' de type Tk_OptionTable. Les options du widget sont mis en place par la fonction Tk_SetOption. Dans notre exemple, les arguments sont -text "me voici". La table des options 'optionSpecs' indique que pour l'option -text, la valeur "me voici" doit être du type 'chaine de caractères (string), et cette valeur est placée dans demo->textePtr. Tk_SetOption opère ainsi pour toutes les options placées en argument, et selon les instructions données par la table des options 'optionSpecs'.

- Création d'une commande tcl '.fr.d' avec Tcl_CreateObjCommand Pour être en conformité avec la syntaxe des scripts tcl, Le nom de l'instance doit être une commande tcl, afin d'appliquer des commandes au widget, par exemple la commande 'configure'

.fr.d configure -image $img

Cette commande entrainera l'exécution de la fonction 'DemoWidgetObjCmd' présentée ci après. Lorsque cette instance est supprimée, la fonction 'DemoDeletedProc' est appelée. Les caractéristiques du widget sont passées à ces fonctions par 'ClientData' (un pointeur de 'demoPtr').

- Événements : la fonction 'Tk_CreateEventHandler' permet de lancer une procédure à la suite d'un événement. Par exemple lorsque la fenêtre, cachée par une autre en premier plan, redevient visible, il faut alors redessiner le contenu (ou du moins la partie visible). Le deuxième argument est le masque pour selectionner les évenements à prendre en compte, dans notre exemple il s'agit de 'ExposureMask' (la fenetre doit être redessiner) et 'StructureNotifyMask' (modification de la géométrie de la fenêtre), voir la documentation de Xlib pour les autres masques d'événements. La fonction 'DemoObjEventProc' sera appelée dans ces cas. Les caractéristiques du widget sont passées à cette fonction par 'ClientData' (un pointeur de 'demoPtr').

 static int DemoWidgetObjCmd ( ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj * CONST objv[])
  {
  int reponse = TCL_OK;
  int index;
  Tcl_Obj *resultObjPtr;

  // les différentes commandes possibles du widget
  static CONST char *demoCmdes[] = {"cget", "configure", (char *) NULL};
  enum { CGET, CONFIGURE };

  // caractéristiques du widget
  Demo *demoPtr = (Demo *) clientData;
  // verifie presence d'une commande
  if (objc < 2) {
    Tcl_WrongNumArgs(interp, 1, objv, "commande ?arg arg...?");
    return TCL_ERROR;
    }

  // vérifie la validité de la commande, si oui place son rang dans index
  if (Tcl_GetIndexFromObj(interp, objv[1], demoCmdes, "commande", 0, &index) != TCL_OK) {
    return TCL_ERROR;
    }

  switch (index) {
    case CGET: { // traitement de la commande cget
      }
    case CONFIGURE: {
      resultObjPtr = NULL;
      if (objc == 2) {
        resultObjPtr = Tk_GetOptionInfo(interp, (char *) demoPtr,
	  demoPtr->optionTable, (Tcl_Obj *) NULL, demoPtr->tkwin);
	if (resultObjPtr == NULL)  reponse = TCL_ERROR;

      } else if (objc == 3) {
        resultObjPtr = Tk_GetOptionInfo(interp, (char *) demoPtr,
          demoPtr->optionTable, objv[2], demoPtr->tkwin);
        if (resultObjPtr == NULL) reponse = TCL_ERROR;
      } else {
        reponse = Tk_SetOptions(interp, (char *) demoPtr, demoPtr->optionTable, objc - 2, objv + 2,
			demoPtr->tkwin, NULL, (int *) NULL);
        if (reponse == TCL_OK) Tcl_DoWhenIdle(DemoAffiche, (ClientData) demoPtr);
        }
      if (resultObjPtr != NULL) Tcl_SetObjResult(interp, resultObjPtr);

      }
    }
  return reponse;
  }

Cette fonction est appelée à l'exécution de l'instruction tcl:

.fr.d configure -image $img

'objc' donne le nombre d'arguments et objv ces arguments. Dans notre exemple, on a objc=4 objv[0]=.fr.d, objv[1]=configure, objv[2]=-image et objv[3]={valeur de $img} Cette fonction oriente le traitement à faire selon selon la commande donnée en argument à l'instruction tcl.

Seule la commande 'configure' est traitée, mais il est possible d'ajouter toutes autres commandes spécifiques au widget 'demo'. Concernant la commande 'configure' on peut avoir le cas ou objc=2, l'instruction 'pathname configure' retourne les différentes options, et dans le cas ou objc=3, l'instruction 'pathname configure option' retourne les valeurs de l'option indiquée.

Dans le cas ou objc>3 (instruction pathname configure option1 valeur1 option2 valeur2 ...), il faut redessiner le widget avec les nouvelles options. Les nouvelles caractéristiques du widget sont mises en place avec la fonction Tk_SetOptions (de la même manière que dans la fonction 'creerWidget'). C'est la fonction DemoAffiche qui dessine le widget, mais cette fonction est appelée par l'intermédiaire de Tcl_DoWhenIdle afin de différer l'exécution que lorsque cela est nécéssaire. Cela évite de redessiner inutilement plusieurs fois de suite le widget, à la suite de plusieurs évements qui peuvent être contradictoires.

 static void DemoObjEventProc(ClientData clientData, XEvent *eventPtr)
  {
   Demo *demoPtr = (Demo *) clientData;
   if (eventPtr->type == Expose) {
     Tcl_DoWhenIdle(DemoAffiche, (ClientData) demoPtr);
     }
   else if (eventPtr->type == DestroyNotify) {
     Tk_FreeConfigOptions((char *) demoPtr, demoPtr->optionTable, demoPtr->tkwin);
     }
  }

Cette fonction est appelée lors d'un événement défini par Tk_CreateEventHandler lors de la création du widget. L'événement 'Expose' correspondant au masque 'ExposureMask' est traité, avec un appel à la fonction pour redessiner le widget. (L'évenement 'ConfigureNotify' correspondant au masque 'StructureNotifyMask' lors du redimensionnement de la fenêtre n'est pas traité). La libération des ressources utilisées pour les options est effectuée lors de l'événement 'DestroyNotify'.

 static void DemoAffiche(ClientData clientData)
  {
  Window d;
  GC gc;  /* contexte graphique */
  Demo *demoPtr = (Demo *) clientData;
  Tk_Window tkwin = demoPtr->tkwin;
  d = Tk_WindowId(tkwin);
  char *message;

  message=Tcl_GetString(demoPtr->textePtr);

  // contexte graphique selon les options (ici uniquement la couleur)
  XGCValues gcValeurs;
  gcValeurs.function = GXcopy;
  gcValeurs.graphics_exposures = False;
  gcValeurs.font=XLoadFont (demoPtr->display, "-*-helvetica-medium-r-*-*-12-*-*-*-*-*-*-*");
  if (demoPtr->couleur != NULL) gcValeurs.foreground = demoPtr->couleur->pixel;
  gc = Tk_GetGC(tkwin,GCFunction|GCForeground, &gcValeurs);

  XFillRectangle (demoPtr->display, d, gc, 1, 1, 20, 20);
  XDrawString (demoPtr->display, d, gc, 50, 20, message, strlen(message));
  Tk_FreeGC(demoPtr->display, gc);

  Tk_Image image;
  int width, height;
  if (demoPtr->imagePtr != NULL) {
    image = Tk_GetImage(demoPtr->interp, demoPtr->tkwin,
		    Tcl_GetString(demoPtr->imagePtr), NULL,
		    (ClientData) demoPtr);
    Tk_SizeOfImage(image, &width, &height);
    Tk_RedrawImage(image, 0, 0, width, height, d, 0, 40);
    Tk_FreeImage (image);
    }
  }

Voici enfin la fonction qui dessine le widget. Le seul argument est 'clientdata', un pointeur sur les caractéristiques du widget, que l'on récupére dans 'demoPtr'. Le contexte graphique permet de définir les paramétres pour dessiner: la couleur, épaisseur de trait, police de caractères, etc, cela évite une longue liste de paramètres dans les fonctions de dessin. Un contexte graphique est obtenu avec la fonction 'Tk_GetGC' dont les arguments sont: la fenêtre de dessin, le masque des valeurs, les valeurs des paramétres de dessin. Les paramétres de dessin sont placés dans une structure XGCValues, dont voici quelques champs (voir la documentation de référence de la Xlib)

typedef struct { // role de l'attribut - Defaut - masque

} XGCValues;

Dans notre exemple, les valeurs des paramétres de dessin renseignées sont la couleur de dessin (foreground), la fonction de dessin (function) et la police de caractère (font). Les autres valeurs sont celles par défaut. La couleur de dessin est obtenue à partir de l'option 'couleur'. Les valeurs à prendre en compte sont la fonction est la couleur de dessin car le masque est 'GCFunction|GCForeground'.

La fonction Tk_WindowId retourne l'identifiant au type Window de la fenêtre, et permet ainsi d'utiliser les fonctions de dessins de Xlib, tel que XFillRectangle et XDrawString Les images sont obtenues par la commande 'image create photo du script tcl, et transmises par l'option -image du widget. Tk_GetImage retourne une instance de l'image dont le nom est donné par imagePtr, et celle est dessiner par la fonction 'Tk_RedrawImage'.

 static void DemoDeletedProc(ClientData clientData)
  {
  Demo *demoPtr = (Demo *) clientData;
  Tk_Window tkwin = demoPtr->tkwin;

  if (tkwin != NULL) {
    Tk_DestroyWindow(tkwin);
    }
  }

Cette fonction est appelée à la suppression d'une instance du widget demo. La fenêtre de cette instance est donnée par le champ 'tkwin' et supprimée avec la fonction 'Tk_DestroyWindow'.

Pour construire la bibliothèque 'libdemo.so', placer le code dans le fichier demo.c et compiler avec

gcc -shared -o libdemo.so -fpic demo.c

conclusion On voit par cet exemple que l'utilisation des procédures 'tcl/tk' apporte une aide importante à la réalisation de la structure du widget, et il est possible d'utiliser les fonctions natives de 'Xlib' en particulier pour le dessin.

Gérard Mouton


Voir aussi


Catégorie Tutoriel