Programmation orientée objet avec Gtk+ v3

Créer son propre widget

Gérald Dumas

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

Commentez

- Titre : Programmation orientée objet avec Gtk+ v3

- Auteur : Gérald Dumas

- Parution : 29 octobre 2021

- Public visé :

- Licence : 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 et 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.

Voir également :

- Créer son widget GTK+ en Langage C

- Création de signaux pour vos widgets GTK+

Sommaire

- 1. Introduction

- 2. Principe des objets selon Gtk+

- 3. Description du widget que nous allons créer

- 4. De quels outils disposons-nous ?

- 4-1. Les macros

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

- 4-1-1-1. G_DECLARE_FINAL_TYPE()

- 4-1-1-2. G_DECLARE_DERIVABLE_TYPE()

- 4-1-1-3. G_DECLARE_INTERFACE()

- 4-1-2. Les macros du fichier source

- 4-1-2-1. G_DEFINE_TYPE()

- 4-1-2-2. G_DEFINE_TYPE_WITH_PRIVATE()

- 4-1-2-3. G_DEFINE_TYPE_WITH_CODE()

- 4-2. Les fonctions

- 5. Déclaration de notre nouveau widget MyDiapo

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

- 5-2. Le fichier source mydiapo.c

- 5-2-1. Les déclarations

- 5-2-2. Les prototypes des fonctions internes

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

- 5-2-2-2. static void my_diapo_init()

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

- 5-2-3-1. Le constructeur

- 5-2-3-2. Le destructeur

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

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

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

- 5-2-5. Les fonctions privées

- 5-2-5-1. static void my_diapo_create_pellicule();

- 5-2-5-2. static void my_diapo_put_image();

- 5-2-5-3. static void my_diapo_load_image();

- 5-2-5-4. static GdkPixbuf *my_diapo_image_with_text();

- 5-2-6. Les fonctions « callback »

- 5-2-6-1. static gboolean my_diapo_on_draw();

- 5-2-6-2. static gboolean my_diapo_on_scroll_event();

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

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

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

- 5-2-8. Utiliser les nouveaux signaux

- 5-2-8-1. static gboolean my_diapo_on_button_press_event();

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

- 6. Conclusion

- 7. Remerciements

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.

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

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 :

- G_DECLARE_FINAL_TYPE() ;

- G_DECLARE_DERIVABLE_TYPE() ;

- G_DECLARE_INTERFACE().

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.

- G_DEFINE_TYPE() ;

- G_DEFINE_TYPE_WITH_PRIVATE() ;

- G_DEFINE_TYPE_WITH_CODE().

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() 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.

1. #ifndef __MY_DIAPO_H__

2. #define __MY_DIAPO_H__

4. #include <gtk/gtk.h>

6. G_BEGIN_DECLS

8. // Ici notre code

10. G_END_DECLS

12. #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 () :

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+ :

#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 :

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é.

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é :

1. #ifndef __MY_DIAPO_H__

2. #define __MY_DIAPO_H__

4. #include <gtk/gtk.h>

6. G_BEGIN_DECLS

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

10. #define MY_TYPE_DIAPO(my_diapo_get_type ())

12. typedef struct _MyDiapoPrivate MyDiapoPrivate;

14. struct _MyDiapo {

15. GtkDrawingArea parent;

16. MyDiapoPrivate *priv;

17. };

19. struct _MyDiapoClass {

20. GtkDrawingAreaClass parent_class;

21. };

23. G_END_DECLS

25. #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 :

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

1. #ifndef __MY_DIAPO_H__

2. #define __MY_DIAPO_H__

4. #include <gtk/gtk.h>

6. G_BEGIN_DECLS

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

10. #define MY_TYPE_DIAPO(my_diapo_get_type ())

12. typedef struct _MyDiapoPrivate MyDiapoPrivate;

14. struct _MyDiapo {

15. GtkDrawingArea parent;

17. MyDiapoPrivate *priv;

18. };

20. struct _MyDiapoClass {

21. GtkDrawingAreaClass parent_class;

22. };

24. // Création d'une instance. (filename peut être NULL)

25. GtkWidget *my_diapo_new(const gchar *filename);

27. // Charge ou change l'image insérée

