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

Programmation orientée objet avec Gtk+ v3

Créer son propre widget

Cet article traite de la création d’un nouveau widget avec Gtk+ v3 en programmation orientée objet.

Commentez Donner une note à l´article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

1. Introduction

Programmer des applications en utilisant Gtk+ est devenu monnaie courante. Tout un chacun utilise à son gré les widgets mis à disposition par la bibliothèque. Il arrive cependant que l'on ait un besoin spécifique et les widgets proposés n'y répondent pas forcément.

En général, on écrit alors une fonction qui agrège différents widgets existants. Elle nous restitue en retour un GtkWidget utilisable. Cependant, cette méthode ne permet pas l'intégration parfaite au sein de la bibliothèque. Le widget résultant n’est que le widget de base choisi sur lequel on a apporté des modifications.

Gtk+ met à notre disposition des outils qui nous permettent de pratiquer la programmation orientée objet tout en écrivant en C. On pourra alors dériver notre widget personnel à partir d'un widget existant, voire même partir de la racine qui est le GObject.

Il existe déjà deux tutoriels sur ce sujet, au demeurant excellents, que je vous conseille vivement de consulter en parallèle de celui-ci (Créer son widget GTK+ en Langage C et Création de signaux pour vos widgets GTK+). J'ai décidé d'en écrire un troisième parce que ces derniers datent de 2006 et traitent des GtkObject (Gtk+ v2.0). Une mise à jour s'impose, d’autant que Gtk+ v4 est déjà là.

Au vu des connaissances nécessaires en C et Gtk+, cet article s’adresse à un public maîtrisant correctement les deux.

2. Principe des objets selon Gtk+

Le langage C n'est pas à priori destiné à faire de la programmation objet. On peut toutefois y tendre. Gtk+ nous propose tous les outils nécessaires pour y parvenir.

Il existe un objet racine dont tous les autres vont dériver : le GObject. Si vous désirez créer un widget à partir de rien, c'est de cet objet qu'il faudra partir. Prenez par exemple les GdkPixbuf. Ils ne sont pas des GtkWidget mais dérivent des GObject. On peut donc les gérer comme tout widget de la bibliothèque.

3. Description du widget que nous allons créer

Pour que ce tutoriel soit le plus compréhensible possible, nous allons expliquer tout le processus de développement à partir d’un exemple concret.

Imaginons que nous sommes en train de développer une application de traitement d’images. Nous désirons disposer d’un affichage de toutes les images en miniature dans une fenêtre à la manière d’une table lumineuse, comme le fait Darktable par exemple.

Image non disponible

Ne voulant pas recopier ce logiciel, nous nous inspirons de cet exemple pour obtenir de notre côté une miniature de ce genre :

Image non disponible

Chaque image sera affichée avec un fond type pellicule photo 24x36. Il sera possible de :

  • sélectionner ou désélectionner une miniature avec un clic gauche de la souris ;
  • réduire ou agrandir l’image avec la molette de défilement de la souris ;
  • exécuter une fonction callback avec un signal propre à ce nouveau widget lors d’un double-clic gauche de la souris ;

Toutes ces possibilités vont nous permettre de balayer l’essentiel de programmation d’un nouveau widget.

Nous appellerons ce nouveau widget MyDiapo. Il dérivera des GtkDrawingArea.

Pour respecter la philosophie des développeurs Gtk+, nous ne devons pas nommer nos widgets personnels avec le préfixe Gtk. Ceci pour ne pas entrer en conflit avec de futurs widgets officiels qui porteraient malencontreusement le même nom.

4. De quels outils disposons-nous ?

Les développeurs de Gtk+ ont mis en place un mécanisme comprenant des macros, des structures de données et des fonctions spécifiques pour permettre une programmation orientée objet en C.

Les macros vont permettre de générer d'autres macros et des prototypes de fonctions que nous utilisons tout le temps. Par exemple, celle qui nous permet d'accéder à un type particulier d'un objet : GTK_WIDGET(). Chaque objet possède le sien.

Avec les macros, nous disposons aussi de différents prototypes de fonctions qui permettent d'intégrer notre nouvel objet à la bibliothèque Gtk+. À chaque initialisation d'un nouvel objet, différentes opérations vont s'exécuter en interne, dans un ordre précis. Le respect de ces prototypes va nous le garantir.

4-1. Les macros

Il en existe plusieurs. Certaines seront utilisées dans le fichier d’entête de notre nouvel objet tandis que d’autres devront s’insérer dans le fichier source associé.

4-1-1. Les macros du fichier d’entête

Nous ne disposons ici que de trois macros :

4-1-1-1. G_DECLARE_FINAL_TYPE()

Cette macro permet de dériver notre objet d’un objet parent. Elle interdit à ce nouvel objet d’être dérivable à son tour.

Son prototype :

#define G_DECLARE_FINAL_TYPE(ModuleObjName, module_obj_name, MODULE, OBJ_NAME, ParentName)

Partons de notre widget pour remplir cette macro :

G_DECLARE_FINAL_TYPE(MyDiapo, my_diapo, MY, DIAPO, GtkDrawingArea)

Il est important ici de bien respecter la casse.

Le premier argument est le nom complet, le deuxième correspond au début des différentes fonctions qui y sont associées, les troisième et quatrième permettent de créer de nouvelles macros (par exemple MY_DIAPO() et MY_IS_DIAPO()). Quant au dernier, il s’agit du widget parent à partir duquel notre nouveau widget dérive.

4-1-1-2. G_DECLARE_DERIVABLE_TYPE()

Comme la précédente, cette macro permet de dériver notre objet d’un objet parent. Elle est, cette fois-ci, elle-même dérivable.

Son prototype :

#define G_DECLARE_DERIVABLE_TYPE(ModuleObjName, module_obj_name, MODULE, OBJ_NAME, ParentName)

Pour notre widget cela donnera :

G_DECLARE_DERIVABLE_TYPE(MyDiapo, my_diapo, MY, DIAPO, GtkWidgetArea)

Il est important ici de bien respecter la casse.

L’écriture est identique à la macro précédente.

4-1-1-3. G_DECLARE_INTERFACE()

Cette dernière macro, comme son nom le laisse présager, permet de déclarer une interface.

Son prototype :

#define G_DECLARE_INTERFACE(ModuleObjName, module_obj_name, MODULE, OBJ_NAME, PrerequisiteName)

Son utilisation est de la même forme que les deux précédentes. Cependant, comme elle déclare notre objet comme étant une interface, nous n’irons pas plus loin dans son descriptif. Ce n’est pas le but de ce tutoriel. Reportez-vous à la documentation officielle pour plus de détail.

4-1-2. Les macros du fichier source

Dans le fichier source, il va nous falloir aussi utiliser quelques macros pour définir le nouveau type de notre widget. Ces macros commencent toutes par G_DEFINE_TYPE… Je ne vous présenterai ici que trois d’entre elles. Ce sont celles en définitive que nous utiliserons en fonction de nos besoins à chaque création d’un nouvel objet.

Elles créent un nouveau type pour notre objet, différentes fonctions internes dont la fonction Gtype my_radio_get_type();.

Toutes ces macros dérivent de G_DEFINE_TYPE_EXTENDED(). Je vous invite à aller voir la documentation officielle à son sujet pour de plus amples informations.

4-1-2-1. G_DEFINE_TYPE()

C’est la macro générique de base.

Son prototype :

#define G_DEFINE_TYPE(TN, t_n, T_P)

Si nous devons l’utiliser, voilà comment la remplir :

