1. Avant-propos

Cette méthode semble automatique et évite beaucoup de complications. C'est pour ça que je m'y suis intéressé ! Mais tout n'est pas aussi simple qu'il le paraît ! C'est pour cette raison que j'écris cette petite aide. Ce qui est expliqué ici a été testé sous GTK2 et GTK3.

Je traiterai ici de ce que j'ai réussi à faire parmi ce que j'ai voulu essayer de faire ! Je peux, avec votre aide compléter ce document !

J'ai beaucoup appris à partir de http://nicolasj.developpez.com/gtk/gtkuimanager/, je récupérerai donc souvent des traductions sur cette page.

2. Création du menu : le fichier de référence

2.1. Format xml

Toute la structure de l'arborescence est écrite dans un fichier au format xml dont voici les spécifications (http://library.gnome.org/devel/gtk/unstable/GtkUIManager.html#XML-UI) :

 
Sélectionnez
<!ELEMENT ui          (menubar|toolbar|popup|accelerator)* >
<!ELEMENT menubar     (menuitem|separator|placeholder|menu)* >
<!ELEMENT menu        (menuitem|separator|placeholder|menu)* >
<!ELEMENT popup       (menuitem|separator|placeholder|menu)* >
<!ELEMENT toolbar     (toolitem|separator|placeholder)* >
<!ELEMENT placeholder (menuitem|toolitem|separator|placeholder|menu)* >
<!ELEMENT menuitem     EMPTY >
<!ELEMENT toolitem     (menu?) >
<!ELEMENT separator    EMPTY >
<!ELEMENT accelerator  EMPTY >
<!ATTLIST menubar      name                      &num;IMPLIED
                       action                    &num;IMPLIED >
<!ATTLIST toolbar      name                      &num;IMPLIED
                       action                    &num;IMPLIED >
<!ATTLIST popup        name                      &num;IMPLIED
                       action                    &num;IMPLIED
                       accelerators (true|false) &num;IMPLIED >
<!ATTLIST placeholder  name                      &num;IMPLIED
                       action                    &num;IMPLIED >
<!ATTLIST separator    name                      &num;IMPLIED
                       action                    &num;IMPLIED
                       expand       (true|false) &num;IMPLIED >
<!ATTLIST menu         name                      &num;IMPLIED
                       action                    &num;REQUIRED
                       position     (top|bot)    &num;IMPLIED >
<!ATTLIST menuitem     name                      &num;IMPLIED
                       action                    &num;REQUIRED
                       position     (top|bot)    &num;IMPLIED
                       always-show-image (true|false) &num;IMPLIED >
<!ATTLIST toolitem     name                      &num;IMPLIED
                       action                    &num;REQUIRED
                       position     (top|bot)    &num;IMPLIED >
<!ATTLIST accelerator  name                      &num;IMPLIED
                       action                    &num;REQUIRED >

Le fichier XML doit commencer par la balise <ui> et se terminer par </ui>. À l'intérieur de ces balises, peuvent se trouver des balises <menubar>, <toolbar>, <popup> et <accelerator>. La balise <menubar> signale le début d'une barre de menu, elle ne prend pas de paramètre. La zone encadrée par les balises <menubar> et </menubar> peut contenir les balises suivantes :

  • <menuitem> : il s'agit d'un bouton qui permet d'effectuer une action lorsque l'on clique dessus ;
  • <separator> : comme sont nom l'indique, il s'agit d'un séparateur ;
  • <placeholder> : ? (je cherche encore) ;
  • <menu> : affiche un sous-menu lorsque l'on clique dessus (le plus courant) ;

2.2. Un exemple

 
Sélectionnez
<ui>
  <menubar>
    <menu action="FichierMenuAction">
      <menuitem action="Tirer" />
      <menu action="hist" />
      <menuitem action="Quitter" />
    </menu>
    <menu action="AideMenuAction">
      <menuitem action="Aide" />
      <menuitem action="AproposAction" />
      <menuitem action="Licence" />
    </menu>
    <menu action="Tirage">
       <menu action="ModeDeTirage">
          <menuitem action="_3D6" />
          <menuitem action="_4D6" />
          <menuitem action="_5D6" />
          <menuitem action="_3D6x6" />
          <menuitem action="_4D5p3" />
        </menu>
        <menu action="Minima">
            <menuitem action="guerrier" />
            <menuitem action="paladin" />
            <menuitem action="ranger" />
            <menuitem action="voleur" />
            <menuitem action="barde" />
            <menuitem action="pretre" />
            <menuitem action="druide" />
            <menuitem action="magicien" />
            <menuitem action="illusionniste" />
            <menuitem action="autre" />
        </menu>
        <menu action="preference">
             <menu action="force">
                <menuitem action="f1" />
                <menuitem action="f2" />
                <menuitem action="f3" />
                <menuitem action="f4" />
                <menuitem action="f5" />
                <menuitem action="f6" />
                <menuitem action="f7" />
                <menuitem action="f8" />
            </menu>
        </menu>
    </menu>
  </menubar>
</ui>

2.3. Explication rapide

Nous reconnaissons la barre de menu avec trois entrées principales FichierMenuAction, AideMenuAction et Tirage : elles sont repérées par les balises <menu>. Les deux premiers sous-menus sont très simples : un seul niveau de menu avec pour le premier deux entrées, le second trois entrées. Le troisième est bien plus intéressant car il contient des sous-menus : la balise <menu> à nouveau. Ces balises peuvent contenir elles-mêmes d'autres balises <menu> et ainsi de suite. Les trois sous-menus de ce menu ont aussi un intérêt pédagogique ici : le premier est un groupe de boutons radio, le second est un groupe de cases à cocher et le troisième est un autre groupe de boutons radio indépendant du premier. La nature des différentes entrées sera définie plus tard.