28. void my_diapo_set_image(MyDiapo *diapo, const gchar *filename);

30. // Définit/récupère la taille du widget

31. void my_diapo_set_size(MyDiapo *diapo, int *width, int *height);

32. void my_diapo_get_size(Myiapo *diapo, int *width, int *height);

34. G_END_DECLS

36. #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 :

1. #include "mydiapo.h"

3. /* Définition de la structure privée. */

4. struct _MyDiapoPrivate

5. {

6. gchar *filename;

7. GdkPixbuf *originalpixbuf;

9. gint width, height;

10. gboolean selected;

11. };

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 :

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 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)

1. static void

2. my_diapo_class_init(MyDiapoClass *class)

3. {

4. G_OBJECT_CLASS(class)->finalize = my_diapo_finalize;

6. GTK_WIDGET_CLASS(class)->button_press_event = my_diapo_on_button_press_event;

7. GTK_WIDGET_CLASS(class)->scroll_event = my_diapo_on_scroll_event;

8. GTK_WIDGET_CLASS(class)->draw = my_diapo_on_draw;

9. }

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

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

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

struct GtkWidgetClass {
GInitiallyUnownedClass parent_class;

guint activate_signal;

/* seldomly overidden */
void (*dispatch_child_properties_changed) (GtkWidget   *widget,
guint        n_pspecs,
GParamSpec **pspecs);

/* basics */
void (* destroy)             (GtkWidget        *widget);
void (* show)                (GtkWidget        *widget);
void (* show_all)            (GtkWidget        *widget);
void (* hide)                (GtkWidget        *widget);
void (* map)                 (GtkWidget        *widget);
void (* unmap)               (GtkWidget        *widget);
void (* realize)             (GtkWidget        *widget);
void (* unrealize)           (GtkWidget        *widget);
void (* size_allocate)       (GtkWidget        *widget,
GtkAllocation    *allocation);
void (* state_changed)       (GtkWidget        *widget,
GtkStateType      previous_state);
void (* state_flags_changed) (GtkWidget        *widget,
GtkStateFlags     previous_state_flags);
void (* parent_set)          (GtkWidget        *widget,
GtkWidget        *previous_parent);
void (* hierarchy_changed)   (GtkWidget        *widget,
GtkWidget        *previous_toplevel);
void (* style_set)           (GtkWidget        *widget,
GtkStyle         *previous_style);
void (* direction_changed)   (GtkWidget        *widget,
GtkTextDirection  previous_direction);
void (* grab_notify)         (GtkWidget        *widget,
gboolean          was_grabbed);
void (* child_notify)        (GtkWidget        *widget,
GParamSpec       *child_property);
gboolean (* draw)            (GtkWidget        *widget,
cairo_t          *cr);

/* size requests */
GtkSizeRequestMode (* get_request_mode)               (GtkWidget      *widget);

void               (* get_preferred_height)           (GtkWidget       *widget,
gint            *minimum_height,
gint            *natural_height);
void               (* get_preferred_width_for_height) (GtkWidget       *widget,
gint             height,
gint            *minimum_width,
gint            *natural_width);
void               (* get_preferred_width)            (GtkWidget       *widget,
gint            *minimum_width,
gint            *natural_width);
void               (* get_preferred_height_for_width) (GtkWidget       *widget,
gint             width,
gint            *minimum_height,
gint            *natural_height);

/* Mnemonics */
gboolean (* mnemonic_activate)        (GtkWidget           *widget,
gboolean             group_cycling);

/* explicit focus */
void     (* grab_focus)               (GtkWidget           *widget);
gboolean (* focus)                    (GtkWidget           *widget,
GtkDirectionType     direction);

/* keyboard navigation */
void     (* move_focus)               (GtkWidget           *widget,
GtkDirectionType     direction);
gboolean (* keynav_failed)            (GtkWidget           *widget,
GtkDirectionType     direction);

/* events */
gboolean (* event)                    (GtkWidget           *widget,
GdkEvent            *event);
gboolean (* button_press_event)&#160;(GtkWidget         *widget,
GdkEventButton      *event);
gboolean (* button_release_event)&#160;(GtkWidget       *widget,
GdkEventButton      *event);
gboolean (* scroll_event)             (GtkWidget           *widget,
GdkEventScroll      *event);
gboolean (* motion_notify_event)&#160;(GtkWidget        *widget,
GdkEventMotion      *event);
gboolean (* delete_event)             (GtkWidget           *widget,
GdkEventAny         *event);
gboolean (* destroy_event)            (GtkWidget           *widget,
GdkEventAny         *event);
gboolean (* key_press_event)          (GtkWidget           *widget,
GdkEventKey         *event);
gboolean (* key_release_event)&#160;(GtkWidget          *widget,
GdkEventKey         *event);
gboolean (* enter_notify_event)&#160;(GtkWidget         *widget,
GdkEventCrossing    *event);
gboolean (* leave_notify_event)&#160;(GtkWidget         *widget,
GdkEventCrossing    *event);
gboolean (* configure_event)          (GtkWidget           *widget,
GdkEventConfigure   *event);
gboolean (* focus_in_event)           (GtkWidget           *widget,
GdkEventFocus       *event);
gboolean (* focus_out_event)          (GtkWidget           *widget,
GdkEventFocus       *event);
gboolean (* map_event)                (GtkWidget           *widget,
GdkEventAny         *event);
gboolean (* unmap_event)              (GtkWidget           *widget,
GdkEventAny         *event);
gboolean (* property_notify_event)&#160;(GtkWidget      *widget,
GdkEventProperty    *event);
gboolean (* selection_clear_event)&#160;(GtkWidget      *widget,
GdkEventSelection   *event);
gboolean (* selection_request_event)&#160;(GtkWidget            *widget,
GdkEventSelection   *event);
gboolean (* selection_notify_event)&#160;(GtkWidget             *widget,
GdkEventSelection   *event);
gboolean (* proximity_in_event)&#160;(GtkWidget         *widget,
GdkEventProximity   *event);
gboolean (* proximity_out_event)&#160;(GtkWidget        *widget,
GdkEventProximity   *event);
gboolean (* visibility_notify_event)&#160;(GtkWidget            *widget,
GdkEventVisibility  *event);
gboolean (* window_state_event)&#160;(GtkWidget         *widget,
GdkEventWindowState *event);
gboolean (* damage_event)             (GtkWidget           *widget,
GdkEventExpose      *event);
gboolean (* grab_broken_event)        (GtkWidget           *widget,
GdkEventGrabBroken  *event);