G_DEFINE_TYPE (MYDIAPO, my_diapo, GTK_TYPE_DRAWING_AREA)

4-1-2-2. G_DEFINE_TYPE_WITH_PRIVATE()

Cette macro effectue les mêmes opérations que la macro précédente. Elle ajoute un pointeur sur des données privées.

Lorsque vous utilisez un GtkWidget, vous rencontrez très souvent l'envie de voir quelles sont les variables internes pour les modifier à la main, ce qui est à proscrire. La documentation vous indique que les données sont privées. Cette macro produit cet effet. C'est, je dois dire, ma préférée.

Son prototype :

#define G_DEFINE_TYPE_WITH_PRIVATE(TN, t_n, T_P)

Pour l’utiliser avec notre widget :

G_DEFINE_TYPE_WITH_PRIVATE(MyDiapo, my_diapo, GTK_TYPE_DRAWING_AREA)

4-1-2-3. G_DEFINE_TYPE_WITH_CODE()

Identique à G_DEFINE_TYPE()G_DEFINE_TYPE() mais permet d'insérer du code exécutable.

Son prototype :

#define G_DEFINE_TYPE_WITH_CODE(TN, t_n, T_P, _C_)

Les trois premiers paramètres sont identiques aux précédentes macros. Le dernier, _C_, correspond au code que l’on désire insérer à la fonction my_diapo_get_type(); créée à l’appel de la macro.

Pour notre exemple, cette macro ne nous sera d’aucune utilité.

4-2. Les fonctions

Dans le fichier source, il va nous falloir initialiser notre nouvel objet avec un processus précis. Nous allons créer deux fonctions spécifiques qui porteront dans leur nom le préfixe de notre objet. Ici le préfixe sera my_diapo.

  • my_diapo_class_init(); Cette fonction initialise la parenté de notre objet. Elle affecte les pointeurs sur les fonctions d'initialisation, de vie et de destruction de l'objet. Elle affecte aussi les pointeurs sur des fonctions callbacks que l'on peut assigner à certains signaux ;
  • my_diapo_init(); Cette fonction est appelée en dernier. C'est ici qu'on initialise les variables internes (privées) avant de rendre la main à l'utilisateur.

5. Déclaration de notre nouveau widget MyDiapo

Nous entrons dans le vif du sujet.

Pour commencer, nous disposerons notre code dans deux fichiers distincts : un fichier d'entête et un fichier pour le code source.

5-1. Le fichier d'entête mydiapo.h

Ce fichier doit toujours commencer par la définition d'une macro. Son prototype est spécifique et doit être respecté.

Puisque nous créons un widget MyDiapo, la macro à définir s'écrira ainsi : MY_DIAPO. Pour parfaire le tout, elle sera encadrée par deux caractères de soulignement (tiret du 8) de part et d'autre.

Après cette condition/création d'une macro, nous incluons les bibliothèques nécessaires. Puis viennent deux macros : G_BEGIN_DECLS et G_END_DECLS.

Notre code se trouvera encadré par ces deux balises.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
#ifndef __MY_DIAPO_H__
#define __MY_DIAPO_H__

#include <gtk/gtk.h>

G_BEGIN_DECLS

// Ici notre code

G_END_DECLS

#endif

Il est primordial de respecter la structure de ce fichier pour que la compilation s’effectue sans heurt.

Nous sommes fin prêts pour déclarer notre nouveau widget. Déclarons notre nouveau type grâce à la macro G_DECLARE_FINAL_TYPE () :

 
Sélectionnez
G_DECLARE_FINAL_TYPE(MYDiapo, my_diapo, MY, DIAPO, GtkDrawingArea)

Elle va nous permettre d'indiquer, entre autres, de quel objet dérive notre widget. Nous voyons en dernier paramètre GtkDrawingArea. Le choix de G_DECLARE_FINAL_TYPE() est arbitraire ici. Il n’y a, à priori, aucune raison pour que cet objet soit dérivable.

Il nous faut encore créer une macro que nous faisons de manière explicite. La fonction my_diapo_get_type(); va être automatiquement générée par Gtk+ :

 
Sélectionnez
#define MY_TYPE_DIAPO my_diapo_get_type ()

Faisons le choix d'avoir un widget avec des variables privées. En tant que programmation objet, c'est, il me semble, la meilleure façon de procéder. Ce choix est personnel. Vous pouvez à votre guise choisir une autre voie.

Si nous voulons autoriser l'accès à une variable, nous mettrons à disposition les fonctions get();/set(); nécessaires. Il sera aussi possible d’y accéder depuis les propriétés des GObject. Pour toutes les autres variables, l’utilisateur final ne saura pas qu’elles existent.

En partant de ce postulat, il nous faut créer une structure pour nos données privées. Ceci se fait de la manière suivante :

 
Sélectionnez
typedef struct _MyDiapoPrivate MyDiapoPrivate;

Remarquons une nouvelle fois que la casse est très importante. Ne mélangeons pas les minuscules et les majuscules et respectons le nom de notre widget. Cette structure contiendra, dans le fichier source, toutes les déclarations des variables privées de notre objet.

Pour finir, il ne nous reste plus qu'à déclarer deux structures. Une pour notre objet lui-même, l'autre pour son lien de parenté.

 
Sélectionnez
struct _MyDiapo {
  GtkDrawingArea parent;
  GtkDiapoPrivate *priv;
};

struct _MyDiapoClass {
  GtkDrawingAreaClass parent_class;
};

La structure _MyDiapo contient un pointeur priv de type MyDiapoPrivate. C'est par ce pointeur que nous accéderons, en interne, à nos variables privées.

Notre fichier d'entête commence à prendre tournure. En voilà un résumé :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
#ifndef __MY_DIAPO_H__
#define __MY_DIAPO_H__

#include <gtk/gtk.h>

G_BEGIN_DECLS

G_DECLARE_FINAL_TYPE(MyDiapo, my_diapo, MY, DIAPO, GtkDrawingArea)

#define MY_TYPE_DIAPO(my_diapo_get_type ())

typedef struct _MyDiapoPrivate MyDiapoPrivate;

struct _MyDiapo {
  GtkDrawingArea parent;
  MyDiapoPrivate *priv;
};

struct _MyDiapoClass {
  GtkDrawingAreaClass parent_class;
};

G_END_DECLS

#endif

Il ne nous reste plus qu'à ajouter les déclarations des fonctions nécessaires à l'utilisation de notre futur widget. Bien entendu, nous commençons par le constructeur :

 
Sélectionnez
GtkWidget *my_diapo_new (const gchar *filename);

Le prototype de cette fonction est arbitraire. Il est possible de lui donner la forme que l’on désire. Il est possible de créer plusieurs constructeurs s’il y a une nécessité. Ici nous transmettons au widget l’image à insérer. Nous verrons dans le code source comment filename est géré.

Vient ensuite une fonction pour permettre de changer l'image et deux autres pour fixer ou renvoyer la taille du widget. Ces fonctions sont là pour l'exemple. À vous de créer celles dont vous avez besoin.

Voilà le code complet de notre fichier d'entête :

MyDiapo.h
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
#ifndef __MY_DIAPO_H__
#define __MY_DIAPO_H__

#include <gtk/gtk.h>

G_BEGIN_DECLS

G_DECLARE_FINAL_TYPE(MyDiapo, my_diapo, MY, DIAPO, GtkDrawingArea)

#define MY_TYPE_DIAPO(my_diapo_get_type ())

typedef struct _MyDiapoPrivate MyDiapoPrivate;

struct _MyDiapo {
  GtkDrawingArea parent;

