janvier
2010
Bonjour !
Je vous propose de poursuivre notre exploration de la bibliothèque LablGTK. Dans ce billet, nous allons parler du module GPack
. Celui-ci contient tous les widgets nécessaires à la réalisation d’interfaces complexes. La plupart d’entre eux sont transparents pour l’utilisateur, car ils n’ont pas de représentation graphique. Pourtant, comme nous allons le voir ici, ce sont de précieux alliés !
Introduction
Nous avons vu dans un précédent billet que l’élément principal d’une interface est généralement une fenêtre (GtkWindow). Or ce widget ne peut contenir qu’un seul widget enfant. Cela semble, a priori, une limitation majeure. Pourtant, GTK permet de réaliser des interfaces arbitrairement complexes. Il est donc possible de lever cette limitation. Pour cela, GTK définit des conteneurs, c’est-à-dire des widgets dont la fonction principale est de contenir d’autres widgets. Ces conteneurs peuvent être imbriqués à souhait et constituent donc une solution à la fois élégante et souple.
Les boîtes horizontales (GtkHBox)
Ces boîtes permettent d’insérer des widgets horizontalement. Les enfants sont insérés avec la méthode #add
, ou avec #pack
si l’on souhaite paramétrer finement le style d’insertion (entre autres, la capacité du widget enfant à être élargi pour remplir l’espace disponible). Voyez par exemple :
let window = GMain.init (); let wnd = GWindow.window ~width:150 ~height:100 () in wnd#connect#destroy GMain.quit; wnd let packing = let hbox = GPack.hbox ~spacing:5 (* Les enfants sont espacés de 5 pixels. *) ~border_width:5 (* La boîte possède une bordure de 5 pixels. *) ~packing:window#add () in hbox#pack ~expand:false let _ = List.iter (fun text -> ignore (GMisc.label ~packing ~text ())) ["foo"; "bar"]; window#show (); GMain.main ()
Pour info : cet exemple, comme les suivants, est complet et peut-être testé dans l’interpréteur après avoir chargé la bibliothèque LablGTK.
Les boîtes verticales (GtkVBox)
Les boîtes verticales fonctionnent de la même manière que les boîtes horizontales, à un détail près : les widgets enfants sont insérés verticalement.
let window = GMain.init (); let wnd = GWindow.window ~width:150 ~height:100 () in wnd#connect#destroy GMain.quit; wnd let packing = let vbox = GPack.vbox ~spacing:5 (* Les enfants sont espacés de 5 pixels. *) ~border_width:5 (* La boîte possède une bordure de 5 pixels. *) ~packing:window#add () in vbox#pack ~expand:false let _ = List.iter (fun text -> ignore (GMisc.label ~packing ~text ())) ["foo"; "bar"]; window#show (); GMain.main ()
Les tableaux (GtkTable)
Les tableaux (GtkTable) sont un moyen pratique d’organiser des widgets sous forme de tableaux (on s’en serait douté…). Ils peuvent être utilisés, par exemple, pour créer des palettes. Les widgets sont insérés à l’aide de la méthode #attach
en indiquant l’emplacement des coins gauche (left
) et haut (top
) du widget.
let window = GMain.init (); let wnd = GWindow.window () in wnd#connect#destroy GMain.quit; wnd let attach = let table = GPack.table ~rows:4 (* Nombre de rangées. *) ~columns:4 (* Nombre de colonnes. *) ~row_spacings:2 (* Espacement des éléments d'une même rangée. *) ~col_spacings:2 (* Espacement des éléments d'une même colonne. *) ~packing:window#add () in fun i -> table#attach ~left:(i mod 4) ~top:(i / 4) let _ = Random.self_init (); for i = 0 to 24 do let r = Random.int 256 and g = Random.int 256 and b = Random.int 256 in let text = Printf.sprintf "#%02x%02x%02x" r g b in let entry = GEdit.entry ~text ~width:80 ~packing:(attach i) () in entry#misc#modify_base [`NORMAL, `NAME text] done; window#show (); GMain.main ()
Les boîtes pour boutons (GtkButtonBox)
GTK fournit une boîte spécialement conçue pour recevoir des boutons : il s’agit de GtkButtonBox, ou GPack.button_box
dans LablGTK. Elle est conçue pour vous permettre d’organiser des boutons à la manière des boîtes de dialogue :
let window = GMain.init (); let wnd = GWindow.window ~title:"Button box demo" ~position:`CENTER ~resizable:false ~width:300 ~height:150 () in wnd#connect#destroy GMain.quit; wnd let vbox = GPack.vbox ~spacing:2 ~border_width:2 ~packing:window#add () let view = GText.view ~packing:vbox#add () let bbox = GPack.button_box `HORIZONTAL ~layout:`EDGE (* C'est ici que l'on choisit la disposition des boutons. *) ~border_width:2 ~packing:(vbox#pack ~expand:false) () let help = GButton.button ~stock:`HELP ~packing:bbox#add () let quit = GButton.button ~stock:`QUIT ~packing:bbox#add () let _ = window#show (); GMain.main ()
Autres éléments
Le module GPack définit d’autres widgets, notamment les pages à onglets (GtkNotebook) et les panneaux mobiles (GtkPaned). En raison de leur intérêt tout particulier dans la réalisation d’interfaces, nous y consacrerons un billet entier.
Atelier : Le triangle de Pascal
Pour terminer, je vous propose de réaliser une petite application qui affiche à l’écran les coefficients binomiaux en les présentant sous la forme du célèbre triangle de Pascal. Voici à quoi devra ressembler notre application :
Pour réaliser notre application, nous avons besoin d’une fenêtre :
let window = let wnd = GWindow.window ~title:"GPack demo" ~position:`CENTER ~resizable:false ~width:400 ~height:300 () in wnd#connect#destroy GMain.quit; wnd
Celle-ci doit recevoir deux éléments distincts : une fenêtre assortie de barres de défilement, pour recevoir le triangle, et un bouton de fermeture. Nous allons donc commencer par insérer une boîte verticale dans window
:
let vbox = GPack.vbox ~border_width:2 ~spacing:2 ~packing:window#add ()
Nous pouvons alors ajouter les barres de défilement pour permettre à l’utilisateur d’explorer les lignes du triangle :
let scroll = GBin.scrolled_window ~hpolicy:`ALWAYS ~vpolicy:`ALWAYS ~packing:window#add ()
Ces barres de défilement ajoutées avec GBin.scrolled_window
ne résolvent pas le problème de GWindow.window
, car l’une comme l’autre ne peuvent contenir qu’un seul widget enfant. Pour lever cette limitation, nous devons donc insérer une boîte verticale à l’intérieur des barres de défilement. Ici, comme il doit être possible de faire défiler le contenu de la boîte, on utilise la méthode #add_with_viewport
au lieu du classique #add
. Ce qui nous donne :
let vbox, packing = let vbox = GPack.vbox ~spacing:2 ~packing:scroll#add_with_viewport () in vbox, vbox#pack ~expand:false
Écrivons maintenant une fonction spécialisée (par application partielle) de GMisc.label
pour nous simplifier la tâche :
let label = GMisc.label ~height:50 ~width:50 ~xalign:0.5
Pour information, les paramètres height
et width
fixent la taille des étiquettes produites (pas étonnant quand même…), et xalign
détermine l’alignement du texte (aligné à gauche avec 0.0, centré avec 0.5 et aligné à droite avec 1.0).
Venons-en maintenant au coeur du problème. Nous devons écrire une fonction qui affiche les lignes successives du triangle de Pascal en calculant les coefficients. Ici, les boucles sont pratiques, je préfère donc donner une version en style impératif. Les principales étapes sont les suivantes :
- Pour tout entier i entre 1 et n, créer un nouveau conteneur horizontal (
GPack.hbox
) et la fonction d’empaquetage associée (packing
). - Pour tout entier j entre 1 et j, calculer le coefficient binomial, égal à 1 lorsque j = 1 ou j = i, et coeff(i – 1, j – 1) + coeff(i – 1, j) sinon. Le stocker dans une table de hachage pour réutilisation ultérieure (mémoïsation).
- Créer une étiquette (
GMisc.label
) contenant le coefficient, et l’insérer dans le conteneur horizontal précédemment créé.
Nous pouvons satisfaire à toutes ces exigences avec le code suivant :
let create n = let mem = Hashtbl.create n in let get = Hashtbl.find mem in for i = 1 to n do let packing = (GPack.hbox ~spacing:2 ~packing ())#pack ~expand:false in for j = 1 to i do let coef = if j = 1 || j = i then 1 else get (i - 1, j) + get (i - 1, j - 1) in label ~text:(string_of_int coef) ~packing (); Hashtbl.add mem (i, j) coef done done
Nous pouvons maintenant terminer notre application en lui ajoutant un bouton de fermeture. C’est le moment de se rappeler que le conteneur GPack.button_box
est destiné tout spécialement à recevoir des boutons :
let quit = let bbox = GPack.button_box `HORIZONTAL ~layout:`END ~packing:(vbox#pack ~expand:false) () in let quit = GButton.button ~stock:`QUIT ~packing:bbox#add () in quit#connect#clicked window#destroy; quit
Pour finir, quelques lignes qui vérifient que le nombre de lignes à afficher est bien passé en argument, et qu’il s’agit d’un entier valide compris entre 1 et 25 (limites arbitraires) :
let _ = try if Array.length Sys.argv = 2 then ( let n = int_of_string Sys.argv.(1) in if n > 0 && n < 26 then create n else raise Exit; window#show (); GMain.main () ) else raise Exit with _ -> prerr_endline "Usage: triangle.ml TAILLE"
C’est tout ! Il ne nous reste plus qu’à lancer l’application :
ocaml -w s -I +lablgtk2 lablgtk.cma code.ml
Voici aussi un lien vers le code complet de cet atelier.
À bientôt,
Cacophrène
On peut comparer les implantations avec celles du module List mais je pense qu’il est plus instructif de bien observer les types de ces 3 fonctions, en particulier sous la forme colonne :
|zéro| |i| <br />
| | -> | | <br />
|succ| |f| <br />
<br />
|nil | |i| <br />
| | -> | | <br />
|cons| |f| <br />
<br />
|i| |nil | <br />
| | -> | | <br />
|f| |cons| <br />
De cette façon on voit bien quel est le rapport entre fold et unfold, fold construit une fonction à l’aide d’une donnée, unfold construit une donnée à l’aide d’une fonction.
Le fold s’obtient en remplaçant ‘a list par ‘b.
Si cons: ‘a -> ‘a list -> ‘a list alors f: ‘a -> ‘b -> ‘b (c’est bien List.fold_right).
Le fold n’est en fait rien d’autre qu’une généralisation du principe de récurrence qui dit comment construire la propriété P(n) à partir d’un entier n sous forme unaire :
|zéro| |P(0)| <br />
| | -> | | <br />
|succ| |P(n)| <br />
Coq dérive automatiquement un nouveau principe d’induction à chaque nouvelle définition de type.
Bonsoir,
Merci pour ton commentaire. Juste deux remarques simples sur le code que tu proposes ici. Pour
on peut aussi utiliser :
let rec int_fold f ini = function <br />
| 0 -> ini <br />
| n -> int_fold f (f ini) (n - 1) <br />
Par ailleurs la fonction list_fold est un List.fold_right qui n’avoue pas son nom. En tout cas c’est un code sympathique qui permet de prolonger judicieusement l’exemple du triangle de Pascal. Sympa !
Merci de poursuivre ton tour d’horizon approfondi de LablGtk.
Tu m’excuseras de squater ton blog LablGtk avec mon vilain code à base d’itérateurs mais l’occasion est trop belle pour montrer comment on encapsule la récursion.
Je vais utiliser le fold sur les entiers proposé sur son blog F# par ylarvor :
let rec int_fold f i = function <br />
| 0 -> i <br />
| n -> f (int_fold f i (n-1)) <br />
Je vais aussi utiliser le fold et le unfold sur les listes :
let rec list_fold f i = function <br />
| [] -> i <br />
| h::t -> f h (list_fold f i t) <br />
<br />
let list_unfold f i = <br />
int_fold <br />
(fun (h,l) -> let fh = f h in fh,fh::l) <br />
(i,[i]) <br />
La fonction new_row construit une nouvelle rangée du triangle de Pascal:
let new_row l = <br />
let h,t = list_fold (fun a (b,l) -> a,a+b::l) (0,[]) l <br />
in h::t <br />
Comme on a encapsulé la récursion on peut coder de façon très compacte.
On peut construire la n-ième rangée du nouvelle rangée du triangle de Pascal:
let pascal_row n = <br />
int_fold new_row [1] n <br />
Ou bien on peut construire le triangle de Pascal jusqu’au rang n:
let pascal_triangle n = <br />
let a,b = list_unfold new_row [1] n in b <br />