/* selection */
void     (* selection_get)       (GtkWidget          *widget,
GtkSelectionData   *selection_data,
guint               info,
guint               time_);
void     (* selection_received)  (GtkWidget          *widget,
GtkSelectionData   *selection_data,
guint               time_);

/* Source side drag signals */
void     (* drag_begin)          (GtkWidget         *widget,
GdkDragContext     *context);
void     (* drag_end)            (GtkWidget          *widget,
GdkDragContext     *context);
void     (* drag_data_get)       (GtkWidget          *widget,
GdkDragContext     *context,
GtkSelectionData   *selection_data,
guint               info,
guint               time_);
void     (* drag_data_delete)    (GtkWidget          *widget,
GdkDragContext     *context);

/* Target side drag signals */
void     (* drag_leave)          (GtkWidget          *widget,
GdkDragContext     *context,
guint               time_);
gboolean (* drag_motion)         (GtkWidget          *widget,
GdkDragContext     *context,
gint                x,
gint                y,
guint               time_);
gboolean (* drag_drop)           (GtkWidget          *widget,
GdkDragContext     *context,
gint                x,
gint                y,
guint               time_);
void     (* drag_data_received)  (GtkWidget          *widget,
GdkDragContext     *context,
gint                x,
gint                y,
GtkSelectionData   *selection_data,
guint               info,
guint               time_);
gboolean (* drag_failed)         (GtkWidget          *widget,
GdkDragContext     *context,
GtkDragResult       result);

/* Signals used only for keybindings */
gboolean (* popup_menu)          (GtkWidget          *widget);

/* If a widget has multiple tooltips/whatsthis, it should show the
* one for the current focus location, or if that doesn't make
* sense, should cycle through them showing each tip alongside
* whatever piece of the widget it applies to.
*/
gboolean (* show_help)           (GtkWidget          *widget,
GtkWidgetHelpType   help_type);