  MyDiapoPrivate *priv;
};

struct _MyDiapoClass {
   GtkDrawingAreaClass parent_class;
};

// Création d'une instance. (filename peut être NULL)
GtkWidget *my_diapo_new(const gchar *filename);

// Charge ou change l'image insérée
void my_diapo_set_image(MyDiapo *diapo, const gchar *filename);

// Définit/récupère la taille du widget
void my_diapo_set_size(MyDiapo *diapo, int *width, int *height);
void my_diapo_get_size(Myiapo *diapo, int *width, int *height);

G_END_DECLS

#endif

5-2. Le fichier source mydiapo.c

5-2-1. Les déclarations

La première chose à faire est d'inclure notre fichier d'entête et de déclarer le contenu de notre structure de données privées MyDiapoPrivate :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
#include "mydiapo.h"

/* Définition de la structure privée. */
struct _MyDiapoPrivate
{
  gchar *filename;
  GdkPixbuf *originalpixbuf;

  gint width, height;
  gboolean selected;
};

Nous aurons donc cinq variables privées. Seules les variables width et height seront accessibles via les deux fonctions que nous avons déclarées précédemment dans le fichier d'entête.

Pour que Gtk+ sache que nous avons une structure privée, nous devons utiliser une dernière macro à cet effet :

 
Sélectionnez
G_DEFINE_TYPE_WITH_PRIVATE(MyDiapo, my_diapo, GTK_TYPE_DRAWING_AREA)

5-2-2. Les prototypes des fonctions internes

Dans la fonction constructeur, nous utilisons la fonction g_object_new();. Elle exécute à son tour deux fonctions particulières comme décrit au chapitre « Les fonctionsLes fonctions » :

  • my_diapo_class_init(); ;
  • my_diapo_init();.
5-2-2-1. static void my_diapo_class_init();

Cette fonction va affecter des pointeurs de fonction pour initialiser le widget et affecter des pointeurs de fonctions callbacks pour différents signaux que nous désirons gérer.

Pour décortiquer tout ce petit monde, voilà le code source :

my_diapo_class_init (MyDiapoClass *class)
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
static void
my_diapo_class_init(MyDiapoClass *class)
{
  G_OBJECT_CLASS(class)->finalize = my_diapo_finalize;

  GTK_WIDGET_CLASS(class)->button_press_event = my_diapo_on_button_press_event;
  GTK_WIDGET_CLASS(class)->scroll_event = my_diapo_on_scroll_event;
  GTK_WIDGET_CLASS(class)->draw = my_diapo_on_draw;
}

Les GObject disposent d'une structure GObjectClass. Cette structure contient entre autres des pointeurs de fonction que nous pouvons initialiser.

En programmation orientée objet, elles se nomment méthodes virtuelles.

Extrait de la documentation officielle :

GObjectClass
Sélectionnez
struct GObjectClass {
  GTypeClass   g_type_class;