Ce menu sera lu par la fonction : gtk_ui_manager_add_ui_from_file(menu_Manager,"main.menu.xml",NULL) dont le format est :

 
Sélectionnez
guint               gtk_ui_manager_add_ui_from_file     (GtkUIManager *manager,
                                                         const gchar *filename,
                                                         GError **error);

filename est le nom du fichier à analyser, error localisation du retour de la fonction erreur et manager l'objet menu (GtkUIManager) dans lequel on aura préalablement défini les actions des différentes entrées. Les définitions sont expliquées dans le chapitre suivant.

Nous créons le menu et l'affichons avec le code qui suit :

 
Sélectionnez
GtkUIManager *menu_Manager;
      [...]
menu_Manager = gtk_ui_manager_new ();      
gtk_ui_manager_add_ui_from_file(menu_Manager,"main.menu.xml",NULL); /* création des items du menu à partir du fichier xml */
      [...]
menubar = gtk_ui_manager_get_widget (menu_Manager,"/menubar"); /* pour trouver le menu */
gtk_box_pack_start (GTK_BOX (vbox), menubar, FALSE, FALSE, 0); /* pour mettre le menu dans la fenêtre http://live.gnome.org/GnomeLove/UIManagerTutorial */

3. Définition des différentes actions

3.1. Initialisation

Avant tout, on commence par créer un objet de type GtkActionGroup :

 
Sélectionnez
p_actionGroup = gtk_action_group_new( "menuActionGroup" );

C'est à cet objet qu'on fera référence dès qu'on voudra faire quelque chose avec notre menu. Il est possible d'en mettre plusieurs dans un menu, les uns après les autres.

3.2. Les entrées simples : GtkActionEntry

Pour les définir, nous avons un tableau de GtkActionEntry donc la structure est la suivante :

 
Sélectionnez
typedef struct
{
   const gchar *name;
   const gchar *stock_id;
   const gchar *label;
   const gchar *accelerator;
   const gchar *tooltip;
   GCallback callback;
} GtkActionEntry;

