Un Toolkit X fournit au programmeur de GUI (Graphic User Interface) une boîte à outil comprenant des éléments graphiques (Widgets) et un ensemble d'outils pour gérer leur comportement (fonctions, structures de données, etc...).
Il existe deux niveaux de Toolkits au-dessus de X :
Si l'on utilise correctement les Widgets d'un Toolkit, cela simplifie considérablement la programmation d'une GUI. En outre, toutes les GUI développées avec un même Toolkit auront le même look and feel.
Lorsque nous écrirons des applications avec MOTIF nous aurons en sus des librairies MOTIF, à appeler certaines fonctions de la librairie Xt puisque MOTIF est construit sur cette dernière (il n'est cependant pas nécessaire de connaître en détail les mécanismes de la librairie Xt car MOTIF effectue la plus grande partie du travail d'interfaçage pour nous). Nous devrons utiliser la Xlib pour dessiner ou pour gérer les événements d'une manière plus fine que celle proposée par MOTIF.
Remarque : MOTIF a été développé par Open Software Foundation (OSF) et le nom complet de MOTIF est en fait OSF/MOTIF.
Dans cette section, nous allons détailler un petit programme d'exemple, notre premier programme MOTIF.
Il ne va pas faire grand chose mais nous allons apprendre beaucoup en l'étudiant.
Le programme push.c ouvre une fenêtre contenant un unique bouton poussoir. Ce bouton contient une chaîne de caractères : "Push_me". Lorsqu'on clique dessus (avec le bouton gauche de la souris), un message est affiché sur stdout (et non pas dans la fenêtre).
Pour sortir du programme, il faut tuer la fenêtre soit par ctrl-c dans le xterm d'oú on l'a lancée, soit à l'aide du window manager.
Nous verrons bientôt comment créer d'autres types de Widgets, des menus, etc...
Le binaire exécutable se trouve dans :
~buffa/cours/minfo/motif/pushbutton/push.
Vous pouvez l'essayer. Par curiosité, regardez la taille du binaire!
On apprend toujours beaucoup du premier programme que l'on écrit avec une nouvelle librairie. Ce petit programme est celui proposé dans le tutorial de la documentation MOTIF. En l'étudiant, vous marchez dans mes traces car comme vous, j'ai commencé l'apprentissage de MOTIF avec ce petit programme de 20 lignes.
Nous allons voir (et comprendre) comment :
#include <Xm/PushB.h> /*--------------------------*/ main(int argc, char **argv) /*--------------------------*/ { Widget top_wid, button; XtAppContext app; void pushed_fn(); top_wid = XtVaAppInitialize(&app, "Push", NULL, 0, &argc, argv, NULL, NULL); button = XmCreatePushButton(top_wid, "Push_me", NULL, 0); /* On dit à Xt de manager le bouton */ XtManageChild(button); /* On attache un CallBack au bouton */ XtAddCallback(button, XmNactivateCallback, activateCB, NULL); /* Affichage de la fenêtre principale (top_wid) et de tous ses enfants */ XtRealizeWidget(top_wid); /* boucle de gestion des événements */ XtAppMainLoop(app); } /*----------------------------------------------*/ void activateCB(Widget w, XtPointer client_data, XmPushButtonCallbackStruct *cbs) /*----------------------------------------------*/ { printf("Don't Push Me!!\n"); }
![]()
Lorsqu'on écrit une application MOTIF, on appelle explicitement des fonctions, on manipule des types de données appartenant aux librairies Xm, Xt et/ou à la Xlib.
En général, on utilise les fonctions Xlib pour dessiner ou effectuer une gestion plus fine des événements.
Conventions de nommage :
Remarque : il y a quelques exceptions notoires comme Widget (Xt) ou Window (Xlib).
Pour qu'une application MOTIF compile sans problèmes, il faut inclure les fichiers suivants :
Pour connaître le nom du fichier include d'un Widget donné, regardez le manuel ou bien listez le répertoire /usr/dt/include (Solaris) ou /usr/include/Xm (Linux, SunOS).
Remarque : dans ces répertoires, chaque Widget possède deux fichiers d'inclusion. Un public et un privé qui contient un "P" majuscule à la fin. Seul le fichier public doit être inclu. Parfois, il existe quatre fichiers. En effet, certains Widgets possèdent des "faux-frères" que l'on appelle "Gadgets". Oublions-les vite fait! Leurs fichiers d'inclusion possèdent un "G" majuscule (exemple : PushBG.h et PushBGP.h pour le bouton poussoir).
Remarque : ce n'est pas la peine d'inclure des fichiers pour la librairie Xt car <Xm/Xm.h> les inclut déjà.
Il faut linker avec les librairies MOTIF, Xt et Xlib. On utilisera donc les options suivantes : -lXm -lXt -lX11 pendant l'édition de liens.
Remarque : l'ordre d'inclusion de ces librairies est très important !
Ces librairies se trouvent dans le répertoire /ust/dt/lib (Solaris) ou /usr/X11R6/lib (Linux, SunOS).
En général, les includes, librairies, manuels de MOTIF se trouvent dans les chemins standards de la librairie X11.
L'initialisation de la toolkit Xt doit se faire avant toute chose !
Il existe plusieurs moyens de réaliser cette opération, celui proposé dans le petit programme push.c, à l'aide de la fonction XtVaAppInitialize() est le plus commun. C'est celui que nous utiliserons dans la plupart de nos exemples.
L'appel de XtVaAppInitialize() réalise les tâches suivantes :
Dans l'exemple, le bouton poussoir a été créé comme fils de la top_level qui est de la classe Shell.En fait les Shells sont des managers très simples qui n'ont qu'un seul enfant dont ils prend la taille en lui "collant au plus près. Du coup, les Shells sont invisibles!
Description des paramètres de XtVaAppInitialize() :
Avec MOTIF, il existe une fonction de création unique pour chaque type de Widget. Il est facile de deviner le nom de la fonction de création lorsqu'on connaît le type de Widget que l'on désire créer. La règle est la suivante :
![]()
Pour créer <widget name>, on utilise la fonction XmCreate<widget name>.
![]()
Presque toutes les fonctions de création possèdent 4 paramètres :
Le troisième paramètre sert à initialiser les ressources des Widgets au moment de leur création (hauteur, largeur, couleur, etc...).
En général ces fonctions retrournent l'ID du Widget créé. Faites cependant attention, il existe des exceptions notoires ! MOTIF permet de créer des objets complexes, comprenant plusieurs Widgets assemblés. Ces objets complexes sont ensuite manipulés comme s'il s'agissait d'un seul Widget. Par exemple le Widget de type ScrolledText est en réalité une fenêtre de type Text (on peut écrire du texte à l'intérieur) accompagnée d'une SrollBar. Mais dans quoi allons nous insérer du texte ? Dans la ScrollBar ? Non ! La plupart du temps, ce qui nous intéresse c'est le Widget Text, donc, c'est l'ID de ce dernier qui est renvoyé.
Une fois créé, un Widget doit être pris en charge, il doit être managé. Cette prise en charge est réalisée par l'appel de la fonction XtManageChild().
Lorsque un Widget est managé, tous les aspects liés à sa taille son pris en compte par son père, il est rattaché à l'arborescence des Widgets qui composent l'application.
Si on ne manage pas un Widget, il demeure invisible!
Il est parfois plus pratique de créer des WIdgets en utilisant directement des fonctions de la librairie Xt telles que XtVaCreateWidget() et XtVaCreateManagedWidget().
L'avantage de ces fonctions est qu'elles permettent de :
Nous verront de nombreux exemples d'utilisation de ces deux fonctions lors des prochains chapitres de ce cours.
Rappel : les fonctions de création proposées par MOTIF créent des widgets qui ne sont pas managés et si l'on veut spécifier des ressources, il faut le faire à l'aide de fonctions supplémentaires.
Chaque Widget appartient à une classe. Chaque classe possède des ressources propres et hérite des ressources de ses superclasses. Ca en fait un paquet !
Il est très important d'avoir une assez bonne idée de la hiérarchie des classes MOTIF. On reconnaît un programmeur MOTIF expérimenté car il devine les ressources des Widgets qu'il manipule.
Le nom des ressources de chaque classe se trouve dans le manuel de chaque objet. Lorsqu'on fait man XmPushButton, on a droit tout d'abord à un tableau listant les ressources de la classe PushButton, puis un tableau moins détaillé listant les ressources de la classe Primitive dont hérite le bouton, etc...
Exemple de spécification dans le code. On aurait pu créer le bouton poussoir de la manière suivante :
button = XtVaCreateManagedWidget("Push_me",
XmPushButtonWidgetClass,
top_wid,
XmNwidth, 200,
XmNheight, 50,
NULL);
Attention lorsqu'on positionne les ressources dans le code, elles ne pourront plus être personalisées dans un fichier de ressources.
Exemple de spécification dans un fichier app-defaults ou dans un .Xdefaults : la ligne
Push_Me.background: red
positionne la couleur de background du bouton poussoir Push_Me (celui du petit bout de code déjà étudié) à rouge. Il suffira de lancer l'application compilée pour que le bouton soit rouge. Inutile de recompiler pour le rendre vert. Il suffit de modifier la ligne en :
Push_Me.background: green
et de relancer le programme.
L'équivalent du codage en dur de la largeur et de la hauteur du bouton aurait pu être réalisé dans un fichier de ressources :
Push_Me.width: 200
Push_Me.height: 50
Un chapitre entier de ce cours est consacré à l'étude détaillée des mécanismes de gestion des ressources. Inutile d'en dire trop maintenant.
Lorsqu'un Widget est créé il sait de lui-même comment répondre à certains événements : changer de taille lors d'une requête du Window Manager, changer son apparence lors d'un click souris (dans le cas d'un bouton poussoir), se redessiner si besoin est (gestion automatique des Expose events) etc...
Nous avons déjà vu dans la partie du cours consacrée à la Xlib les différents types d'événements et la complexité de leur prise en compte. Fort heureusement, Xt va grandement faciliter la tâche du programmeur car la plupart des Widgets possèdent des mécanismes simples et puissants pour prendre en compte les événements les plus courants.
Pour qu'un Widget utilise des fonctions spécifiées par le programmeur, il faut positionner certaines ressources du Widget que l'on appelle des Callback Resources.
Chaque Widget possède une table de translation qui définit la manière dont il va réagir à certains événements particuliers.
La liste complète des tables de translation de chaque Widget se trouve dans le manuel de référence Motif que chacun d'entre vous devrait avoir sur les genoux si cette monstruosité ne pesait pas dix kilos et n'était pas impossible à photocopier. Tout ceci rend la tâche du pauvre enseignant bien difficile...
Heureusement, il reste les manuels en ligne.
Voici un extrait de la table de translation du bouton poussoir :
![]()
BSelectPress: Arm()
BSelectClick: Activate(), Disarm()
![]()
BSelectPress correspond à un appui sur le bouton de gauche de la souris. L'action correspondante est un appel à la fonction interne Arm() qui modifie l'affichage du bouton pour simuler son enfoncement.
BSelectClick correspond à un click souris, c'est-à-dire lorsque le bouton de gauche est enfoncé puis relâché, alors l'action sera Activate() suivi de Disarm(), cette dernière redessine le bouton comme s'il avait repris son apparence "non enfoncé".
Les événements clavier peuvent également figurer dans la table de translation. Le programmeur peut spécifier des raccourcis clavier, prendre en compte l'appui de certaines touches aux fonctions particulières...
Par exemple : KActivate - typiquement la touche return (lorsque on entre un texte dans un sélécteur, <return> sera équivalent à presser le bouton Ok par exemple), KHelp, etc...
Les fonctions Arm(), Activate() ou Disarm() sont des exemples de callbacks prédéfinis. On les appelle des actions. Si l'on veut exécuter une fonction particulière lorsque l'utilisateur appuie sur un bouton, il faut attacher des callbacks aux Widgets.
Dans push.c la fonction activateCB() est un callback. On l'attache au bouton poussoir à l'aide de la fonction XtAddCallBack() qui est la fonction la plus commune pour effectuer cette opération.
Elle possède 4 paramètres :
Dans cet exemple, lors d'un click souris sur le bouton, MOTIF va bien ajouter à la liste des actions l'action précisé par le programmeur, qui consiste à appeler la fonction activateCB(), mais les mécanismes internes du Toolkit vont quand même exécuter les actions prédéfinies Activate() et Disarm(). Il s'agit d'un mécanisme d'ajout (XtAddCallBack). Le programmeur peut très bien spécifier plus d'un callback pour un même type d'événement.
Par exemple, dans push.c, si l'on voulait également appeler la fonction utilisateur quit(), on ajouterait la ligne suivante :
XtAddCallback(button, XmNdisarmCallback, quit, NULL);
![]()
Regardons maintenant dans le programme push.c comment la fonction de callback activateCB() a été déclarée :
void activateCB(Widget w, XtPointer client_data, XmPushButtonCallbackStruct *cbs)
![]()
Les fonctions de callback possèdent trois paramètres :
Par exemple, dans push.c, cette structure devra être castée par le type XmPushButtonCallbackStruct puisque le widget qui a provoqué l'appel est un bouton poussoir.
D'une maniére générale, la Callback Structure du widget <widget name> a la forme suivante :
typdef struct { int reason; XEvent *event; ... champs spécifiques au Widget } Xm<widget name>CallbackStruct;
![]()
Le champ reason contient des informations sur le callback telles que "arm ou disarm ont provoqué l'appel de la fonction de callback".
Nous avons presque terminé l'étude de notre premier programme. Il ne reste plus que deux étapes avant la fin, deux étapes essentielles que tout programnme MOTIF doit effectuer en dernier :