  /* seldom overridden */
  GObject*   (*constructor)     (GType                  type,
                                 guint                  n_construct_properties,
                                 GObjectConstructParam *construct_properties);
  /* overridable methods */
  void       (*set_property)            (GObject        *object,
                                         guint           property_id,
                                         const GValue   *value,
                                         GParamSpec     *pspec);
  void       (*get_property)            (GObject        *object,
                                         guint           property_id,
                                         GValue         *value,
                                         GParamSpec     *pspec);
  void       (*dispose)                 (GObject        *object);
  void       (*finalize)                (GObject        *object);
  /* seldom overridden */
  void       (*dispatch_properties_changed) (GObject      *object,
                                             guint         n_pspecs,
                                             GParamSpec  **pspecs);
  /* signals */
  void       (*notify)                  (GObject&#160;*object,
                                         GParamSpec&#160;*pspec);

  /* called when done constructing */
  void       (*constructed)             (GObject&#160;*object);
};

Tous ces pointeurs de fonction ne sont pas à initialiser obligatoirement.

Ces fonctions doivent être chaînées avec la même fonction de la classe parente. Pour ce faire, on utilise une macro dédiée (code exemple) : G_OBJECT_CLASS(my_diapo_parent_class)->dispose (object);

On accède à tous ces pointeurs depuis la fonction my_diapo_class_init(MyDiapoClass *class);. Si nous regardons son code source, les pointeurs nécessaires pour notre objet sont initialisés avec une fonction que nous aurons préalablement déclarée sous forme de prototype.

Une remarque avant d’aller plus loin. Le pointeur de fonction constructor est à utiliser avec parcimonie. Il existe déjà une fonction constructor interne à Gtk+. Si vous désirez l'écraser et utiliser la vôtre, alors il vous faudra initialiser ce pointeur avec l'adresse de votre fonction. Dans la majorité des cas, il ne sera pas nécessaire d'y toucher.

Pour vous donner un exemple concret, voilà l’ordre d’appel des différentes fonctions lors de la vie d’un GObject hormis la fonction constructor que je n’ai pas utilisée :

Ordre d’exécution des différents callback
Sélectionnez
gtk_diapo_class_init(GtkDiapoClass *class);
gtk_diapo_init(GtkDiapo *diapo);
gtk_diapo_constructed(GObject *object);
gtk_diapo_dispatch_properties_changed(GObject *object);
gtk_diapo_dispatch_properties_changed(GObject *object);
gtk_diapo_dispatch_properties_changed(GObject *object);
gtk_diapo_dispatch_properties_changed(GObject *object);
gtk_diapo_dispatch_properties_changed(GObject *object);
gtk_diapo_dispatch_properties_changed(GObject *object);
gtk_diapo_dispatch_properties_changed(GObject *object);
gtk_diapo_dispose(GObject *object);
gtk_diapo_dispatch_properties_changed(GObject *object);
gtk_diapo_dispose(GObject *object);
gtk_diapo_finalize(GObject *object);

Ensuite, toujours dans la même fonction, nous pouvons affecter des pointeurs de fonction pour les signaux du GtkWidget parent. Cette fois-ci, la structure qui contient ces pointeurs est le GtkWidgetClass. Si nous nous référons à la documentation officielle, nous pouvons accéder à pratiquement tous les signaux d'un GtkWidget standard. Pour l'exemple qui nous concerne, nous aurons besoin des signaux suivants :

  • draw pour dessiner dans notre GtkDrawingArea ;
  • button_press_event pour gérer les clics de la souris ;
  • scroll_event pour gérer le bouton scroll de la souris.

Le principe revient à attacher des fonctions callbacks aux signaux désirés. Nous pourrions utiliser en lieu et place la fonction g_signal_connect(); pour obtenir le même résultat. Cependant, les prototypes des fonctions associées ne sont pas tout à fait les mêmes. Vous trouverez leur description dans la description d'un GtkWidgetClass.

Extrait de la documentation officielle :

GtkWidgetClass
Cacher/Afficher le codeSélectionnez
5-2-2-2. static void my_diapo_init()

Cette fonction initialise les paramètres de notre widget.

Comme pour la fonction précédente, commençons par voir le code source :

my_diapo_init (MyDiapo *diapo)
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
static void my_diapo_init(MyDiapo *diapo)
{
  MyDiapoPrivate *priv = my_diapo_get_instance_private(diapo);

  priv->filename = NULL;
  priv->originalpixbuf = NULL;
  priv->width = 100;
  priv->height = 100;
  priv->selected = FALSE;

  // Autorise les bulles d’aide
  gtk_widget_set_has_tooltip(GTK_WIDGET(diapo), TRUE);

  // Fixe la taille de départ à 100x100
  gtk_widget_set_size_request(GTK_WIDGET(diapo), priv->width, priv->height);
 
  // Ajout de deux écouteurs au widget (gestion de la souris)
  gtk_widget_add_events(GTK_WIDGET(diapo), GDK_BUTTON_PRESS_MASK | GDK_SCROLL_MASK);
}

La ligne 4 nous permet d'accéder aux données privées du widget. Nous initialisons à la création de l’objet les pointeurs privés à NULL. La taille par défaut quant à elle est fixée à 100x100 pixels. Le booléen « selected » permet de modifier l’affichage de notre widget en fonction de son état sélectionné ou désélectionné.

Pour finir, nous ajoutons deux écouteurs qui vont permettre de gérer les clics et les boutons de défilement de la souris.

Résumons un peu le travail déjà accompli. Actuellement, notre source se présente ainsi : 

 
Sélectionnez
#include "mydiapo.h"

/* Private structure definition. */
struct _MyDiapoPrivate
{
  gchar *filename;
  GdkPixbuf *originalpixbuf;

  gint width, height;
  gboolean selected;
};

G_DEFINE_TYPE_WITH_PRIVATE(MyDiapo, my_diapo, GTK_TYPE_DRAWING_AREA)

static void my_diapo_class_init(MyDiapoClass *class);
static void my_diapo_init(MyDiapo *diapo);
static void my_diapo_finalize(GObject * object);

// callbacks
static gboolean my_diapo_on_draw(GtkWidget *widget, cairo_t *cr);
static gboolean my_diapo_on_button_press_event(GtkWidget *widget, GdkEventButton *event);
static gboolean my_diapo_on_scroll_event(GtkWidget *widget, GdkEventScroll *event);

static void my_diapo_class_init(MyDiapoClass *class)
{
  G_OBJECT_CLASS(class)->finalize = my_diapo_finalize;

  GTK_WIDGET_CLASS(class)->button_press_event = my_diapo_on_button_press_event;
  GTK_WIDGET_CLASS(class)->scroll_event = my_diapo_on_scroll_event;
  GTK_WIDGET_CLASS(class)->draw = my_diapo_on_draw;
}

static void my_diapo_init(MyDiapo *diapo)
{
  MyDiapoPrivate *priv = my_diapo_get_instance_private(diapo);

  priv->filename = NULL;
  priv->originalpixbuf = NULL;
  priv->width = 100;
  priv->height = 100;
  priv->selected = FALSE;

  // Autorise les bulles d’aide
  gtk_widget_set_has_tooltip(GTK_WIDGET(diapo), TRUE);

  // Fixe la taille de départ à 100x100
  gtk_widget_set_size_request(GTK_WIDGET(diapo), priv->width, priv->height);
 
  // Ajout de deux écouteurs au widget (gestion de la souris)
  gtk_widget_add_events(GTK_WIDGET(diapo), GDK_BUTTON_PRESS_MASK | GDK_SCROLL_MASK);
}

Plusieurs prototypes sont déclarés. Nous détaillerons plus loin chacune de ces fonctions.

5-2-3. Initialisation et destruction de l’objet

5-2-3-1. Le constructeur

Lorsque nous créons un objet, nous avons besoin d’une fonction (méthode en langage orienté objet) pour le construire.

Il est possible d’écrire plusieurs constructeurs avec des prototypes différents. Bien entendu la surcharge de fonction est ici impossible. N’oublions pas que nous programmons en C.

Reprenons notre prototype de notre unique constructeur et écrivons son code source.

GtkWidget *my_diapo_new (const gchar *filename);

En premier lieu nous utilisons la fonction g_object_new(); pour initialiser notre nouvel objet comme suit :

 
Sélectionnez
GtkWidget *my_diapo_new(const gchar *filename) {
  MyDiapo *diapo;

  diapo = g_object_new(MY_TYPE_DIAPO, NULL);
  if (!diapo) {
    g_printerr ("Erreur d'initialisation du nouvel objet dans %s\n", __func__);
    return NULL;
  }

Cette fonction prend en premier argument le type d’objet que nous désirons créer. Les arguments suivants peuvent être une liste de données que le widget parent attend. Par exemple, si nous dérivons d’un GtkButton, il pourrait être intéressant de lui transmettre un label.

SI tout se passe correctement, nous obtenons en retour un pointeur dûment initialisé. Dans le cas contraire, la valeur NULL est renvoyée. Il nous faut donc tester cette valeur avant de continuer.

À ce stade notre objet est opérationnel. Nous traitons maintenant le paramètre transmis. Autorisons la possibilité de transmettre la valeur NULL. Cette valeur sera testée lors de l’affichage proprement dit. Si elle vaut NULL, une image indiquant qu’il n’y a pas d’image sera affichée. S’il y a un chemin non valide, une image indiquant une erreur sera affichée.

Pour initialiser le pointeur filename qui se trouve dans les données privées, il nous faut avant tout accéder au pointeur de la structure qui le contient. Ceci se fait grâce à la ligne suivante :

 
Sélectionnez
MyDiapoPrivate *priv = my_diapo_get_instance_private(diapo);

Il ne nous reste plus qu’à affecter la valeur transmise en paramètre au pointeur privé filename.

La fonction my_diapo_load_image(); permet de charger l’image en mémoire et d’affecter son pointeur au pointeur privé originalpixbuf.

Le code source final de notre constructeur :

GtkWidget *my_diapo_new (const gchar *filename)
Sélectionnez
GtkWidget *my_diapo_new(const gchar *filename)
{
  MyDiapo *diapo;

  diapo = g_object_new(MY_TYPE_DIAPO, NULL);
  if (!diapo) {
    g_printerr ("Erreur d'initialisation du nouvel objet dans %s\n", __func__);
    return NULL;
  }

  MyDiapoPrivate *priv = my_diapo_get_instance_private(diapo);

  priv->filename = g_strdup(filename);

  my_diapo_load_image(diapo);

  return GTK_WIDGET(diapo);
}

Cette fonction retourne un GtkWidget. Nous pourrions retourner directement un MyDiapo mais ce n’est pas la philosophie de Gtk+. Ceci se pratique plutôt pour des objets qui héritent directement des GObject, comme les GdkPixbuf par exemple.

5-2-3-2. Le destructeur

Lors de la vie de l’objet, il peut être amené à effectuer des allocations dynamiques de mémoire dans le tas. C’est d’ailleurs le cas ici avec priv→filename et priv→originalpixbuf. Il est donc primordial de s’assurer, lors de sa destruction, que ces allocations soient libérées. C’est le rôle du destructeur.

La fonction appelée est attachée au pointeur de fonction finalize du GObject parent. Par commodité, appelons cette fonction du même nom. Il nous faudra s’assurer de la libération des allocations dans le tas qui auront été faites.

Comme le code source de cette fonction pour notre objet est court, autant le voir maintenant :

 
Sélectionnez
static void my_diapo_finalize(GObject *object)
{
 MyDiapoPrivate *priv = my_diapo_get_instance_private(MY_DIAPO (diapo));

 if (priv->filename)
  g_free(priv->filename);

 if (GDK_IS_PIXBUF(priv->originalpixbuf))
  g_object_unref(priv->originalpixbuf);

  G_OBJECT_CLASS(my_diapo_parent_class)->finalize(object);
}

La première ligne nous permet d’accéder aux données privées. Nous l’avons déjà vu pour le constructeur. Nous ne reviendrons plus sur son utilité dorénavant. La seule différence ici est le transtypage du GObject transmis en type MyDiapo.

Ensuite vient la libération mémoire de filename si nécessaire. Rappelons-nous que nous nous donnons la possibilité d’avoir ce pointeur à NULL. Nous libérons ensuite l’image si elle existe.

La dernière ligne est une nouveauté. Pour rappel, la bibliothèque Gtk+ permet d’instancier les objets. C'est-à-dire de créer à partir d’un objet d’autres objets identiques. Ce ne sont pas des copies à proprement parler. Modifier un objet les modifie tous.

Lors de la destruction, cette instruction va permettre de détruire toutes les instances existantes. Elle est donc nécessaire.

5-2-4. Accès aux données privées par l’utilisateur

Comme indiqué au chapitre « le fichier d’entêteLe fichier d'entête mydiapo.h », l’utilisateur ne pourra pas accéder à toutes les données internes. Il nous faut lui fournir des fonctions pour cela.

Tout d’abord, nous lui autorisons la possibilité de changer l’image avec la fonction suivante :

Void my_diapo_set_image(MyDiapo *diapo, const gchar *filename)
Sélectionnez
void my_diapo_set_image(MyDiapo *diapo, const gchar *filename)
{
  g_return_if_fail(MY_IS_DIAPO(diapo));

  MyDiapoPrivate *priv = my_diapo_get_instance_private(diapo);

  if (priv->filename)
    g_free(priv->filename);

  if (GDK_IS_PIXBUF(priv->originalpixbuf))
    g_object_unref(priv->originalpixbuf);

  priv->filename = g_strdup(filename);

  gtk_diapo_load_image(diapo);
}

Rien de particulier dans ce code. Nous libérons la mémoire des anciens pointeurs pour affecter les nouveaux. Puis nous chargeons la nouvelle image, comme dans le constructeur. D’ailleurs, nous pourrions utiliser cette fonction dans le constructeur pour initialiser notre image.

Seule la première ligne est nouvelle. Nous utilisons la macro MY_IS_DIAPO() pour nous assurer que l’utilisateur transmet bien un pointeur correctement initialisé. Dans le cas contraire, g_return_if_fail(); sort de la fonction en envoyant un message d’erreur sur le canal stderr.

Tester les paramètres en entrée doit être un réflexe.

5-2-4-1. void my_diapo_set_size();

C’est grâce à cette fonction et la suivante que l’utilisateur va pouvoir interagir avec les données internes de notre objet. Ici elle permet de change la taille.

Pourquoi déclarer des pointeurs et non des entiers pour les paramètres de la taille ?

Cette petite subtilité permet de ne modifier qu’un des deux paramètres sans en connaître l’autre.

Void my_diapo_set_size(MyDiapo *diapo, gint *width, gint *height)
Sélectionnez
void my_diapo_set_size(MyDiapo *diapo, gint *width, gint *height)
{
  g_return_if_fail(MY_IS_DIAPO(diapo));
  
  MyDiapoPrivate *priv = my_diapo_get_instance_private(diapo);

  if (width && *width>=80)
    priv->width = *width;

  if (height && *height>=80)
    priv->height = *height;

  // Mise à jour de la taille
  gtk_widget_set_size_request(GTK_WIDGET(diapo), priv->width, priv->height);
}

Nous interdisons une taille inférieure à 80 pixels. Encore une fois c’est un choix arbitraire. La mise à jour de la taille du GtkWidget va activer automatiquement sa mise à jour graphique.

5-2-4-2. void my_diapo_get_size();

Cette fonction permet de récupérer la taille du widget. Il n’y a pas grand-chose à dire sur ce code très simple. Elle permet de ne récupérer qu’une seule donnée si besoin en fournissant un pointeur NULL à l’autre.

Void my_diapo_get_size(MyDiapo *diapo, int *width, int *height)
Sélectionnez
void my_diapo_get_size(MyDiapo *diapo, int *width, int *height)
{
  g_return_if_fail (MY_IS_DIAPO (diapo));

  MyDiapoPrivate *priv = my_diapo_get_instance_private(diapo);

  if (width)
    *width = priv->width;

  if (height)
    *height = priv->height;
}

5-2-5. Les fonctions privées

Nous allons décrire toutes les fonctions privées (déclarées static) qui sont le moteur interne de notre objet.

Pour rappel, leur prototype a été déclaré en début du fichier source. Nous allons en ajouter quelques autres pour pouvoir avoir un code cohérent :

 
Sélectionnez
// Dessine un pourtour du type pellicule
static void my_diapo_create_pellicule(MyDiapo *diapo, cairo_t *cr);

// Insère l'image à la bonne taille dans le widget
static void my_diapo_put_image(MyDiapo *diapo, cairo_t *cr);

// Charge l'image désignée par filename
static void my_diapo_load_image(MyDiapo *diapo);

// Crée un GdkPixbuf blanc dans lequel le text1 est affiché au-dessus de text2, le tout en rouge
static GdkPixbuf *my_diapo_image_with_text(MyDiapo *diapo, const gchar *text1, const gchar *text2);
5-2-5-1. static void my_diapo_create_pellicule();

Cette fonction dessine les traits discontinus en haut et en bas de notre widget pour simuler une pellicule photo. Ce code utilise Cairo pour dessiner dans un cairo context. Bien évidemment ce context sera, lors de l’appel, celui de notre objet.

static void my_diapo_create_pellicule(MyDiapo *diapo, cairo_t *cr)
Sélectionnez
static void my_diapo_create_pellicule(MyDiapo *diapo, cairo_t *cr)
{
  MyDiapoPrivate *priv = my_diapo_get_instance_private(diapo);
  gint offsetX=priv->width*10/100;
  gint offsetY=priv->height*10/100;
  if (priv->width>=priv->height)
    offsetX = offsetY;
  else
    offsetY = offsetX;

  gint lineWidth = offsetY*70/100;
  gdouble dashes[2];
  dashes[0] = priv->width/18;
  dashes[1] = priv->width/36;

  // Création des tirets blancs en haut et en bas de l'image dans la bordure noire
  cairo_set_source_rgb (cr, 1, 1, 1);
  cairo_set_line_width (cr, lineWidth);
  cairo_set_dash (cr, dashes, 2, 0);
  cairo_move_to (cr, 0, (offsetY)/2);
  cairo_line_to (cr, priv->width, (offsetY)/2);
  cairo_move_to (cr, 0, priv->height- (offsetY)/2);
  cairo_line_to (cr, priv->width, priv->height - (offsetY)/2);
  cairo_stroke (cr);
}
5-2-5-2. static void my_diapo_put_image();

Cette fonction calcule un nouveau GdkPixbuf à la bonne taille en partant de l’original. Cette nouvelle image est ensuite appliquée sur le context graphique.

static void my_diapo_put_image(MyDiapo *diapo, cairo_t *cr)
Sélectionnez
static void my_diapo_put_image(MyDiapo *diapo, cairo_t *cr)
{
  MyDiapoPrivate *priv = my_diapo_get_instance_private(diapo);
  GdkPixbuf *subPixbuf = NULL;
  int offsetX=priv->width*10/100;
  int offsetY=priv->height*10/100;
  if (priv->width>=priv->height)
    offsetX = offsetY;
  else
    offsetY = offsetX;

  // Création d'un pixbuf réduit à partir de l'original pour s'insérer dans le widget à la bonne taille en tenant compte des marges
  subPixbuf = gdk_pixbuf_scale_simple(priv->originalpixbuf, priv->width-offsetX*2, priv->height-offsetY*2, GDK_INTERP_NEAREST);

  // Insertion du pixbuf dans le context de cairo (affichage dans le GtkDrawingArea)
  gdk_cairo_set_source_pixbuf(cr, subPixbuf, offsetX, offsetY);

  // Mise à jour de l'affichage du context modifié
  cairo_paint (cr);

  // Suppression du pixbuf devenu inutile
  g_object_unref(subPixbuf);
}
5-2-5-3. static void my_diapo_load_image();

Cette fonction détermine quelle image il faut insérer et à quelle taille exactement en fonction de la demande de l’utilisateur.

 
Sélectionnez
static void my_diapo_load_image(MyDiapo *diapo)
{
  MyDiapoPrivate *priv = my_diapo_get_instance_private(diapo);
  GError *error= NULL;
  if (priv->filename)
    {
      priv->originalpixbuf = gdk_pixbuf_new_from_file(priv->filename, &error);

      if (!priv->originalpixbuf)
    {
      priv->originalpixbuf = my_diapo_image_with_text(diapo, "error", "image");
      gtk_widget_set_tooltip_text(GTK_WIDGET (diapo), error->message);
      g_error_free(error);
    }
      else
    gtk_widget_set_tooltip_text(GTK_WIDGET (diapo), priv->filename);
    }
  else
    {
      priv->originalpixbuf = my_diapo_image_with_text(diapo, "empty", "image");
      gtk_widget_set_tooltip_text(GTK_WIDGET(diapo), "image vide");
    }

  gdouble rapport = gdk_pixbuf_get_width(priv->originalpixbuf) / gdk_pixbuf_get_height(priv->originalpixbuf);

  // Calcul du redimensionnement
  if (rapport<1) // Photo en portrait
    priv->height = priv->height * rapport;
  else // Photo en paysage
    priv->width = priv->width * rapport;
}
5-2-5-4. static GdkPixbuf *my_diapo_image_with_text();

Cette fonction crée un GdkPixbuf qui affiche le message text1 / text2 avec text1 au-dessus de text2.

static GdkPixbuf*my_diapo_image_with_text(MyDiapo *diapo, const gchar *text1, const gchar *text2)
Sélectionnez
static GdkPixbuf*my_diapo_image_with_text(MyDiapo *diapo, const gchar *text1, const gchar *text2)
{
  GdkPixbuf *pixbuf;
  cairo_surface_t *surface;
  cairo_t *cr;
  MyDiapoPrivate *priv = my_diapo_get_instance_private(diapo);

  // Création d'une surface dans laquelle on va pouvoir dessiner avec les fonctions de cairo
  surface= cairo_image_surface_create(CAIRO_FORMAT_ARGB32, priv->width, priv->height);

  // Création d'un context en fonction de la surface nouvellement créée.
  cr = cairo_create(surface);

  // Mise au blanc de la surface
  cairo_set_source_rgba(cr, 1, 1, 1, 1);
  cairo_paint(cr);

  // On fixe la taille de la police
  cairo_set_font_size(cr, 20);

  // Création d'un chemin d'écriture du texte en rouge.
  cairo_set_source_rgb(cr, 1, 0, 0);
  cairo_move_to(cr, 15, priv->height/2);
  cairo_text_path(cr, text1);
  cairo_move_to(cr, 15, priv->height/2+20);
  cairo_text_path(cr, text2);

  // Affichage du texte. Ici l'utilisation de fill(); permet de remplir le texte.
  // L'utilisation de stroke(); donnerait des lettres creuses.
  cairo_fill(cr);

  // Transfert de la surface dessinée dans le GdkPixbuf
  pixbuf = gdk_pixbuf_get_from_surface(surface, 0, 0, priv->width, priv->height);

  // Suppression du contexte devenu inutile
  cairo_destroy(cr);

  // Suppression de la surface devenue inutile
  cairo_surface_destroy(surface);

  return pixbuf;
}

5-2-6. Les fonctions « callback »

Maintenant que nous disposons d’outils pour créer une image affichable, il ne nous reste plus qu’à créer les fonctions callback que nous avons attachées aux pointeurs de fonction dans my_diapo_class_init ();.

Pour rappel :

 
Sélectionnez
static gboolean my_diapo_on_draw(GtkWidget *widget, cairo_t *cr);
static gboolean my_diapo_on_button_press_event(GtkWidget *widget, GdkEventButton *event);
static gboolean my_diapo_on_scroll_event(GtkWidget *widget, GdkEventScroll *event);
5-2-6-1. static gboolean my_diapo_on_draw();

La fonction d’affichage est activée par le signal « draw ». Elle efface le contexte en noir puis applique l’image. Si le booléen « selected » est vrai, un voile gris est ajouté pour simuler une sélection. Un code le plus simple possible ici.

static gboolean my_diapo_on_draw (GtkWidget *widget, cairo_t *cr)
Sélectionnez
static gboolean my_diapo_on_draw(GtkWidget *widget, cairo_t *cr)
{
 // Effacement du widget en noir
 cairo_set_source_rgba(cr, 0, 0, 0, 1);
 cairo_paint(cr);

 // Affichage de l'image dans la surface
 my_diapo_put_image(MY_DIAPO(widget), cr);

 // Dessin de la pellicule dans la surface
 my_diapo_create_pellicule(MY_DIAPO(widget), cr);

  // Ajout d'un voile gris si le widget est sélectionné
  if (priv->selected)
  {
    cairo_set_source_rgba(cr, 1, 1, 1, 0.5);
    cairo_paint(cr);
  }

  return FALSE;
}
5-2-6-2. static gboolean my_diapo_on_scroll_event();

Ce callback est activé lors d’un défilement de la souris. Nous ne traitons que les mouvements haut et bas pour agrandir ou réduire la taille de notre widget à la volée. Nous limitons la taille minimale à 80x80.

static gboolean my_diapo_on_scroll_event(GtkWidget *widget, GdkEventScroll *event)
Sélectionnez
static gboolean my_diapo_on_scroll_event(GtkWidget *widget, GdkEventScroll *event)
{
  if (event->type==GDK_SCROLL)
    {
      MyDiapoPrivate *priv = my_diapo_get_instance_private(MY_DIAPO(widget));
      gdouble factor =priv->width/priv->height;

      switch (event->direction)
    {
        case GDK_SCROLL_UP :
          {
            if (priv->width>80 || priv->height>80) {// évite de se retrouver avec un widget trop petit pour être sélectionné
          gint width = priv->width-10*factor;
          gint height = priv->height-10;
              my_diapo_set_size(MY_DIAPO (widget), &width, &height);
          break;
        }
        case GDK_SCROLL_DOWN :
          {
        gint width = priv->width+10*factor;
        gint height = priv->height+10;
        my_diapo_set_size(MY_DIAPO (widget), &width, &height);
        break;
          }
          default :
        {
          break;
        }
      }
    }
    }

  return FALSE;
}

5-2-7. Création d’un signal personnel

Nous allons voir comment créer un signal bien à nous. Histoire de corser un peu l’affaire, le prototype du callback associé sera le plus compliqué possible.

Pourquoi le compliquer ?

Les GObject permettent de créer des signaux avec des prototypes pour les callbacks associés clef en main. Les fonctions à utiliser se trouvent dans le chapitre « Closures » de la documentation officielle.

Un extrait de la documentation officielle (il y en beaucoup d’autres) :

 
Sélectionnez
void g_cclosure_marshal_VOID__VOID()
void g_cclosure_marshal_VOID__BOOLEAN()
void g_cclosure_marshal_VOID__CHAR()
void g_cclosure_marshal_VOID__UCHAR()
void g_cclosure_marshal_VOID__INT()
void g_cclosure_marshal_VOID__UINT()
...

À quoi correspondent-elles ?

Le premier terme en majuscules indique le type que renvoie la fonction callback. Le deuxième en majuscules indique le type de la variable que l’on désire récupérer en paramètre.

Plutôt qu’un long discours, prenons un exemple concret. Le signal « clicked » d’un GtkButton. Le prototype du callback associé est :

 
Sélectionnez
void user_function(GtkButton *button, gpointer user_data);

Quelle fonction dois-je utiliser pour avoir ce même prototype ?

La fonction g_cclosure_marshal_VOID__VOID(); est toute désignée. Elle ne renvoie rien et n’attend aucun paramètre.

Pourtant, il y a deux paramètres dans le callback du signal « clicked » associé ?

En effet, ces deux paramètres sont implicites. Nous les aurons donc à tous les coups.

5-2-7-1. Initialisation d’un nouveau signal

La déclaration d’un nouveau signal s’effectue dans la fonction my_diapo_class_init();. Pour ce premier exemple, nous créons un signal « clicked » qui a la même fonction et le même prototype que pour celui d’un GtkButton.

Avant toute chose nous déclarons, pour des commodités d’écriture, une énumération et un tableau qui reprennent le nombre de nouveaux signaux que nous désirons créer pour notre widget :

 
Sélectionnez
enum {
  MY_DIAPO_CLICKED_SIGNAL,
  MY_DIAPO_PERSONAL_SIGNAL,
  MY_DIAPO_NB_SIGNALS
};

static guint my_diapo_signals [MY_DIAPO_NB_SIGNALS] = { 0 };

Le tableau contient deux signaux :

  • MY_DIAPO_CLICKED_SIGNAL ;
  • MY_DIAPO_PERSONAL_SIGNAL.

Pour ajouter des signaux, il suffit d’ajouter des déclarations avant MY_DIAPO_NB_SIGNALS.

Dans la fonction my_diapo_class_init();, nous utilisons la fonction g_signal_new (); pour initialiser ces nouveaux signaux :

 
Sélectionnez
my_diapo_signals [MY_DIAPO_CLICKED_SIGNAL] =
    g_signal_new("clicked",
                 G_TYPE_FROM_CLASS (class),
                 G_SIGNAL_RUN_LAST,
                 0,
                 NULL, NULL,
                 g_cclosure_marshal_VOID__VOID,
                 G_TYPE_NONE,
                 0);

 my_diapo_signals [MY_DIAPO_PERSONAL_SIGNAL] =
    g_signal_new("my_personal_signal",
                 G_TYPE_FROM_CLASS (class),
                 G_SIGNAL_RUN_LAST,
                 0,
                 NULL, NULL,
                 g_cclosure_user_marshal_BOOLEAN__STRING_INT_INT,
                 G_TYPE_BOOLEAN,
                 3,
                 G_TYPE_STRING, G_TYPE_INT, G_TYPE_INT);

Nous étudierons au chapitre « Initialisation d’un signal personnelInitialisation d’un signal personnel » la deuxième déclaration pour le signal « my_personal_signal ».

g_signal_new(); est un peu complexe à utiliser. Les paramètres dans l’ordre d’apparition sont :

  • Le nom du nouveau signal ;
  • Le type de l’objet auquel se réfère le signal ;
  • Une liste de drapeaux qui peuvent indiquer comment activer le signal. Typiquement, on utilisera G_SIGNAL_RUN_FIRST ou G_SIGNAL_RUN_LAST ;
  • Ce paramètre peut être rempli avec un G_STRUCT_OFFSET(). Lui transmettre 0 ici ne change rien au bon fonctionnement de la fonction.
  • Peut être à NULL ;
  • Associé au paramètre précédent, on peut aussi le positionner à NULL ;
  • C’est ici que nous plaçons la fonction prédéfinie pour déterminer le type réel de notre callback ;
  • Le type que renvoie notre callback ;
  • Le nombre de paramètres attendus dans le callback. Si nous avons plusieurs paramètres, leur type viendra après ce dernier paramètre. Comme pour notre premier exemple, il n’y en a pas, les paramètres s’arrêtent ici.

La création d’un nouveau signal utilisant un prototype de callback prédéfini par la bibliothèque Gtk+ est maintenant terminée. Nous verrons dans la fonction callback my_diapo_on_button_press_event(); comment émettre le signal « clicked ».

Au chapitre suivant, nous allons ajouter un peu de complexité au principe en créant non seulement un nouveau signal, mais aussi un prototype personnel qui n’existe pas dans la bibliothèque.

5-2-7-2. Initialisation d’un signal personnel

Nous devons choisir la forme de notre prototype du callback. Mais si, parmi toutes les fonctions proposées, il n’y a pas ce que nous voulons. Comment faire ?

Pour créer un nouveau prototype, il nous faut tout d’abord créer un simple fichier texte dans lequel nous allons décrire notre prototype.

Par habitude, je nomme ce fichier du nom du nouveau widget suivi du mot marshal avec l’extension .list. Voilà à quoi ressemble ce fichier :

MyDiapoMarshal.list
Sélectionnez
# Prototype pour le signal "my_personal_signal" du widget MyDiapo
BOOLEAN:STRING,INT,INT

La première ligne est une ligne de commentaires. Un classique. La deuxième décrit mon prototype avec :

  • en premier argument le type renvoyé séparé par un deux-points ;
  • les arguments suivants correspondent aux paramètres transmis séparés par des virgules.

Si nous traduisons cette description, voilà le prototype créé :

 
Sélectionnez
gboolean (*my_personal_signal) (MyDiapo *diapo, gchar *filename, gint width, gint height, gpointer userdata);

Le fichier créé, nous devons utiliser en console une application fournie par la bibliothèque : glib-genmarshal. Elle va, en lui transmettant ce fichier et quelques options, générer un fichier d’entête et un fichier source dans lesquels nous aurons une nouvelle fonction g_closure…(); personnelle. Il nous suffira alors d’ajouter ces fichiers à notre projet et d’utiliser la fonction lors de l’initialisation du signal.

Pour utiliser cette application, voilà les deux lignes de commande à exécuter pour générer les fichiers :

  • glib-genmarshal --header MyDiapoMarshal.list > MyDiapoMarshal.h pour le fichier d’entête ;
  • glib-genmarshal --body MyDiapoMarshal.list > MyDiapoMarshal.c pour le fichier source.

Voilà à quoi ressemble le fichier d’entête :

MyDiapoMarshal.h
Sélectionnez
/* This file is generated by glib-genmarshal, do not modify it. This code is licensed under the same license as the containing project. Note that it links to GLib, so must comply with the LGPL linking clauses. */
#ifndef __G_CCLOSURE_USER_MARSHAL_MARSHAL_H__
#define __G_CCLOSURE_USER_MARSHAL_MARSHAL_H__

#include <glib-object.h>

G_BEGIN_DECLS

/* BOOLEAN:STRING,INT,INT (MyDiapoMarshal.list:2) */
extern
void g_cclosure_user_marshal_BOOLEAN__STRING_INT_INT (GClosure     *closure,
                                                      GValue       *return_value,
                                                      guint         n_param_values,
                                                      const GValue *param_values,
                                                      gpointer      invocation_hint,
                                                      gpointer      marshal_data);


G_END_DECLS

#endif /* __G_CCLOSURE_USER_MARSHAL_MARSHAL_H__ */

Nous pouvons voir la nouvelle fonction g_cclosure_user_marshal_BOOLEAN__STRING_INT_INT();. C’est elle que nous utilisons. Je vous redonne ici la déclaration du nouveau signal :

 
Sélectionnez
my_diapo_signals [MY_DIAPO_PERSONAL_SIGNAL] =
    g_signal_new("my_personal_signal",
                 G_TYPE_FROM_CLASS (class),
                 G_SIGNAL_RUN_LAST,
                 0,
                 NULL, NULL,
                 g_cclosure_user_marshal_BOOLEAN__STRING_INT_INT,
                 G_TYPE_BOOLEAN,
                 3,
                 G_TYPE_STRING, G_TYPE_INT, G_TYPE_INT);

Nous voyons, à la différence du signal « clicked », que nous avons un type de retour G_TYPE_BOOLEAN et trois types en paramètre déclarés en fin de fonction.

Il ne reste plus qu’à ajouter #include « MyDiapoMarshal.h » dans le fichier d’entête.

5-2-8. Utiliser les nouveaux signaux

Notre widget dispose maintenant de deux nouveaux signaux. Pour les utiliser, il faut les émettre à un moment bien choisi. Pour notre exemple, le signal clicked sera émis lors d’un simple clic gauche de la souris. Le signal « my_personnal_signal » quant à lui sera émis sur un double-clic.

Pour ce faire, nous allons ajouter un callback qui sera exécuté lors du clic de la souris. Cette fonction est attachée au pointeur « button_press_event ». Elle se nomme my_diapo_on_button_press_event();.

Pour émettre un signal, nous utilisons la fonction g_signal_emit_by_name();. Nous lui transmettons l’objet concerné, le nom du signal et les paramètres s’il y en a. En dernier paramètre vient un pointeur pour récupérer la valeur de retour s’il y en a une.

Pour reprendre notre exemple pour le signal « clicked », voilà l’instruction correspondante :

 
Sélectionnez
g_signal_emit_by_name(G_OBJECT(widget), "clicked");

Et pour le signal « my_personal_signal » :

 
Sélectionnez
g_signal_emit_by_name(G_OBJECT(widget), "my_personal_signal", priv->filename, priv->width, priv->height, &result);
5-2-8-1. static gboolean my_diapo_on_button_press_event();

En résumé, voilà le code du callback qui intègre tout ce petit monde et qui nous montre comment récupérer la valeur de retour.

static gboolean my_diapo_on_button_press_event(GtkWidget *widget, GdkEventButton *event)
Sélectionnez
static gboolean my_diapo_on_button_press_event(GtkWidget *widget, GdkEventButton *event)
{
  if (event->type==GDK_2BUTTON_PRESS) // Double clic
    {
      MyDiapoPrivate *priv = my_diapo_get_instance_private(MY_DIAPO (widget));
      gboolean result;

      /* Emission du signal "my_personal_signal" sur un double clic. La valeur de retour se trouve dans result */
      g_signal_emit_by_name(G_OBJECT (widget), "my_personal_signal", priv->filename, priv->width, priv->height, &result);

      g_print("Le callback pour le signal my_personal_signal à renvoyer ");
      if (result)
    g_print("TRUE\n");
      else
    g_print("FALSE\n");
    }
  else  if (event->type==GDK_BUTTON_PRESS) // Un simple clic
    {
      switch (event->button)
    {
    case 1: // bouton gauche
      {
        MyDiapoPrivate *priv = my_diapo_get_instance_private (MY_DIAPO (widget));
        priv->selected = !priv->selected;

        /* Emission du signal "clicked" sur un simple clic */
        g_signal_emit_by_name(G_OBJECT (widget), "clicked");

        gtk_widget_queue_draw(widget);
        break;
      }
    default :
      {
        break;
      }
    }
    }

  return FALSE;
}

5-2-9. Un code exemple pour utiliser les nouveaux signaux

Il ne nous reste plus qu’à écrire un code exemple pour tester tout ce petit monde. Nous allons créer une fenêtre principale dans laquelle on insérera un GtkGrid. Dans ce widget viendront deux MyDiapo : un vide et un avec une image de notre choix.

Le premier MyDiapo aura le signal « clicked » connecté à un callback tandis que le deuxième aura le signal « my_personnal_signal » connecté. Pour montrer que les données personnelles sont gérées en interne, nous transmettrons un nombre sous forme de pointeur aux différents callbacks qui l’afficheront.

main.c
Sélectionnez
#include <gtk/gtk.h>
#include "mydiapo.h"

void clicked_event(MyDiapo *diapo, gpointer userdata)
{
  g_print("Signal clicked activé\n");
  g_print("donnée utilisateur : %d\n", GPOINTER_TO_INT(userdata));
}

gboolean my_personal_signal_event(MyDiapo *diapo, gchar *filename, gint width, gint height, gpointer userdata)
{
  g_print("Signal my_personal_signal activé\n");
  g_print("filename : %s\n", filename);
  g_print("taille : %d, %d\n", width, height);
  g_print("donnée utilisateur : %d\n", GPOINTER_TO_INT(userdata));
  return FALSE;
}

gint main(gint argc, gchar *argv[])
{
  gtk_init(&argc, &argv);

  GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
  GtkWidget *grid = gtk_grid_new();
  GtkWidget *diapo = NULL;

  gtk_container_add(GTK_CONTAINER(window), grid);

  // Configuration de la grille
  gtk_grid_set_row_homogeneous(GTK_GRID (grid), TRUE);
  gtk_grid_set_column_homogeneous(GTK_GRID (grid), TRUE);
  gtk_grid_set_row_spacing(GTK_GRID (grid), 10);
  gtk_grid_set_column_spacing(GTK_GRID (grid), 10);

  // Insertion d'une première diapo sans image
  diapo = my_diapo_new(NULL);
  gtk_grid_attach(GTK_GRID(grid), diapo, 0, 0, 1, 1);
  g_signal_connect(G_OBJECT(diapo), "clicked", G_CALLBACK(clicked_event), GINT_TO_POINTER(20));

  // Insertion d'une deuxième image avec une image
  diapo = my_diapo_new("./test.jpg");
  gtk_grid_attach(GTK_GRID (grid), diapo, 1, 0, 1, 1);
  g_signal_connect(G_OBJECT (diapo), "my_personal_signal", G_CALLBACK(my_personal_signal_event), GINT_TO_POINTER(20));

  g_signal_connect(G_OBJECT(window), "destroy", (GCallback)gtk_main_quit, NULL);

  gtk_widget_show_all(window);

  gtk_main();

  return 0 ;
}

6. Conclusion

Ce tutoriel est long et certainement difficile à suivre. L’idée est qu’il soit le plus complet possible. J’avais dans l’idée d’ajouter aussi la gestion des propriétés du widget depuis les GObject mais ça fait beaucoup trop pour un seul tutoriel. N’hésitez pas à poser toutes les questions qui vous semblent utiles sur le forum dédié à Gtk+. Je vous y attends avec plaisir.

Pour être le plus exhaustif possible, voici les codes sources complets de l’exemple détaillé.

main.c
Cacher/Afficher le codeSélectionnez
mydiapo.h
Cacher/Afficher le codeSélectionnez
mydiapo.c
Cacher/Afficher le codeSélectionnez
MyDiapoMarshal.list
Cacher/Afficher le codeSélectionnez

Je vous mets en plus des codes sources le Makefile.am et le configure.ac pour celles et ceux qui désirent utiliser les autotools.

Makefie.am
Cacher/Afficher le codeSélectionnez
Configure.ac
Cacher/Afficher le codeSélectionnez

7. Remerciements

Je remercie chaleureusement f-leb pour la relecture orthographique de cet article.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

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 © 2021 Gérald Dumas. 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.