/* accessibility support
*/
AtkObject *  (* get_accessible)     (GtkWidget *widget);

void         (* screen_changed)     (GtkWidget *widget,
GdkScreen *previous_screen);
gboolean     (* can_activate_accel) (GtkWidget *widget,
guint      signal_id);

void         (* composited_changed) (GtkWidget *widget);

gboolean     (* query_tooltip)      (GtkWidget  *widget,
gint        x,
gint        y,
gboolean    keyboard_tooltip,
GtkTooltip *tooltip);

void         (* compute_expand)     (GtkWidget  *widget,
gboolean   *hexpand_p,
gboolean   *vexpand_p);

void         (* adjust_size_request)    (GtkWidget         *widget,
GtkOrientation     orientation,
gint              *minimum_size,
gint              *natural_size);
void         (* adjust_size_allocation) (GtkWidget         *widget,
GtkOrientation     orientation,
gint              *minimum_size,
gint              *natural_size,
gint              *allocated_pos,
gint              *allocated_size);

void         (* style_updated)          (GtkWidget *widget);

gboolean     (* touch_event)            (GtkWidget     *widget,
GdkEventTouch *event);

void         (* get_preferred_height_and_baseline_for_width)  (GtkWidget     *widget,
gint           width,
gint          *minimum_height,
gint          *natural_height,
gint          *minimum_baseline,
gint          *natural_baseline);
void         (* adjust_baseline_request)(GtkWidget         *widget,
gint              *minimum_baseline,
gint              *natural_baseline);
void         (* adjust_baseline_allocation) (GtkWidget         *widget,
gint              *baseline);
void         (*queue_draw_region)           (GtkWidget         *widget,
const cairo_region_t *region);
};

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)

1. static void my_diapo_init(MyDiapo *diapo)

2. {

3. MyDiapoPrivate *priv = my_diapo_get_instance_private(diapo);

5. priv->filename = NULL;

6. priv->originalpixbuf = NULL;

7. priv->width = 100;

8. priv->height = 100;

9. priv->selected = FALSE;

11. // Autorise les bulles d’aide

12. gtk_widget_set_has_tooltip(GTK_WIDGET(diapo), TRUE);

14. // Fixe la taille de départ à 100x100

15. gtk_widget_set_size_request(GTK_WIDGET(diapo), priv->width, priv->height);

17. // Ajout de deux écouteurs au widget (gestion de la souris)

18. gtk_widget_add_events(GTK_WIDGET(diapo), GDK_BUTTON_PRESS_MASK | GDK_SCROLL_MASK);

19. }

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 :

#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 :

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 :

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)

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 :

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ête », 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)

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)

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)

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 :

// 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)

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)

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.

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)

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 :

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)

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)

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) :

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 :

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 :

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 :

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 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

# 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éé :

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

/* 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 :

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 :

g_signal_emit_by_name(G_OBJECT(widget), "clicked");

Et pour le signal « my_personal_signal » :

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)

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

#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

#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 ;
}

mydiapo.h

/* MyDiapo
A subclass of GtkDrawingArea to create a thumbnail.
(C) <2021>  <Gérald Dumas> <gerald.dumas@laposte.net>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software Foundation,
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
*/

#ifndef __MY_DIAPO_H__
#define __MY_DIAPO_H__

#include <gtk/gtk.h>
#include "MyDiapoMarshal.h"

G_BEGIN_DECLS

G_DECLARE_FINAL_TYPE (MyDiapo, my_diapo, MY, DIAPO, GtkDrawingArea)
typedef struct _MyDiapo MyDiapo;

#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 (MyDiapo *diapo, int *width, int *height);

G_END_DECLS

#endif

mydiapo.c

#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);

// 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);

// 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);

enum {
MY_DIAPO_CLICKED_SIGNAL,
MY_DIAPO_PERSONAL_SIGNAL,
MY_DIAPO_NB_SIGNALS
};

static guint my_diapo_signals [MY_DIAPO_NB_SIGNALS] = { 0 };

static void
my_diapo_class_init (MyDiapoClass *class)
{
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);

G_OBJECT_CLASS(class)->finalize = my_diapo_finalize;

