septembre
2010
Bonjour !
Après un long moment d’absence et de sérieux doutes sur la pérennité de ce blog, je reviens vous parler de LablGTK. Cette fois-ci, je ne vous présenterai pas un autre module de la bibliothèque. Pour célébrer la renaissance de ce blog, à la suite de mon admission en thèse (dans un domaine très éloigné du sujet de ce blog), je vous propose de parler du mécanisme de callback dans LablGTK.
Nous allons aborder dans ce billet une manière automatisée d’associer des fonctions callback à des widgets. Mais, pour partir sur de bonnes bases, nous allons d’abord rappeler ce qu’est une fonction callback.
Le principe d’une fonction callback
Une fonction callback (je ne me risque pas à proposer de traduction) est une fonction qui est communiquée dans un argument passé à une autre fonction. Dans le cas de GTK, un widget interagit avec l’utilisateur en émettant des événements auxquels des fonctions peuvent être associées. Ces dernières, on l’aura compris, ont pour but de répondre à l’événement émis, et donc in fine à l’utilisateur. Par exemple, le code suivant est typique :
let main_window = let window = GWindow.window ~title:"LablGTK demo" ~position:`CENTER ~width:640 ~height:480 () in window#connect#destroy ~callback:GMain.quit; window
Ce code crée la fenêtre principale d’une application. On y indique également qu’il faut fermer l’application (GMain.quit
) lorsque la fenêtre est détruite (ce qui se traduit par la levée de l’événement destroy
). Dans ce cas, la fonction GMain.quit
est utilisée comme callback.
Séparation de l’interface et du code
Pour construire une application, on recommande généralement de dissocier les éléments de l’interface et les actions associées. Le code est ainsi mieux structuré et plus facile à maintenir. Par exemple, on peut considérer un module GUI
qui contient l’ensemble des widgets de l’interface, et divers modules qui définissent les actions associées (copie, collage, zoom, enregistrement, etc.). Malgré des qualités indéniables, cette approche présente au moins deux inconvénients de taille :
- Chaque fois que l’interface est modifiée (par exemple en ajoutant ou en supprimant un widget), le module qui définit l’action correspondante doit être mis à jour pour ajouter ou supprimer un enregistrement de fonction callback et tenir ainsi compte des modifications réalisées en amont.
- Le module qui définit les actions n’est plus autonome : il dépend du module d’interface de l’application, de sorte que sa réutilisation dans un autre contexte devient plus difficile.
Nous allons présenter ici une solution qui résout ces deux problèmes et permet d’automatiser l’enregistrement des fonctions callback.
Automatisation du mécanisme de callback
Quelques nouveaux types
Pour automatiser le mécanisme d’enregistrement des fonctions callback, nous allons commencer par définir des types qui correspondent à une action particulière, comme l’activation d’un élément ou un clic gauche de souris. Ce sont :
class type clickable = object method clicked : callback:(unit -> unit) -> GtkSignal.id end class type activatable = object method activate : callback:(unit -> unit) -> GtkSignal.id end
Notez que l’on pourrait écrire de très nombreux autres types de ce genre, mais ceux-ci sont les plus fréquents car ils couvrent à la fois les barres d’outils et les menus, c’est-à-dire les éléments les plus susceptibles d’être modifiés dans une interface (on peut aussi voir dans ces définitions, quoique d’assez loin, une influence des typeclass de Haskell). On va ensuite définir un type somme pour réunir le tout :
type item = | C of clickable | A of activatable
Stockage des couples identifiant/données
Nous allons ensuite définir une table de hachage destinée à stocker des identifiants (type string
) et des données associées. Ces données sont constituées d’une liste d’objets (item list
) et d’une fonction de callback (de type unit -> unit
). Comme nous voulons modifier ces valeurs directement, nous allons les stocker sous forme de références :
val table : (string, action list ref * (unit -> unit) ref) Hashtbl.t
Nous allons maintenant définir une fonction register_any
qui, étant donné une fonction f
, un identifiant id
et un widget wid
, ajoute f wid
(de type item
) à la liste des objets associés à l’identifiant id
:
let register_any f id wid = let item = f wid in try let items = fst (Hashtbl.find table id) in items := item :: !items with Not_found -> Hashtbl.add table id (ref [item], ref ignore)
Remarque : vous avez peut-être remarqué que, si l’identifiant n’existe pas dans la table de hachage, les données ajoutées comportent une fonction callback qui ne fait rien (Pervasives.ignore
). Elle pourra bien sûr être modifiée par la suite !
Enregistrement des widgets
Nous pouvons ensuite définir des fonctions spécialisées pour les deux types clickable
et activatable
définis précédemment :
let register_clickable ~id obj = register_any (fun obj -> C (obj#connect :> clickable)) id obj let register_activatable ~id obj = register_any (fun obj -> A (obj#connect :> activatable)) id obj
Ces fonctions ont la signature suivante :
val register_clickable : id:string ->-> unit
val register_activatable : id:string ->-> unit
En d’autres termes, elles reçoivent en entrée un identifiant de type string
et un widget dont la méthode connect
est un sur-ensemble de la classe clickable
(ou activatable
, respectivement).
Enregistrement des fonctions de callback
Il faut ensuite écrire une fonction capable d’enregistrer les fonctions callback. Pour simplifier son utilisation, on souhaite qu’elle renvoie en sortie la fonction reçue en entrée :
let register_callback ~id f = try snd (Hashtbl.find table id) := f; f with Not_found -> f
Cette fonction est de type :
val register_callback : id:string -> (unit -> unit) -> unit -> unit
Association des fonctions aux widgets
Il ne reste plus qu’à définir la fonction qui associe les callback au lancement de l’application :
let connect_all () = Hashtbl.iter (fun _ ({contents = t}, {contents = f}) -> List.iter (function | C connect -> ignore (connect#clicked ~callback:f) | A connect -> ignore (connect#activate ~callback:f) ) t ) table
Rappel : en OCaml, les références peuvent être vues comme des valeurs de type record avec un champ unique appelé contents
. On peut donc filtrer les références en écrivant {contents = x}
, où x
désigne le contenu de la référence.
Cette fonction présente une signature très simple :
val connect_all : unit -> unit
Utilisation du module dans le programme
L’utilisation de ce module dans un programme se déroule en trois temps :
- Tout d’abord, les éléments de l’interface sont créés et enregistrés avec les fonctions
register_clickable
ouregister_activatable
. À cet effet, un identifiant spécifique est nécessaire… mais celui-ci peut être recyclé avantageusement et servir pour les plugins, voire pour la traduction de l’application en plusieurs langues. - Ensuite, les fonctions callback sont enregistrées avec
register_callback
et le même identifiant que précédemment. Les modules qui les contiennent restent indépendants du module d’interface. Accessoirement, les équipes de développeurs qui se parlent peu auront moins de surprises lors de la mise en commun de leur code ;-). - Enfin, au lancement de l’application, on connecte tous les widgets à leurs fonctions respectives à l’aide d’un unique appel à
connect_all
. On n’oublie personne !
On peut bien entendu imaginer diverses généralisations plus ou moins complexes de ce procédé, notamment au cas des fonctions callback multiples. On aurait alors une table de hachage de type :
val table : (string, action list ref * (unit -> unit) list ref) Hashtbl.t
et la fonction connect_all
deviendrait :
let connect_all () = Hashtbl.iter (fun _ ({contents = obj_list}, {contents = fun_list}) -> List.iter (fun item -> let f = match item with | C connect -> (fun f -> ignore (connect#clicked ~callback:f)) | A connect -> (fun f -> ignore (connect#activate ~callback:f)) in List.iter f fun_list ) obj_list ) table
Remarque : il commence à y avoir beaucoup d’itérateurs imbriqués. Il faut bien voir qu’il s’agit d’appliquer les fonctions associées à l’identifiant id
à chacun des éléments enregistrés.
En conclusion…
D’abord, un lien vers le code complet et l’interface complète.
Il est certain que l’intérêt d’un tel module se ressent surtout dans de moyennes ou grosses applications, dont l’interface est complexe et présente un certain niveau de redondance. Je ne pense pas qu’il soit raisonnable de l’utiliser pour écrire un pong.
Voilà, c’est tout pour ajourd’hui !
Cordialement,
Cacophrène
Salut,
Merci pour ton message sympathique
Maintenant je vais pouvoir mieux équilibrer le temps consacré à ce blog.
Je rédigerai peut-être aussi un billet annuel sur ce que je fais au labo, ça fera une petite pause dans ce monde de chameaux. ^^
Mes félicitations pour ton admission en thèse