Voici le détail de chaque champ :

  • name : c'est le nom de l'élément du menu, il s'agit du nom spécifié dans le fichier XML par l'intermédiaire du paramètre action
  • stock_id : ce champ permet de spécifier une image prédéfinie (GTK_STOCK_NEW par exemple, voir http://library.gnome.org/devel/gtk/unstable/gtk3-Stock-Items.html ou http://library.gnome.org/devel/gtk/stable/gtk-Stock-Items.html)
  • label : c'est le texte qui sera affiché
  • accelerator : pour spécifier un accès direct par raccourci clavier, nous y reviendrons dans la suite de cet article
  • tooltip : ce paramètre ne sert à rien dans le cas d'un menu, il nous sera utile lors de la création de la barre d'outils
  • callback : c'est l'adresse de la fonction qui va être appelée lors d'un clic sur cet élément

Pour continuer notre exemple, voici la structure correspondant au fichier XML ci-dessus :

 
Sélectionnez
GtkActionEntry entrees_menu[] = 
{
 {"FichierMenuAction",NULL           ,"_Jeu"        , NULL       , NULL                   ,NULL                        },
 {"Tirer"            ,"_3D6"         ,"_Tirer"      ,"<Control>T","Tirage d'un personnage",G_CALLBACK (tirage         )},
 {"Quitter"          ,GTK_STOCK_QUIT ,"_Quitter"    ,"<Control>Q","Finir la partie"       ,G_CALLBACK( cb_fin         )},
 {"AideMenuAction"   ,NULL           ,"_Aide"       , NULL       , NULL                   ,NULL                        },
 {"Aide"             ,GTK_STOCK_HELP ,"_Aide"       ,"<Shift>A"  , NULL                   ,G_CALLBACK( menu_aide      )},
 {"AproposAction"    ,GTK_STOCK_ABOUT,"A _propos"   ,"<Alt>A"    , NULL                   ,G_CALLBACK( menu_a_propos  )},
 {"Licence"          ,GTK_STOCK_INFO ,"_Licence GPL","<Release>L", NULL                   ,G_CALLBACK( menu_licence   )}, 
 {"Tirage"           ,"_3D6x6"       ,"Choix et spécification du _Tirage"  }, 
 {"ModeDeTirage"     ,"_5D6"         ,"_Mode de tirage"  }, 
 {"Minima"           ,"_4D5p3"       ,"_Classe"  }, 
 {"preference"       ,NULL           ,"_Préférence" }, 
 {"force"            ,NULL           ,"_Force"  }
};

Ces données seront activées par la ligne suivante :

 
Sélectionnez
  gtk_action_group_add_actions( p_actionGroup, entrees_menu, G_N_ELEMENTS( entrees_menu ), param );

On retrouve dans cette ligne la référence au menu avec la variable p_actionGroup, le paramètre suivant est la structure qui indique quelles données traiter avec le nom du tableau juste décrit par : entrees_menu, l'entrée suivante est le nombre d'éléments du tableau et le dernier paramètre représente le paramètre à passer à la fonction callback désignée par le 6e paramètre de chaque ligne du tableau.

La fonction callback doit avoir le format de la fonction suivante :

 
Sélectionnez
void explication (GtkWidget *wid, gpointer param);

Il faut penser que le paramètre passé aux fonctions callback ainsi définies est toujours le même : le paramètre param ici nommé, précisé par la fonction gtk_action_group_add_actions. Le premier paramètre reçu, wid, est un pointeur sur l'élément qui a été activé pour appeler la fonction.

3.3. Les cases à cocher

Une case à cocher est un élément solitaire sans interaction avec les autres (à moins d'une programmation spécifique).

Pour définir les cases à cocher, nous avons un tableau de GtkActionEntry dont la structure est la suivante :

 
Sélectionnez
typedef struct {
  const gchar     *name;
  const gchar     *stock_id;
  const gchar     *label;
  const gchar     *accelerator;
  const gchar     *tooltip;
  GCallback  callback;
  gboolean   is_active;
} GtkToggleActionEntry;

Il y a un paramètre de plus que pour les entrées simples : is_active. Il définit si le bouton est actif ou non au départ.

Pour continuer notre exemple, voici la structure correspondant au fichier XML ci-dessus :

 
Sélectionnez
static const GtkToggleActionEntry toggle_entries[] = 
{ /* liste des paramètres fournis avec gtk_action_group_add_toggle_actions */ 
 { "guerrier", NULL, "_Guerrier", NULL, "Minima du guerrier", (GCallback)liste_guerrier,TRUE }, 
 { "paladin", NULL, "_Paladin", NULL, "Minima du paladin", (GCallback)liste_paladin, FALSE }, 
 { "ranger", NULL, "_Ranger", NULL, "Minima du ranger", (GCallback)liste_ranger, FALSE }, 
 { "voleur", NULL, "_Voleur", NULL, "Minima du voleur", (GCallback)liste_voleur, FALSE }, 
 { "barde", NULL, "_Barde", NULL, "Minima du barde", (GCallback)liste_barde, FALSE }, 
 { "pretre", NULL, "Prê_Tre", NULL, "Minima du prêtre", (GCallback)liste_pretre, FALSE }, 
 { "druide", NULL, "_Druide", NULL, "Minima du druide", (GCallback)liste_druide, FALSE }, 
 { "magicien", NULL, "_Magicien", NULL, "Minima du magicien", (GCallback)liste_magicien, FALSE }, 
 { "illusionniste", NULL, "_Illusionniste", NULL, "Minima de l'illusionniste ",(GCallback)liste_illusionniste, FALSE }, 
 { "autre", NULL, "_Autre", NULL, "Autre", NULL, FALSE}
};

Ces données seront activées par la ligne suivante :

 
Sélectionnez
  gtk_action_group_add_toggle_actions (p_actionGroup, toggle_entries, G_N_ELEMENTS (toggle_entries), param);

Comme pour les boutons simples, on retrouve dans cette ligne la référence au menu avec la variable p_actionGroup, le paramètre suivant est la structure qui indique quelles données traiter avec le nom du tableau juste décrit : entrees_menu, l'entrée suivante est le nombre d'éléments du tableau et le dernier paramètre représente le paramètre à passer à la fonction callback désignée par le 6e paramètre de chaque ligne du tableau : ce sera le même pour tous les boutons ainsi définis.

La fonction callback est du même format que pour les boutons simples. Dans cette fonction, on peut trouver facilement l'état du bouton avec gtk_toggle_action_get_active(current), regardez l'exemple suivant :

 
Sélectionnez
void liste_magicien (GtkToggleAction *current, gpointer param) 
{ 
   unsigned short * val; 
   val =*((unsigned short **)param+1); 

   val[MAGICIEN]=gtk_toggle_action_get_active(current); 
   if (gtk_toggle_action_get_active(current)) 
   { 
       supprime("illusioniste",param); 
   } 
}

3.4. Les boutons radio

Un groupe de boutons radio ne peut avoir qu'un seul item coché, dès qu'on en coche un nouveau, celui qui était activé auparavant se désactive automatiquement.

3.4.1. La première liste de boutons radio

Pour les définir, nous avons un tableau de GtkActionEntry dont la structure est la suivante :

 
Sélectionnez
typedef struct {
  const gchar *name;
  const gchar *stock_id;
  const gchar *label;
  const gchar *accelerator;
  const gchar *tooltip;
  gint   value; 
} GtkRadioActionEntry;

Les différences avec les boutons simples, c'est la disparition de la fonction callback et l'apparition du champ value. Ce champ est la valeur prise par le groupe de boutons radio lorsque c'est l'item défini par cette ligne qui est actif.

Pour continuer notre exemple, voici la structure correspondant au fichier XML ci-dessus :

 
Sélectionnez
static const GtkRadioActionEntry radio_entries[] = { 
 { "_3D6"  , "_3D6" , "_3 dés à 6 faces", NULL, "Tirages des caractéristiques avec 3 dés à 6 faces", _3D6 }, 
 { "_4D6"  , "_4D6"  , "_4 dés à 6 faces, 3 meilleurs", NULL, "Tirages des caractéristiques avec 4 dés à 6 faces, somme des 3 meilleurs", _4D6 }, 
 { "_5D6"  , "_5D6"  , "_5 dés à 6 faces, 3 meilleurs", NULL, "Tirages des caractéristiques avec 5 dés à 6 faces, somme des 3 meilleurs", _5D6 }, 
 { "_3D6x6", "_3D6x6", "_6 fois 3 dés à 6 faces, le meilleur", NULL, "Tirages des caractéristiques avec 6 fois 3 dés à 6 faces, on garde le meilleur", _3D6x6 }, 
 { "_4D5p3", "_4D5p3", "4 dés à 5 faces, 3 meilleurs _+ 3", NULL, "Tirages des caractéristiques avec 4 dés à 5 faces, somme des 3 meilleurs plus 3",_4D5p3}
};

Pour des raisons de lisibilité, le champ value est rempli avec des constantes que j'ai définies comme ceci :

 
Sélectionnez
#define _4D6    1 
#define _3D6x6  2 
#define _5D6    3
#define _4D5p3  4

Ces données seront activées par la ligne suivante :

 
Sélectionnez
gtk_action_group_add_radio_actions (p_actionGroup, radio_entries, G_N_ELEMENTS (radio_entries), 1, (GCallback)print_selected, (gpointer) param);

On retrouve dans cette ligne la référence au menu avec la variable p_actionGroup, le paramètre suivant est la structure qui indique quelles données traiter avec le nom du tableau juste décrit avant, entrees_menu, l'entrée suivante est le nombre d'éléments du tableau, l'avant-dernier paramètre (que nous n'avions ni pour les cases à cocher, ni pour les boutons simples) est la fonction callback et le dernier paramètre représente le paramètre à passer à la fonction callback définie précédemment. Pour chaque bouton radio du groupe ainsi défini, on aura la même fonction avec le même paramètre qui sera appelé.

La fonction callback qui sera appelée lors d'une modification de l'état du bouton radio est heureusement un peu modifiée par rapport aux formats des fonctions appelées pour les deux précédents cas. Elle a un paramètre de plus. Voici l'exemple :

 
Sélectionnez
void print_selected(GtkRadioAction *action, GtkRadioAction *current, gpointer param) 
{ 
   unsigned short * val; 
   val =*((unsigned short **)param+1); 
   *val = gtk_radio_action_get_current_value (current); /* récupère le numéro du bouton activé : dernier paramètre de GtkRadioActionEntry */

   (void)action; 
}

gtk_radio_action_get_current_value * current et gtk_radio_action_get_current_value * action (c'est quoi la différence entre ces deux paramètres?) sert à récupérer le numéro associé au bouton activé. param est le pointeur sur les paramètres envoyés pour la fonction callback.

3.4.2. Les listes de boutons radio suivantes

Il est tout à fait possible d'avoir plusieurs groupes de boutons radios indépendants dans un menu. J'en ai mis un second dans mon exemple. Pour cela, c'est très simple. Il suffit de recommencer autant de fois que nécessaire la manipulation précédente !

 
Sélectionnez
static const GtkRadioActionEntry f_radio_entries[] = {
   { "f1"  , NULL , "1", NULL, "1", 1 },
   { "f2"  , NULL , "2", NULL, "2", 2 },
   { "f3"  , NULL , "3", NULL, "3", 3 },
   { "f4"  , NULL , "4", NULL, "4", 4 },
   { "f5"  , NULL , "5", NULL, "5", 5 },
   { "f6"  , NULL , "6", NULL, "6", 6 },
   { "f7"  , NULL , "7", NULL, "7", 7 },
   { "f8"  , NULL , "8", NULL, "8", 8 },
};
 
Sélectionnez
gtk_action_group_add_radio_actions(p_actionGroup,f_radio_entries,G_N_ELEMENTS (f_radio_entries),1,(GCallback)print_force,(gpointer) param);

3.5. Activation du menu

Il faut insérer le groupe d'actions ainsi créé dans un menu, il faut un menu !

 
Sélectionnez
menu_Manager = gtk_ui_manager_new ();
gtk_ui_manager_add_ui_from_file(menu_Manager,"main.menu.xml",NULL);

C'est cette fonction décrite précédemment qui le fait à partir du fichier main.menu.xml que j'ai montré en exemple.

Après, il faut insérer notre groupe d'actions dans ce menu :

 
Sélectionnez
gtk_ui_manager_insert_action_group( menu_Manager, p_actionGroup, 0 );

Il faut aussi activer les raccourcis clavier :

 
Sélectionnez
  pAccel = gtk_ui_manager_get_accel_group (menu_Manager);
  gtk_window_add_accel_group (GTK_WINDOW (win), pAccel);

win est la fenêtre principale dans laquelle on inclut notre menu ( win = gtk_window_new (GTK_WINDOW_TOPLEVEL); ).

3.6. Les raccourcis clavier

3.6.1. Le champ accelerator

Dans la structure GtkActionEntry, il y a un champ accelerator qui est destiné à contenir un raccourci clavier pour le menu correspondant. Le raccourci doit être spécifié sous forme d'une chaîne de caractères au format accepté par la fonction gtk_accelerator_parse(), c'est-à-dire commençant par au moins l'une des balises suivantes :

  • <Control> ;
  • <Shift> ;
  • <Alt> ;
  • <Release> pour le relâchement d'une touche.

3.6.2. Raccourci pour le parcours des menus

On peut tout simplement faire précéder la lettre qui activera le menu d'un _ (caractère souligné ou underscore) dans les tableaux de « ActionEntry ».

Par exemple dans notre menu, nous avons :

 
Sélectionnez
{"AideMenuAction",NULL           ,"_Aide"       , NULL       , NULL , NULL                      }, 
{"Aide"          ,GTK_STOCK_HELP ,"_Aide"       ,"<Shift>A"  , NULL , G_CALLBACK( menu_aide    )}, 
{"AproposAction" ,GTK_STOCK_ABOUT,"A _propos"   ,"<ALT>A"    , NULL , G_CALLBACK( menu_a_propos)}, 
{"Licence"       ,GTK_STOCK_INFO ,"_Licence GPL","<Release>L", NULL , G_CALLBACK( menu_licence )},

Cela correspond à :

 
Sélectionnez
    <menu action="AideMenuAction"> 
      <menuitem action="Aide" /> 
      <menuitem action="AproposAction" /> 
      <menuitem action="Licence" /> 
    </menu>

La première entrée "_Aide" de "AideMenuAction" fait partie de la barre de menu visible sur l'écran : il faut activer [ALT]-A pour ouvrir ce menu. Après, pour afficher l'aide, il suffit de taper A pour activer la seconde entrée "_Aide" de "Aide". Pour la licence, il aurait fallu activer L pour "_Licence GPL" de "Licence". L'ouverture des entrées du menu demande l'action d'un [ALT] en plus de la lettre. On voit donc qu'il y a conflit avec le raccourci "<ALT>A" attribué à l'entrée "AproposAction". Dans ce cas, quel que soit l'ordre de définition de ces raccourcis, ce sera toujours le raccourci défini avec le _ qui sera pris en compte.

Remarque : même les cases à cocher ou les boutons radios peuvent être activés de cette façon, mais la fonction callback n'est pas appelée !

3.7. Les menus détachables

3.7.1. Tous d'un coup

Ne cherchez pas une entrée particulière pour mettre la ligne en pointillé qui vous permettra de mettre un menu en flottant ! Ceci se fait pour tous les menus directement avec la fonction :

 
Sélectionnez
gtk_ui_manager_set_add_tearoffs     (menu_Manager,TRUE);

3.7.2. Un par un

C'est bien beau de rendre tous les menus détachables d'un coup, mais il serait surprenant que ce soit toujours intéressant, certains menus n'ont pas à être détachables. Il est possible de les rendre un à un détachables et alors de choisir lesquels doivent l'être.

Pour cela, j'ai fait une petite procédure :

 
Sélectionnez
void Tearoff_on(GtkUIManager * menu_Manager,char * chemin)
{
    GtkWidget * pere = gtk_ui_manager_get_widget(menu_Manager,chemin); /* identification du sous-menu           */
    GtkWidget * pMenu=gtk_menu_item_get_submenu(GTK_MENU_ITEM(pere));  /* en deux étapes                        */

    GtkWidget * nv_entree = gtk_tearoff_menu_item_new();               /* création du TeaOff                    */
    gtk_menu_shell_prepend(GTK_MENU_SHELL(pMenu), nv_entree);          /* insertion du Teaoff dans le sous-menu */
}

Voici comment on l'appelle :

 
Sélectionnez
Tearoff_on(menu_Manager,"/menubar/Tirage/preference/force");

Les paramètres sont : le GtkUIManager *menu_Manager du 2.3 et le chemin d'accès au menu force donné par le fichier xml du 2.2.

4. Création d'icônes personnelles

Dans les tableaux du genre « GtkActionEntry entrees_menu[] », l'entrée stock_id définit une icône. Beaucoup d'icônes prédéfinies existent déjà comme GTK_STOCK_QUIT (vous pouvez par exemple les trouver dans le fichier .http://library.gnome.org/devel/gtk/2.21/gtk-Stock-Items.html ). Mais vous pouvez en définir vous-même.

Voici comment j'ai fait :

 
Sélectionnez
 static struct 
 { 
  gchar *filename; 
  gchar *stock_id; 
 } stock_icons[] = 
 { 
  { "3d6.png", "_3D6" },
  { "4d6.png", "_4D6" },
  { "4d5p3.png", "_4D5p3" },
  { "5d6.png", "_5D6" },
  { "3d6x6.png", "_3D6x6" }
 }; 
 static gint n_stock_icons = G_N_ELEMENTS (stock_icons); 
 GtkIconFactory *icon_factory; 
 GtkIconSet *icon_set; 
 GtkIconSource *icon_source; 
 gint i;

  /* création des icônes */ 
  ici=g_get_current_dir(); /* pour avoir le chemin absolu nécessaire pour gtk_icon_source_set_filename */
  icon_factory = gtk_icon_factory_new (); 
  for (i = 0; i <= n_stock_icons; i++) 
  { 
      icon_set = gtk_icon_set_new (); 
      icon_source = gtk_icon_source_new (); 
      sprintf(chemin,"%s/%s",ici,stock_icons[i].filename);
      gtk_icon_source_set_filename (icon_source, chemin);
      gtk_icon_set_add_source (icon_set, icon_source); 
      gtk_icon_source_free (icon_source); 
      gtk_icon_factory_add (icon_factory, stock_icons[i].stock_id, icon_set); 
      gtk_icon_set_unref (icon_set); 
  } 
  gtk_icon_factory_add_default (icon_factory); 
  g_object_unref (icon_factory); /* supprimer l'objet de la mémoire */
  free(ici) ;

Le tableau stock_icons est constitué d'une structure qui contient deux informations :

  • 1) le chemin du fichier (à partir de l'exécutable) ;
  • 2) le nom qui va être donné à l'icône dans le champ stock_id des structures.

Après, vous pouvez regarder, par exemple, l'entrée suivante du menu :

 
Sélectionnez
 {"Tirer"            ,"_3D6"         ,"Tirer"      ,"<Control>T","Tirage d'un personnage", G_CALLBACK (tirage         )},

Le second champ est le nom de la première icône définie dans le tableau : le dessin 3d6.png sera donc l'icône de cette entrée du menu.

Ce n'est pas plus compliqué que ça !

5. Travailler avec le menu directement

5.1. Le couteau suisse

Je me suis fait un petit programme qui parcourt tous les items du menu, affiche les noms de leurs différents champs. Cela m'a permis de mieux comprendre où j'étais :

 
Sélectionnez
GList * gl=gtk_action_group_list_actions (p_actionGroup),*k;

for (k = gl; k != NULL; k = g_list_next(k))
{
    gchar *action_label;
    gchar *action_name;
    gchar *action_stock;
    GtkAction *action;
    action = GTK_ACTION(k->data);
    g_object_get(action,"label",&action_label,"name",&action_name,"stock-id", &action_stock,NULL);
    printf("label :%s , name : %s , stock : %s\n",action_label,action_name,action_stock);
    if (strcmp(action_name,"_3D6")==0) /* pour l'item donc le nom est _3D6 */
    {  /* trouver la liste des propriétés d'un item */
         guint nb_s,f;
         GParamSpec ** l_spec=g_object_class_list_properties(G_OBJECT_GET_CLASS(action),&nb_s);
         for (f=0;f<nb_s;f++)           /* afficher la liste des propriétés */
         {
             printf("%s \n",l_spec[f]->name);
         }
    }
    g_free(action_stock);
    g_free(action_name);
    g_free(action_label);
}

Après, on recherche dans http://library.gnome.org/devel/gtk/stable/ le mot qui nous intéresse et finalement, il y a http://www.google.com/codesearch pour trouver des bouts de code avec les mots qui nous intéressent afin de voir comment c'est utilisé.

Certes, comme je vais tout vous expliquer, cela vous est a priori inutile ! Mais, ça pourra peut-être vous servir pour trouver autre chose. C'est grâce à cette astuce que j'ai pu comprendre un peu mieux comment fonctionnait le menu. C'est avec ce système que je parcours chaque fois mon menu à la recherche de l'item avec lequel je veux travailler, que ce soit pour récupérer son état ou pour le modifier.

5.2. Trouver l'item voulu dans le menu

À partir de ces essais, j'ai fait la fonction pour trouver l'item souhaité dans le menu (attention, elle ne marche que pour les entrées créées avec gtkuimanager) :

 
Sélectionnez
GtkAction * trouve_action(gchar * ch,GList * gl) 
{ 
   GList * k; 
   GtkAction * action; 
   gchar *action_name; 

    for (k = gl; k != NULL; k = g_list_next(k)) 
    { 
       action = GTK_ACTION(k->data); 
        g_object_get(action, 
                     "name",     &action_name, 
                     NULL); 
        if (strcmp(action_name,ch)==0) 
        { 
            g_free(action_name); 
            return action; 
        } 
        g_free(action_name); 
    } 

    return NULL; 
} 

Elle reçoit comme paramètre le nom de l'item recherché et un pointeur sur le menu, la liste des éléments du menu et cela correspond avec les noms des variables que j'ai donnés jusqu'à maintenant à gtk_action_group_list_actions (p_actionGroup). Elle renvoie un pointeur sur l'item recherché s'il est trouvé, sinon, ce sera un pointeur sur NULL.

5.3. Changer le nom d'une entrée de menu

Tout d'abord, il faut trouver l'entrée à modifier avec la fonction précédente.

 
Sélectionnez
GtkAction *action;

action=trouve_action(nom,p_action) ;
if (action!=NULL)
        g_object_set(action,"label",ligne,NULL);
           /* ligne est une chaine de caractères qui contient le nouveau label à mettre */

Et après, on change la propriété « label » de cet objet avec g_object_set . Cette fonction est définie d'une manière assez surprenante :

  • premier paramètre : un pointeur sur l'item à modifier (GtkAction *) ;
  • après par groupe de deux : le nom de ma propriété (chaîne de caractères) et la valeur à lui mettre (le format dépend de la propriété) ;
  • NULL pour clore la série propriété/valeur.

Cette petite manipulation pourrait être une fonction en soi.

5.4. Savoir si un bouton radio ou une case à cocher est coché

Il faut parcourir le menu pour trouver l'entrée de menu souhaitée, ensuite, on vérifie si elle est à TRUE ou à FALSE :

 
Sélectionnez
GtkAction *action;
gint etate; /* même si c'est 0 ou 1, il faut un gint, sinon, il y a débordement */

action=trouve_action(nom,p_action) ;
if (action!=NULL)
        g_object_get(action,"active",&etate,NULL);

Cette fois, nous avons utilisé la fonction g_object_get qui est fort semblable à g_object_set. La différence est qu'au lieu de passer la valeur à mettre, on passe un pointeur sur la variable sur laquelle il va falloir mettre le résultat trouvé.

Pour un groupe de boutons radio, on sera obligé de parcourir la liste si on veut savoir lequel est coché.

Cette action peut devenir une petite fonction.

5.5. Modifier un bouton radio ou une case à cocher

C'est strictement comme pour changer le nom de l'item.

Pour le cocher :

 
Sélectionnez
GtkAction *action;

action=trouve_action(nom,p_action) ;
if (action!=NULL)
        g_object_set(action,"active",1,NULL);

Et pour le décocher, cette action ne se fait pas avec un bouton radio :

 
Sélectionnez
GtkAction *action;

action=trouve_action(nom,p_action) ;
if (action!=NULL)
        g_object_set(action,"active",0,NULL);

Ces actions peuvent encore faire deux fonctions.

5.6. Rendre inactif un item du menu

Ceci met en grisé l'entrée souhaitée. On ne peut donc plus utiliser cette entrée du menu.

À partir du main du programme, juste après avoir fini de déclarer mon menu, j'ai inséré les lignes suivantes :

 
Sélectionnez
 /* inactivation de certaines entrées du menu */
  inactive("f7",p_actionGroup);
  inactive("f8",p_actionGroup);

Je souhaite griser les entrées f7 et f8 de mon menu.

Pour cela, je fais appel à cette fonction :

 
Sélectionnez
void inactive(gchar * ch,GtkActionGroup * p_actionGroup)
{
    GtkAction *action=trouve_action(ch,gtk_action_group_list_actions (p_actionGroup));

    if (action!=NULL)
        g_object_set(action,"sensitive",0,NULL);
}

C'est toujours la même méthode qu'avant, sauf que cette fois, c'est sur la propriété « sensitive » qu'il faut agir.

Si on veut la rendre active, il suffira de remplacer le 0 par un 1 dans l'appel de la fonction g_object_set.

Remarque : il y a aussi la propriété visible, mais je ne suis pas arrivé à la faire marcher ! Je la modifie sans effet visible...

6. Modifications en temps réel du menu

La principale limite de la conception montrée jusque-là est que le menu est static, non modifiable. Nous allons donc montrer comment le modifier. Nous allons utiliser la bonne vieille méthode, la méthode classique expliquée dans le Cours GTK 2. C'est la méthode que j'ai utilisée sans le dire pour créer un à un les Teaoffs au 3.7.2.

6.1. Ajout d'un item dans un menu

Nous allons faire une routine qui ajoute des entrées dans le sous-menu hist du 2.2 :

 
Sélectionnez
void cree_entree_menu(char * nom_,char * titre,GtkUIManager *  menu_Manager,unsigned short i,gpointer * param)
{
    GtkWidget * pere ;
    GtkWidget *pMenu ;
    GtkWidget * nv_entree;

    pere = gtk_ui_manager_get_widget(menu_Manager,"/menubar/FichierMenuAction/hist");          /* on trouve l'item pere du sous-menu                 */
    pMenu=gtk_menu_item_get_submenu(GTK_MENU_ITEM(pere));                                      /* on trouve le sous-menu                             */
    nv_entree = gtk_menu_item_new_with_mnemonic (nom_);                                        /* création de l'entrée                               */
    gtk_menu_item_set_label(GTK_MENU_ITEM(nv_entree),titre);                                   /* affectation la chaine à afficher pour le menu      */
    g_signal_connect(G_OBJECT(nv_entree), "activate", G_CALLBACK(menu_va_hist1), param);       /* affection d'une fonction callback                  */
    gtk_menu_shell_insert(GTK_MENU_SHELL(pMenu), nv_entree,i+1);                               /* insertion de l'entrée dans le menu à la place i+1  */
											       /* attention, la première place est la place 1 (pas 0)*/
}

Nous n'avons pas pris la méthode la plus simple pour insérer la nouvelle entrée, nous avons pris gtk_menu_shell_insert qui impose la place alors qu'il existe une fonction pour insérer en début de liste gtk-menu-shell-prepend et une autre en fin de liste gtk-menu-shell-append.

6.2. Ajout d'un item dans un menu vide, ou modifier un menu

Le précédent point avait un problème avec les menus vides : ceux-ci récupèrent l'entrée déactivée (non sensitive) Vide comme le montre l'image suivante.

Image non disponible

Il faut donc remplacer cette entrée Vide par la première entrée. Je propose donc une routine qui remplace l'entrée existante (et la rend active) si elle existe ou la crée si elle n'existe pas. C'est cette fonction qu'il faudra utiliser si vous voulez modifier une entrée d'un menu créé ou modifié dynamiquement (avec le point précédent).

 
Sélectionnez
void cree_entree_menu(char * nom_,char * titre,GtkUIManager *  menu_Manager,unsigned short i,gpointer * param)
{
 GtkWidget * pere ;
 GtkWidget *pMenu ;
 GtkWidget * nv_entree;
 unsigned short j;
 GList * gl;
 GList * k;

 pere = gtk_ui_manager_get_widget(menu_Manager,"/menubar/FichierMenuAction/hist");          /* on trouve l'item père du sous-menu                 */
 pMenu=gtk_menu_item_get_submenu(GTK_MENU_ITEM(pere));                                      /* on trouve le sous-menu                             */
 gl=gtk_container_get_children(GTK_CONTAINER(pMenu));                                       /* on récupère la GList des entrées du sous-menus     */

 for (k = gl,j=0; k != NULL; k = g_list_next(k),j++)                                        /* on cherche l'entrée i                              */
 {
  if (j==i)                                                                                 /* elle existe déjà                                   */
  {
   nv_entree=k->data;                                                                       /* on modifie son état                                */
   gtk_menu_item_set_label(GTK_MENU_ITEM(nv_entree),titre);                                 /*                        - l'affichage               */
   g_object_set(nv_entree,"sensitive",1,NULL);                                              /*                        - l'activation              */
  }
 }
 if (i>=j)                                                                                  /* si elle n'existe pas                               */
 {
    nv_entree = gtk_menu_item_new_with_mnemonic (nom_);                                     /*  création de l'entrée                              */
    gtk_menu_item_set_label(GTK_MENU_ITEM(nv_entree),titre);                                /*  affectation la chaine à afficher pour le menu     */

    gtk_menu_shell_insert(GTK_MENU_SHELL(pMenu), nv_entree,i);                              /* insertion de l'entrée dans le menu à la place i    */
 }
 g_signal_connect(G_OBJECT(nv_entree), "activate", G_CALLBACK(menu_va_hist1), param);
 }
}

6.3. Suppression d'une entrée

Toujours au même sous-menu, nous allons supprimer l'entrée 2 :

 
Sélectionnez
pere = gtk_ui_manager_get_widget(menu_Manager,"/menubar/FichierMenuAction/hist");          /* on trouve l'item père du sous-menu                 */
pMenu=gtk_menu_item_get_submenu(GTK_MENU_ITEM(pere));                                      /* on trouve le sous-menu                             */
gl=gtk_container_get_children(GTK_CONTAINER(pMenu));                                       /* on récupère la GList des entrées du sous-menu      */
for (k = gl,j=0; k != NULL && j<2; k = g_list_next(k),j++)
{
    if (j==2)
    {
        gl=g_list_remove_link (gl,k);
        k=gl;
    }
}

7. Remarques importantes

7.1. Fonctions callback

Une fonction callback est une fonction qu'on appelle suite à un évènement particulier, à nous de le définir lors de la conception de notre application. Son exécution se fait en parallèle avec la gestion de l'interface. Il est donc possible d'avoir plusieurs fonctions callback qui s'exécutent simultanément.

Nous avons vu qu'on peut passer un seul paramètre à une fonction callback : un gpointer. Celui-ci peut être, en fait, un pointeur sur un tableau de pointeurs où chaque pointeur pointe sur un élément de nature différente.

Dans la fonction principale :

 
Sélectionnez
int main (int argc, char *argv[]) 
{ 
 [...]
 void * param[10];
 GtkWidget *win = NULL;
 GtkActionGroup * p_actionGroup ;
 [...]


 param[0]=(void *)win;
 param[2]=(void *)menubar; 
 param[3]=(void *)p_actionGroup;
 [...]

g_signal_connect (G_OBJECT (button), "clicked", G_CALLBACK (tirage), (gpointer) param);

Dans un autre fichier :

 
Sélectionnez
void tirage (GtkWidget *wid, gpointer param) 
{ 
 [...]
 unsigned short * caract ;
 GtkActionGroup *act_gr = *((GtkActionGroup **)param+3);

 [...]
 caract=(unsigned short *)malloc(sizeof(unsigned short));
 ((void **)param)[1]=(void *)caract;

 g_signal_connect (G_OBJECT (button), "clicked", G_CALLBACK (sauvegarde_perso), (gpointer) param);

Ou :

 
Sélectionnez
void sauvegarde_perso (GtkWidget *wid, gpointer param) 
{ 
 [...]

 unsigned short * caract; 
[...]

 caract=*((unsigned short **)(param)+1);

Notez que, dans le programme principal, j'ai pu prendre un tableau non dynamique : tant que la fenêtre ne sera pas détruite, le tableau restera réservé en mémoire. Par contre, dans les autres fonctions, la place mémoire réservée pour le tableau non dynamique sera libérée dès que la fonction sera achevée. Il faudra donc prendre un tableau où la mémoire a été allouée dynamiquement. Il faudra donc prévoir de désallouer la mémoire dès qu'on n'en aura plus besoin :

 
Sélectionnez
g_signal_connect (G_OBJECT (win),"destroy",G_CALLBACK (fin_perso), (gpointer) param);

[...]

void fin_perso(GtkWidget *p_widget, gpointer param) 
{ 
 free(*((unsigned short **)(param)+1)); 
 
 /* paramètres inutilisés */ 
 (void)p_widget; 
} 

7.2. Liens

Liste des signaux principaux : http://developer.gnome.org/gtk/stable/GtkWidget.html#GtkWidget.signals. Il en existe d'autres pour beaucoup d'autres objets (ici, ce sont les widgets), ils sont affichés sur la page de description de l'objet. N'oubliez-pas de regarder tous les signaux des parents d'un objet quand vous voulez travailler avec celui-ci.

Liste des Boutons : http://developer.gnome.org/gtk/stable/gtk-Stock-Items.html

8. Améliorations possibles

Si vous avez lu mon document, vous êtes sûrement tombés sur des questions que je me pose, je suis bien sûr preneur de toute réponse.

Je ne suis pas un expert en la matière, j'ai juste passé du temps à tester la configuration de mon menu pour arriver à mes fins. Comme j'y suis arrivé, j'ai écrit ce texte pour vous aider à y arriver aussi. Donc, si vous pensez que je dois rajouter des informations qui vous semblent importantes, si vous pensez qu'il y a mieux, faites-moi parvenir vos remarques pour que tous puissent en bénéficier et améliorer la qualité de cette aide.

9. Remerciements

Pour les relectures, je remercie jacques_jean et ClaudeLELOUP.

12 commentaires Donner une note à l'article (5)