widget_class->button_press_event = my_diapo_on_button_press_event;
widget_class->scroll_event = my_diapo_on_scroll_event;
widget_class->draw = my_diapo_on_draw;

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);
}

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;

gtk_widget_set_has_tooltip (GTK_WIDGET(diapo), TRUE);
gtk_widget_set_size_request (GTK_WIDGET(diapo), priv->width, priv->height);

// Ajout de deux écouteurs au widget (gestion entière de la souris)
gtk_widget_add_events (GTK_WIDGET(diapo), GDK_BUTTON_PRESS_MASK | GDK_SCROLL_MASK);
}

static void
my_diapo_finalize (GObject * object)
{
MyDiapoPrivate *priv = my_diapo_get_instance_private (MY_DIAPO(object));

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);
}

/******************************************************************************/

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

diapo = g_object_new (MY_TYPE_DIAPO, NULL);

MyDiapoPrivate *priv = my_diapo_get_instance_private (diapo);

priv->filename = g_strdup (filename);

my_diapo_load_image (diapo);

return GTK_WIDGET(diapo);
}

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;
}

static gboolean
my_diapo_on_draw (GtkWidget *widget, cairo_t *cr)
{
MyDiapoPrivate *priv = my_diapo_get_instance_private (MY_DIAPO (widget));

// On efface le widget en noir et on fixe la couleur comme canal alpha
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;
}

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;

/* Émission 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;

/* Émission 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;
}

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;
}

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 redimenssionnement
if (rapport<1) // Photo en portrait
priv->height = priv->height * rapport;
else // Photo en paysage
priv->width = priv->width * rapport;
}

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);
*width = priv->width;
*height = priv->height;
}

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 l'affichage
gtk_widget_set_size_request (GTK_WIDGET(diapo), priv->width, priv->height);
}

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_rgba (cr, 1, 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);
}

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);

// Encadrement en rouge de l'image
cairo_set_line_width (cr, 1);
cairo_set_source_rgb (cr, 1, 1, 1);
cairo_move_to (cr, offsetX, offsetY);
cairo_line_to (cr, priv->width-offsetX, offsetY);
cairo_line_to (cr, priv->width-offsetX, priv->height-offsetY);
cairo_line_to (cr, offsetX, priv->height-offsetY);
cairo_line_to (cr, offsetX, offsetY);
cairo_stroke (cr);

// Suppression du pixbuf devenu inutile
g_object_unref (subPixbuf);
}

void
my_diapo_set_image (MyDiapo *diapo, const gchar *filename)
{
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);

my_diapo_load_image (diapo);
}

MyDiapoMarshal.list

# Prototype pour le signal "my_personal_signal" du widget MyDiapo
BOOLEAN:STRING,INT,INT

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

AUTOMAKE_OPTIONS = subdir-objects

bin_PROGRAMS = mydiapo

mydiapo_SOURCES = MyDiapoMarshal.h MyDiapoMarshal.c mydiapo.c mydiapo.h main.c

AM_CFLAGS = -Wall -Wunused -rdynamic $(GTK_CFLAGS)
LDADD =  $(GTK_LIBS)

Configure.ac

#                                               -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.

AC_PREREQ([2.69])
AC_INIT([mydiapo], [0.1], [gerald.dumas@laposte.net])
AC_CONFIG_SRCDIR([.])
AC_CONFIG_HEADERS([config.h])

AM_INIT_AUTOMAKE([foreign -Wall -Werror])

AC_COPYRIGHT([GPLv3])

# Checks for programs.
AC_PROG_CC
AC_PROG_INSTALL
AC_PROG_MAKE_SET
AC_PROG_MKDIR_P
AC_PROG_LN_S

# Checks for libraries.
PKG_CHECK_MODULES([GTK], [gtk+-3.0], , AC_MSG_ERROR(   gtk+3.0 library not detected !!!

******************* GTK 3 library required for gdiapo ******************
Install the GTK 3 library from your package manager or from source.

WARNING: Take care to install the development files from gtk-3 !!!
Often call: libgtk-3-dev or libgtk-3-devel.

Then retry to launch the ./configure script...
************************************************************************
, 1))

AC_CONFIG_FILES([Makefile
src/Makefile])

AC_OUTPUT

7 - Remerciements

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