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_FINAL_TYPE() ;
- G_DECLARE_DERIVABLE_TYPE()G_DECLARE_DERIVABLE_TYPE() ;
- G_DECLARE_INTERFACE()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() ;
- G_DEFINE_TYPE_WITH_PRIVATE()G_DEFINE_TYPE_WITH_PRIVATE() ;
- G_DEFINE_TYPE_WITH_CODE()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()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.
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 () :
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é :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
#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)
{
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ê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)
{
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)
{
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)
{
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)
{
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)
{
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)
{
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)
{
// 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)
{
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 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 :
# 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 :
/* 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)
{
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.
#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é.
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.
7. Remerciements▲
Je remercie chaleureusement f-leb pour la relecture orthographique de cet article.