Article publié dans Linux Magazine 64, septembre 2004.
Copyright © 2004 - David Elbaz.
Nous poursuivons la série d'articles sur la boîte à outils GTK+
, avec
toujours comme objectif la présentation des concepts et items de base, ainsi
que la mise en œuvre de ces concepts dans une application à la problématique
concrète.
Le premier volet a plus mis l'accent sur les principes fondamentaux du
toolkit
, celui-ci va surtout expliciter et mettre en œuvre certains des
widgets
les plus courants (et utiles).
Avec l'article précédent, vous savez à présent d'où vient GTK
[1], quelles
sont ses possibilités et ses limitations. Vous avez surtout appris à créer une
fenêtre, ou tout autre widget
, mais aussi comment les organiser les uns par
rapport aux autres afin d'obtenir le design voulu.
Nous avons aussi abordé la façon de traiter les interventions des utilisateurs.
Que reste-t-il ? Le plus intéressant et le plus drôle (et le plus difficile). Vous allez maintenant apprendre à modéliser une structure de données, à gérer les interventions de l'utilisateur sur cette structure de données, à créer une interface MDI (Multi-Document Interface, interface multi-documents). Vous allez aussi voir comment permettre aux utilisateurs de sélectionner des éléments de cette structure de données et appliquer des actions à cette sélection.
Bien, assez parlé, passons à l'action :)
.
Pour les personnes qui n'ont pas lu le premier volet de le série, il est bon de rappeler ce qui a déjà été effectué dans le cadre de l'application d'exemple.
Il s'agit de fournir une interface d'administration de la base de données des adhérents d'une petite association de quelques dizaines de membres. On souhaite permettre à son président de visualiser la liste des adhérents, modifier cette liste et appliquer des actions à la sélection de certains adhérents dans cette liste.
Nous avons donc déjà vu comment créer les widgets
conteneurs qui vont
constituer l'ossature de l'interface utilisateur (fenêtre, table...) et nous
avons placé le menu principal de l'application.
Un commentaire dans le récapitulatif du code vous montrera où nous en sommes au début du second article.
Nous reprenons l'explication au moment où il va falloir s'occuper de la base de données (la modéliser, l'afficher...).
C'est une des fonctionnalités de la bibliothèque GTK qui a le plus évolué entre les versions 1 et 2. En fait l'API a changé du tout au tout, faisant une croix à l'encre indélébile sur la compatibilité ascendante.
Avec Gtk-Perl, les tableaux et listes de valeurs, les arbres, étaient le fait
des Gtk::CList
et Gtk::CTree
, ces éléments disparaissent complètement dans
Gtk2-Perl
. Pour faire face aux besoins grandissants et de plus en plus
pointus de cet élément de l'interface (avec des programmes comme les tableurs
par exemple), on est passé au MVC, le Model/View/Controller.
L'idée est la même que pour les menus ou pour la saisie et l'affichage de
texte : distribuer sur plusieurs classes de widgets ou d'objets GTK2
le
résultat final, obtenant ainsi une modularité qui permet de faire face à nombre
de problématiques.
Dans le cas présent, la modularisation s'est faite au niveau de la séparation (quasi-)naturelle entre données et rendu. Le coût est une complexification sensible du concept de tableau de valeurs, mais le mieux disant est net en termes de performances, de contrôle des données manipulées et de possibilités de rendu.
Et puis, la Gtk2-Perl team nous a concocté une version simplifiée...
Gtk2::SimpleList
Ce widget va prendre la place de feue la Gtk::CList
en rendant possible la
présentation de valeurs sous forme de tableau, à partir d'un widget unique et
d'un concept simple.
Mais la comparaison s'arrête là. Gtk2::SimpleList
est une version
sur-vitaminée de la Gtk::CList
. Tout en offrant une interface complète au
prototypage rapide, elle a malgré tout accès à tous les éléments avancés du MVC
car Gtk2::SimpleList
hérite de la classe Gtk2::TreeView
.
Par défaut, une Gtk2::SimpleList
construira un tableau simple à deux
dimensions présentant des listes de valeurs. Mais en tirant parti de son
héritage, en quelques lignes, on peut par exemple rendre actives les en-têtes de
colonnes (inertes par défaut), leur associer une fonction de rappel pour
effectuer une action lorsqu'elles sont cliquées.
Mais le grand avantage, non seulement de Gtk2::SimpleList
sur Gtk::CList
,
mais aussi d'avoir une version packagée de MVC, c'est que l'accès et la
manipulation de données ont été simplifiés.
Les données et leur représentation dans le widget sont liées (tied), ainsi une modification dans l'interface implique une modification de la structure de données sous-jacente et inversement.
La Gtk2::SimpleList
se construit de la façon classique :
$simplelist = Gtk2::SimpleList->new( colonne1 => 'type1', colonne2 => 'type2', ... );
On voit ici que le constructeur prend comme argument les noms des colonnes
(en-têtes) et le type qui leur est associé. Nous n'utiliserons que le type
text
, mais il y en a cinq autres (énumérés dans la documentation).
Une fois le widget
créé, la structure de données est simplement accessible
en tant qu'attribut de l'objet Gtk2::SimpleList
, sous la forme d'une
référence à une liste de listes :
@structure = @{$simplelist->{data}}; @premiere_ligne = @{$simplelist->{data}[0]};
Dans l'application (d'exemple) que nous développons pour notre association (qui
rénove des castels gascons, souvenez-vous ;-)
, nous allons avoir à gérer
plusieurs dizaines d'adhérents.
D'évidence, la Gtk2::SimpleList
qui va les présenter à l'écran va déborder
des limites qui lui sont imparties dans la fenêtre.
Il faut donc lui adjoindre un ou plusieurs widgets qui vont lui permettre de
se dérouler dans un espace fini (scrolling). La Gtk2::ScrolledWindow
remplira très bien ce rôle puisqu'elle cumule les fonctionnalités d'une barre de
défilement verticale, d'une barre horizontale et d'un conteneur. Le conteneur
est du type Gtk2::Bin
, c'est à dire qu'il ne peut contenir qu'un seul autre
widget.
C'est donc cette Gtk2::ScrolledWindow
que nous allons attacher à la table
(avec la méthode vue dans l'article précédent), avec les bons paramètres.
my $scrolled = Gtk2::ScrolledWindow->new(); $table->attach_defaults($scrolled, 0, 1, 0, 4); $scrolled->set_policy(qw/automatic automatic/); $scrolled->show();
On peut voir dans les précédentes lignes que la Gtk2::ScrolledWindow
est
construite de façon classique et qu'elle est attachée à la table, couvrant ses
quatre-cinquièmes. Avec la méthode set_policy
, a été définie la façon dont
apparaissent les barres de défilement. Le choix se porte sur automatic
, afin
que GTK juge de lui-même quand il est nécessaire qu'elles apparaissent ou pas.
C'est avec les lignes qui suivent que va être créé et surtout ajouté le tableau
de valeurs. Vous connaissez les arguments de création, ce sont les noms et la
nature des colonnes. Cette liste s'obtient facilement : il suffit de créer
une liste où la chaîne text
(le type de données choisi) se placera à la suite
de chaque élément de la liste des champs du fichier source (un fichier CSV).
TIMTOWTDI[2] mais la fonction map()
est le candidat idéal pour cela.
my @field = qw/GENRE NOM PRENOM ADRESSE/; my @header; @header = map { $_ => 'text' } @field; my $list = Gtk2::SimpleList->new(@header);
Puis, la Gtk2::SimpleList
est ajoutée au hachage qui regroupe tous les
widgets
auxquels nous voulons avoir facilement accès, ainsi qu'à la
Gtk2::ScrolledWindow
.
Nous appelons aussi une routine qui va définir quelles valeurs vont figurer dans le tableau (voir plus bas pour son explication). Ici, cette routine n'a que l'avantage de clarifier le code mais dans d'autres cas, elle peut par exemple vous servir à ne garnir le tableau de valeurs que de certaines préalablement sélectionnées.
$W{list} = $list; $scrolled->add($list); Fill_List();
Compte tenu de l'optique de cet article, une explication en détail de ce concept est hors de propos, mais un petit voyage au pays du MVC ne nous fera pas de mal et montrera comment passer sans douleur du simplifié au plus compliqué.
Le prétexte n'est en plus en aucun cas fallacieux puisqu'il s'agit d'ajouter une
fonctionnalité des plus classiques : le classement des lignes de valeurs par
ordre alphabétique croissant par rapport à la colonne NOM
.
GTK a prévu des routines pour le classement des données dans un tableau de
valeurs mais elles ne sont pas accessibles à la Gtk2::SimpleList
.
Pour cela, grâce aux méthodes héritées de Gtk2:TreeView
, nous allons
retrouver les éléments packagés dans la Gtk2::SimpleList
qui eux ont accès
à ces routines (ainsi qu'à d'autres fonctionnalités que nous allons voir aussi).
Nous avons besoin en l'occurrence du Model qui est la modélisation de la
structure de données (Gtk2::TreeModel
).
Il s'obtient à partir de la Gtk2::SimpleList
(qui hérite de
Gtk2::TreeView
) avec une méthode prosaïque s'il en est :
$model = $simplelist->get_model();
C'est en fait sur la structure de données que vont s'opérer les changements. Il
seront répercutés sur l'interface par toutes les fonctions de rappel installées
à la création de la Gtk2::SimpleList
.
Nous avons aussi besoin d'un autre élément du MVC : la colonne. C'est à partir des données qu'elle contient que va se faire le tri et c'est sur son en-tête que l'utilisateur va cliquer pour activer le tri.
La méthode pour obtenir l'objet Gtk2::TreeViewColumn
est aussi explicite que
la précédente :
$colonne = $simplelist->get_column($index);
Où $index
est l'indice de la colonne (en partant de zéro).
Dans notre application, nous les utiliserions comme suit.
my $mod = $list->get_model(); $mod->set_default_sort_func(sub { my ($model, $item1, $item2) = @_; my $a = $model->get($item1, 1); # récupération de la valeur de la 2ème my $b = $model->get($item2, 1); # colonne (indice 1) return $a cmp $b; } ); $list->set_headers_clickable(1); my $col = $list->get_column(1); # la colonne NOM, colonne numéro 2 $col->signal_connect('clicked', sub { my ($colonne, $model) = @_; $model->set_sort_column_id (1, 'ascending'); }, $mod );
Dans ces lignes, nous commençons par récupérer l'objet Gtk2::TreeModel
avec
la méthode susmentionnée. Puis nous définissons la routine de tri par défaut des
colonnes de la liste simple (et du model). Cette méthode est bien pratique
car bien souvent nous souhaitons trier toutes les colonnes de la même façon.
Vous avez donc compris qu'a contrario, il est possible de positionner une
routine de tri différente par colonne si vous le souhaitez.
A la suite de ceci, nous rendons "clickable" (à la manière d'un bouton) le titre
de la colonne (qui ne l'est pas défaut avec la liste simple Gtk2-Perl
).
Ensuite, nous isolons la colonne sur laquelle nous voulons baser le tri avec la
méthode get_column()
à laquelle est fourni l'argument 1
(indice de la
seconde colonne).
Nous connectons enfin le signal clicked
à cette colonne (ou plutôt à son seul
l'élément clickable, à savoir son en-tête).
Une fois cliqué, ce bouton donnera le signal au Gtk2::TreeModel
(passé en
argument) d'effectuer le tri.
Lorsque l'utilisateur s'en mêle, il y a besoin de savoir, entre autres choses, sur quoi il souhaite travailler. De même, alors que le programme n'est plus linéaire, il faut malgré tout faire en sorte de prévoir toutes les combinaisons possibles d'utilisation de votre programme.
Et prévoir quelques lignes de code pour chacun des cas de figure...
Dans le cadre d'un tableau de valeurs, c'est la sélection qui va dire ce sur
quoi l'utilisateur souhaite travailler. Cette dernière, dans Gtk2-Perl
, est
une classe à part entière : le Gtk2::TreeSelection
.
Vous pouvez donc vous douter qu'il y a là aussi des possibilités de traitement
et de manipulation accrues.
Au sein de notre application, nous obtenons cet objet à partir du widget
Gtk2::SimpleList
par le biais de la méthode get_selection()
. Nous lui
appliquons alors une méthode qui va modifier le type de sélection des lignes du
tableau. En l'occurrence, l'utilisateur peut désormais choisir plusieurs lignes
à la fois.
La Gtk2::TreeSelection
n'est pas un widget
mais il est tout de même
possible d'interagir avec cet objet par l'intermédiaire de signaux.
Nous lui connectons donc classiquement un signal pour pouvoir agir dès qu'une
modification sur la sélection est faite (le signal changed
est émis dès que
la sélection change : ajout, retrait ou annulation).
my $select = $list->get_selection(); $select->set_mode('multiple'); $select->signal_connect('changed', \&row_to_entry, $list); $list->show();
A noter que nous allons chercher l'objet Gtk2::TreeSelection
uniquement parce
que nous avons vraiment besoin de le manipuler. Autrement, on peut obtenir les
composants de la sélection directement à partir de l'objet Gtk2::SimpleList
avec la méthode get_selected_indices()
.
Nous allons à présent nous occuper de la partie de l'interface graphique où
seront placés les widgets
concourant à l'interaction entre l'utilisateur et
les valeurs dans le tableau au-dessus.
Cette section de l'interface sera composée de deux éléments verticaux l'un à
côté de l'autre (dans une Gtk2::HBox
donc, attachée au tableau principal).
my $saisie = Gtk2::HBox->new(); $table->attach_defaults($saisie, 0, 1, 4, 5);
Le premier élément (une Gtk2::VBox
) regroupera les cadres de saisie (autant
que de champs), empilés deux à deux. Pour ce faire, nous allons boucler sur une
liste de quatre éléments, allant de 0 à 3. Les cadres de saisie sont alignés
dans une boîte horizontale, l'un à côté de l'autre. Mais à chaque indice pair
(calculé par le modulo), on crée une nouvelle boîte qu'on empaquette (méthode
pack_start_defaults()
), descendant ainsi les cadres de saisie d'un niveau.
Au-dessus de chacun de ces cadres de saisie, sont placés des labels tirés de la
valeur courante dans le tableau des champs (@field
).
Labels et cadres sont empaquetés et on connecte un signal à chaque cadre.
On utilise le signal changed qui se déclenche lorsque la chaîne dans le cadre est changée (ajout, retrait de caractères, effacement, etc...). Ainsi, avec la fonction de rappel qui est attachée au signal, il est possible d'avoir la chaîne entrée dans le cadre de saisie toujours prête à l'emploi. En l'occurrence, la chaîne est stockée dans un hachage avec comme clé le label courant.
my $entree = Gtk2::VBox->new(0, 0); $saisie->pack_start_defaults($entree); my %info; my $current_hbox; for(0..3) { unless($_%2) { $current_hbox = Gtk2::HBox->new(); $entree->pack_start_defaults($current_hbox); } my $label = Gtk2::Label->new("$field[$_] :"); $current_hbox->pack_start_defaults($label); my $valeur = Gtk2::Entry->new(); $W{$field[$_]} = $valeur; $current_hbox->pack_start_defaults($valeur); $valeur->signal_connect('changed', sub{ my ($widget, $label) = @_; $info{$label} = $widget->get_text(); }, $field[$_] ); }
Le deuxième élément (une Gtk2::VBox
également) présentera trois boutons. Il y
en aura un pour vider les cadres de leur contenu, un pour récupérer les entrées
des cadres pour en faire une ligne supplémentaire et le dernier modifie les
valeurs de la ligne sélectionnée.
Les trois boutons sont reliés à la même fonction de rappel mais elle saura aisément faire la différence entre chacun, puisqu'est envoyé leur label respectif en argument à la fonction.
my @bouton = qw/RAZ créer modifier/; my $button_box = Gtk2::VBox->new(1, 3); $saisie->pack_start_defaults($button_box); for(0..2) { my $b = Gtk2::Button->new($bouton[$_]); $button_box->pack_start($b, 0, 0, 0); $b->signal_connect('clicked', \&Button, $bouton[$_]); $W{$bouton[$_]} = $b; }
Enfin, on montre tous les widgets
se trouvant dans la boîte horizontale
principale de cette partie de l'interface avec la méthode show_all()
.
$saisie->show_all();
L'interface d'administration est à présent (fruste mais) faite et nous souhaitons encore rajouter une fonctionnalité à notre programme : l'envoi de courriel à tout ou partie de la liste des adhérents.
On pourrait opter pour une solution qui se baserait sur l'édition en externe du
courriel et sa récupération par notre programme mais ce serait passer à côté
d'une autre des nouveautés intéressantes de Gtk2-Perl : la saisie de texte.
Non pas que la saisie de texte n'était pas possible avant mais plutôt que GTK
dans sa nouvelle version propose des widgets
de saisie qui intègrent nativement
des fonctionnalités d'édition comme le copier-coller.
C'est largement assez pour nos besoins actuels et au prix de très peu de lignes
de code.
Oui, mais alors où intégrer la saisie du courriel au sein d'une interface déjà bien remplie ?
Il y a plusieurs solutions à cette problématique comme notamment l'édition du courriel dans une seconde fenêtre. Mais ces interfaces à plusieurs fenêtres sont souvent malaisées et surtout répondent à des besoins plus complexes.
En ce qui nous concerne, nous n'avons besoin que de deux masques, le MDI est la solution idéale.
En anglais Multi-Document Interface, et en français à peu près la même chose : interface multi-documents, aussi appelés couramment fenêtre à onglets. Il s'agit d'une interface où se superposent des pages de taille égale qui sont sélectionnées tour à tour par l'utilisateur en cliquant sur leur titre. Dans le cas qui nous intéresse, cela va nous permettre d'avoir deux interfaces en pleine page au sein d'une même fenêtre avec un passage aisé de l'une à l'autre des interfaces.
Le MDI en Gtk2-Perl
est le fait des Gtk2::Notebook
. Ce widget
est un
conteneur Gtk2::Container
tout ce qu'il y a de plus classique puisque l'on va
pouvoir lui ajouter à l'infini des widgets
fils (les pages) qui pourront être
n'importe quel autre Gtk2::Widget
. La différence avec les autres conteneurs
ne se fera qu'au niveau du rendu.
Ainsi vous pouvez voir que l'utilisation du Gtk2::Notebook
est très simple
d'abord et que les changements à apporter à notre interface pour le prendre en
compte seront minimes.
Le notebook va s'intercaler entre la boîte verticale principale et la table
qui contient le tableau de valeurs et les cadres de saisie : c'est le
notebook que l'on va empaqueter (méthode pack_start()
) dans la boîte et
la table Gtk2::Table
deviendra la première page du notebook.
En lieu et place des lignes :
$vbox->pack_start($table, 1, 1, 1); $table->show();
On aura :
my $book = Gtk2::Notebook->new(); $book->show(); $vbox->pack_start($book,1,1,0); $book->append_page( $table, 'Adherents' ); $table->show();
Vous voyez ici la création d'un Gtk2::Notebook
, à la manière de n'importe
quel autre widget
. Nous n'oublions pas de le montrer pour qu'il apparaisse à
l'écran.
La méthode pour lui ajouter des pages (n'importe quel widget
) est la méthode
append_page()
. Les arguments sont simples, le widget
fils tout d'abord et
le nom de la page (qui apparaîtra sur l'onglet).
Comme évoqué plus haut, voilà encore un point où l'évolution entre GTK et GTK2
est manifeste. De la même façon qu'avec le MVC, on a éclaté les fonctionnalités
de rendu et de saisie de texte en un grand nombre de widgets
et d'objets
GTK2. Là aussi, la modularisation se fait au niveau de la séparation entre
données et rendu.
Basiquement, le texte va être entré, modifié, copié, collé dans un tampon
Gtk2::TextBuffer
, composé d'itérations Gtk2::TextIter
(caractères) et
montré à travers un visualiseur Gtk2::TextView
.
Cette implémentation va donc permettre plusieurs types de rendus pour le même
texte, des règles de rendu uniques pour de multiples textes. De plus chaque
morceau choisi du texte (possiblement la totalité) sera borné par des objets
Gtk2::TextIter
qui permettront d'appeler là encore des méthodes concourant à
une manipulation plus fine du texte.
Le tampon et le visualiseur se créent de façon simple comme suit :
my $buffer=Gtk2::TextBuffer->new(); my $view=Gtk2::TextView->new_with_buffer($buffer);
Sachez que ce n'est qu'une des façons de procéder. De nombreuses méthodes permettent de retrouver les uns par rapport aux autres. En l'occurrence, nous créons au préalable le tampon qui est adjoint à un visualiseur au moment de la construction de ce dernier (mais nous aurions pu faire l'inverse, par exemple).
Nous allons de suite connecter un signal afin de positionner une variable qui contiendra le contenu du texte du courriel.
Pour récupérer tout ou partie du texte qui se trouve dans le tampon
(Gtk2::textBuffer
), il faut bien sûr préciser quels sont les caractères de
début et de fin de la portion que vous voulez récupérer.
Les caractères d'un texte sont des itérations (Gtk2::TextIter
) que vous
pouvez obtenir avec la méthode get_iter_at_offset()
. Elle prend comme
argument la position du caractère qui vous intéresse (en partant de 0). Il y a
la possibilité (comme avec les listes Perl) de préciser des positions négatives.
-1 indiquera le dernier caractère du texte, -2 l'avant-dernier et ainsi de
suite.
En ce qui nous concerne, nous procéderions ainsi :
$buffer->signal_connect( 'changed', sub{ my $buffer=shift; my $start=$buffer->get_iter_at_offset(0); my $end =$buffer->get_iter_at_offset(-1); $info{text}=$buffer->get_text($start,$end,0); } );
Après avoir obtenu les objets Gtk2::textIter
, nous les donnons en arguments à
la méthode get_text()
(le troisième argument, le 0, est un booléen qui doit
être laissé ainsi en ce qui nous concerne). Avec cette méthode, nous stockons le
contenu du texte du courriel dans le hachage dans lequel nous gardons un certain
nombre de valeurs courantes.
De cette façon, à la manière des cadres de saisie dans la première page de l'interface, nous avons en permanence la valeur courante du texte. En l'occurrence, ce n'est probablement pas la meilleure façon de procéder : vous faites travailler votre programme à chaque saisie de caractère alors que vous n'en avez besoin qu'à la toute fin lorsque vous envoyez le courriel.
Néanmoins, optimale ou pas, vous avez vu qu'il est possible d'implémenter de
l'édition de texte simple mais native, pour le coût d'un nombre réduit de lignes
de code.
Grâce au travail des auteurs de GTK, vous proposez à l'utilisateur les
fonctionnalités (incluses nativement) de couper-copier-coller (entre autres)
avec les raccourcis clavier habituels (respectivement Ctrl-X
, Ctrl-C
et
Ctrl-V
). Les fanatiques du cliquodrome retrouverons tout cela dans un menu
contextuel activé par un clic droit.
Nous en arrivons à la dernière partie de la fonctionnalité d'envoi de courriel. Il manque encore la possibilité d'envoyer en plus du texte une pièce jointe (une photographie comme précédemment, mais aussi un récépissé, une facture).
Nous allons donc rajouter à l'interface des éléments qui vont permettre de sélectionner un fichier qui sera joint au courriel envoyé à la fin des opérations.
Tant que nous y sommes, nous allons aussi implémenter la sauvegarde du texte du message courant si le président veut les garder, à titre d'historique par exemple.
C'est le même widget
qui va nous servir dans les deux cas : le
Gtk2::FileSelection
.
Ce widget
est en fait l'empaquetage d'un certain nombre d'autres afin de
proposer au développeur des fonctionnalités ultra-classiques prêtes à l'emploi.
C'est aussi le cas pour plusieurs autres widgets
, citons la
Gtk2::ScrolledWindow
que nous avons vue, le Gtk2::Combo
que nous ne
verrons pas ou encore la Gtk2::SimpleList
pour les bibliothèques
additionnelles.
Pour en revenir au Gtk2::FileSelection
, il est composé de pas moins de
dix-huit éléments qu'il est possible de retrouver grâce à des méthodes ad hoc.
Pour notre part, nous aurons besoin :
cancel_button
(Gtk2::Button
)
C'est le bouton sur lequel l'utilisateur clique pour signifier que l'action de sélection est annulée.
ok_button
(Gtk2::Button
)
C'est au contraire le bouton qui va clore et valider la sélection effectuée.
Même si le Gtk2::FileSelection
est nouveau pour nous, nous sommes en partie
en terrain connu. Nous allons au final manipuler plus de choses que nous avons
déjà vues que le contraire.
Notre travail ici va se borner à placer les bonnes fonctions de rappel aux bons
widgets
(ici des boutons) afin de diriger l'utilisateur dans la bonne
direction. Il faut savoir pour cela que les auteurs de GTK ont essayé de trouver
le juste milieu entre un bon packaging et une liberté de conception maximum
pour le développeur.
Ainsi, par exemple lorsque l'utilisateur clique sur le bouton OK
pour
signifier que son choix est fait, le choix est bien sûr validé mais c'est à vous
de fermer la fenêtre de dialogue. Les auteurs de GTK ont voulu prévoir le cas où
vous souhaiteriez encore manipuler le widget
ou obtenir des informations avant
de le détruire.
Dans notre interface, le sélecteur de fichier va être présenté comme c'est l'habitude avec un label pour signifier de quelle information il s'agit. Il y aura aussi un cadre de saisie pour permettre à l'utilisateur de rentrer l'information à la main ou de rappeler la sélection courante. Enfin, le sélecteur de fichier apparaîtra à la demande de l'utilisateur grâce à un clic sur un bouton.
Tous ces widget
s vont bien entendus être empaquetés (méthode pack_start()
)
dans une boîte horizontale.
my $file_box=Gtk2::HBox->new(); my $file_label=Gtk2::Label->new('Fichier joint'); $file_box->pack_start_defaults($file_label); my $file_entry=Gtk2::Entry->new(); $file_box->pack_start_defaults($file_entry); $W{file_entry}=$file_entry; my $file_button=Gtk2::Button->new('Parcourir'); $file_box->pack_start_defaults($file_button); $file_button->signal_connect( clicked=>\&File_Select, $file_entry ); $file_box->show_all();
Idem pour la sauvegarde des messages :
my $message_box=Gtk2::HBox->new(); my $message_label=Gtk2::Label->new('Fichier de sauvegarde'); $message_box->pack_start_defaults($message_label); my $message_entry=Gtk2::Entry->new(); $message_box->pack_start_defaults($message_entry); $W{message_entry}=$message_entry; my $message_button=Gtk2::Button->new('Parcourir'); $message_box->pack_start_defaults($message_button); $message_button->signal_connect( clicked=>\&File_Select, $message_entry ); $message_box->show_all();
Vous avez sans doute remarqué que, malgré le signal show_all()
, aucun des
widgets
créé dans la section précédente ne peut être visible encore. Ils ont
été construits certes mais pas rattachés à l'interface.
Pour ce faire, les boîtes de regroupement vont nous être indispensables. La
deuxième page du Gtk2::Notebook
sera une Gtk2::VBox
dans laquelle se
placeront les autres éléments de l'interface (saisie de texte et sélecteur de
fichier principalement).
Nous aurions pu utiliser une Gtk2::Table
comme dans la première page mais ce
qui suit sera un bon exemple de l'utilisation des options de rangement dans les
boîtes.
Vous allez voir que, si des proportions strictes entre widgets
ne sont pas
indispensables, il est tout autant possible de maîtriser le design de
l'application avec les boîtes.
my $text_box=Gtk2::VBox->new();
La méthode utilisée ici donne l'instruction au widget
de prendre toute la
place qu'il peut (et de ne pas mettre de pixels d'espace entre lui et les autres
widgets
).
$text_box->pack_start_defaults($view);
Nous donnons l'ordre aux widgets
de prendre la place minimum.
$text_box->pack_start($file_box,0,0,0); $text_box->pack_start($message_box,0,0,0);
$book->append_page( $text_box, 'courriel' );
$text_box->show_all();
Comme c'est expliqué dans le précédent article, on montre la fenêtre principale en dernier afin que l'application apparaisse d'un coup.
Et enfin, une fois que toute la phase préparatoire de l'interface est exécutée, on peut entrer dans la boucle principale GTK.
$window->show(); Gtk2->main();
Nous en sommes arrivés à la boucle principale, le programme va attendre ici les événements et signaux pour agir, ou encore la sortie de la boucle et donc du programme.
C'est notamment après que vont être placées les routines et autres fonctions de rappel.
select_all()
La première fonction de rappel est là plus pour des raisons de lisibilité du code, nous aurions pu l'imbriquer dans la construction du menu.
Cette routine va, à la suite de l'activation de l'élément du menu "Edition",
sélectionner toutes les lignes du tableau de valeurs. Le premier argument
qu'elle reçoit est la donnée additionnelle (et non pas le widget
émetteur
comme c'est l'usage habituellement). C'est le Gtk2::SimpleMenu (qui nous a servi
à construire le menu) qui prévoit les choses ainsi.
En l'occurrence, la donnée additionnelle est la référence au hachage qui
recense un certain nombre de widgets
. Grâce à ce dernier nous récupérons
le widget
qui nous intéresse : le tableau de valeurs (Gtk2::SimpleList
)
contenu dans la valeur $W->{list}
.
Ensuite, nous chaînons deux méthodes, la première pour récupérer l'objet
Gtk2::TreeSelection
et la seconde pour envoyer à ce dernier le signal
select-all
.
List_Fill()
Dans la première partie de cette routine, au cas où @DATA
est vide, on lit
l'ensemble des données adhérents dans le fichier. On découpe chaque ligne
suivant le séparateur (la tabulation) et on empile la liste résultante à la
des autres dans @DATA
. On obtient donc une liste de listes, qui est la
structure de données attendue.
Dans la seconde partie de la routine, en fonction d'une éventuelle liste
d'indices passés en paramètres, on détermine quels éléments de @DATA
doivent
être affichés.
On applique le résultat à la Gtk2::SimpleList
en manipulant directement
l'attribut data
de la liste.
sub Fill_List { unless(@DATA) { open LIRE, "/home/president/adherents.csv" or die "$!\n"; while(<LIRE>) { chomp; push @DATA, [split "\t"]; } close LIRE; } my @row; if(@_) { @row = @_; } else { @row = (0..$#DATA); } @{$W{list}->{data}} = @DATA[@row]; }
row_to_entry()
Cette autre fonction de rappel va placer le contenu des cellules de la ligne sélectionnée à l'intérieur des cadres de saisies correspondants.
Pour retrouver les valeurs à placer dans les cadres, il nous faut l'indice de la
ligne sélectionnée. Nous l'obtenons en deux temps. La méthode
get_selected_row()
appelée par la Gtk2::TreeSelection
(l'objet qui a émis
le signal) nous renvoie un objet Gtk2::TreePath
qui nous renvoie
l'information recherchée avec la méthode as_string()
).
Par la suite, nous formons un hachage avec les champs en clés et les valeurs des cellules de la ligne courante en valeurs. Il ne reste plus qu'à faire le tour cadres de saisie, identifiés grâce à l'intitulé qui leur est associé, et de récupérer les valeurs dans le hachage avec la clé correspondante.
C'est la méthode set_text()
qui nous permet de d'indiquer aux cadres de
saisie la valeur à afficher.
sub row_to_entry { my ($select, $list) = @_; my $row = $select->get_selected_rows(); return unless $row; $row = $row->to_string(); my %valeur; @valeur{@field} = @{$list->{data}[$row]}; $W{$_}->set_text($valeur{$_}) for @field; }
Button()
Ici, on va traiter les informations présentes ou saisies dans les cadres prévus à cet effet, et appliquer le comportement attendu en fonction du bouton.
Les boutons sont aisément identifiables les uns des autres puisque, rappelez-vous, ils envoient en argument supplémentaire le label qui leur est associé. Il est donc pratiqué des tests là-dessus.
Dans le premier cas, on retrouve le widget dont on veut modifier le contenu
grâce au label qui sert aussi de clé dans le hachage %W
. Puis nous modifions
le contenu du cadre de saisie avec la méthode que nous connaissons déjà avec
comme argument une chaîne vide.
Le second bouton vise à récupérer les valeurs des cadres et créer avec une
nouvelle ligne dans le tableau de valeurs. Nous créons pour cela une liste qui
contient toutes les valeurs courantes dans le bon ordre grâce à @field
, et
ajoutons cette liste dans la structure de données figurant le contenu du tableau
de valeurs.
A noter que l'ajout en question se fait comme s'il s'agissait de n'importe
liste classique et que ce changement se répercute sur l'affichage automatiquement
grâce à la magie de la Gtk2::SimpleList
.
sub Button { my ($b, $label) = @_; if($label eq 'RAZ') { $W{$_}->set_text('') for @field; } elsif($label eq 'créer') { my @baggy; push @baggy, $info{$_} for @field; push @{$W{list}->{data}}, [@baggy]; } else { my ($row) = $W{list}->get_selected_indices(); my @baggy; push @baggy, $info{$_} for @field; @{$W{list}->{data}[$row]} = @baggy; } }
File_Select()
Cette fonction permet de renvoyer un fichier sélectionné par un
Gtk2::FileSelection
et de l'insérer dans le cadre de saisie afférent.
Nous commençons par récupérer la bonne Gtk2::Entry
dans laquelle insérer le
nom du fichier. Nous créons ensuite le sélecteur de fichier en renseignant le
nom que nous souhaitons lui donner (obligatoire) et nous le montrons.
Ensuite, comme expliqué dans le paragraphe qui concerne le
Gtk2::FileSelection
, nous connectons des signaux aux widgets
le composant.
Au cancel_button
, nous rattachons une petite routine qui récupère le
sélecteur de fichier et le détruit.
sub File_Select{ my ($widget,$entry)=@_; my $f=Gtk2::FileSelection->new('Fichier Joint'); $f->show(); $f->cancel_button->signal_connect( 'clicked', sub{ my ($button,$f)=@_; $f->destroy(); }, $f ); }
Au ok_button
, on connecte une routine qui récupère en deux temps (voir le
paragraphe des signaux du premier article) le sélecteur de fichier et le cadre
de saisie.
Ensuite, nous obtenons le fichier sélectionné, nous détruisons le sélecteur et
insérons le nom du fichier dans le cadre de saisie (Gtk2::Entry
).
$f->ok_button->signal_connect( 'clicked', sub{ my ($widget,$arg)=@_; my ($f,$entry)=@$arg; my ($file)=$f->get_selections(); $f->destroy(); $entry->set_text($file); }, [$f,$entry] );
Msg_Send()
Le but ultime de notre application :)
C'est ici que nous allons enfin retrouver les différentes valeurs saisies par
l'utilisateur pour les formater comme il se doit (voir les Linux Magazine n°59,
60 et 61) et les envoyer.
Cette routine vous montre comment récupérer les valeurs qui nous intéressent à
partir de différents widgets
.
Vous connaissez la méthode get_text()
qui retourne le contenu des cadres de
saisie (Gtk2::Entry
). Il y a aussi la méthode get_selected_indices
qui
retourne la liste des indices des lignes sélectionnées dans la
Gtk2::SimpleList
(sur laquelle nous bouclons pour envoyer un message à tous
les destinataires).
sub Msg_Send { # ici on récupère les données rentrées par l'utilisateur via l'interface # on formate et envoie le courriel ensuite. # cf. Linux Mag N° FIXME pour le détail du code d'envoi d'un mail à partir des # données obtenues ici. my ($widget,$arg)=@_; my ($W,$info)=@$arg; my $body=$info->{text}; my $file_save=$W->{message_entry}->get_text(); my $attach=$W->{file_entry}->get_text(); for($W->{list}->get_selected_indices()) { my %adherent; @adherent{@field}=@{$W->{list}->{data}[$_]}; # Formatage et envoi de courriel } }
Voilà, nous pouvons à présent livrer cette première version (à peu près) utilisable au président de notre association. Même si le président nous remercie chaudement pour cette aide, nous restons insatisfaits quelque part.
La fenêtre n'a pas de taille par défaut, l'interface n'est pas très aérée, les
options sont très peu nombreuses, certains widgets
ne sont pas alignés et il
n'y a pas de barre de progression pour faire patienter le président pendant que
partent les courriels.
La faute à qui, à quoi ?
La faute à cet article et à sa ligne directrice, pas à GTK qui vous permet bien évidemment de remédier à toutes ces imperfections.
En attendant un éventuel article qui s'occuperait de ces aspects de l'interface utilisateur, allez jeter un œil, et même plus, dans la documentation.
Avec ce que vous avez appris dans ces deux articles, sa lecture devrait vous être limpide et très rapidement fructueuse.
GTK le GIMP ToolKit (la boîte à outils de GIMP). Le toolkit graphique spécialement développé pour le programme de manipulation d'image GIMP.
Une des devises de Perl : There Is More Than One Way To Do It. En français :Il y a plus d'une façon de le faire.
la documentation sur tie()
en français sur le site des mongueurs.
Le nouveau site de référence de Gtk2-Perl
La documentation de référence
Le site de GTK+.
#!/usr/bin/perl -w use Gtk2 '-init'; use Gtk2::SimpleMenu; use Gtk2::SimpleList; use strict; my %W; my @DATA; my $window = Gtk2::Window->new('toplevel'); $window->signal_connect('delete_event', sub{Gtk2->main_quit();}); $W{main} = $window; my $vbox = Gtk2::VBox->new(); $vbox->show(); $window->add($vbox); $W{vbox} = $vbox; my $menu_model = [ _File => { item_type => '<Branch>', children => [ Quit => { callback => sub{Gtk2->main_quit()}, callback_action => 1, accelerator => '<ctrl>Q' } ] }, _Edition => { item_type => '<Branch>', children => [ 'Tout Sélectionner' => { callback => \&select_all, callback_action => 2, accelerator => '<ctrl>A' } ] } ]; my $menu = Gtk2::SimpleMenu->new( menu_tree => $menu_model, user_data => \%W ); my $menubar = $menu->{widget}; $W{menubar} = $menubar; $vbox->pack_start($menubar, 0, 0, 1); $menubar->show(); $W{menu} = $menu; $window->add_accel_group($menu->{accel_group}); my $table = Gtk2::Table->new(5, 1, 1); my $book = Gtk2::Notebook->new(); $book->show(); $vbox->pack_start($book,1,1,0); $book->append_page( $table, 'Adherents' ); $table->show(); ########################## # fin du premier article # ########################## my $scrolled = Gtk2::ScrolledWindow->new(); $table->attach_defaults($scrolled, 0, 1, 0, 4); $scrolled->set_policy(qw/automatic automatic/); $scrolled->show(); my @field = qw/GENRE NOM PRENOM ADRESSE/; my @header; @header = map { $_ => 'text' } @field; my $list = Gtk2::SimpleList->new(@header); $W{list} = $list; $scrolled->add($list); Fill_List(); my $mod = $list->get_model(); $mod->set_default_sort_func(sub { my ($model, $item1, $item2) = @_; my $a = $model->get($item1, 1); # récupération de la valeur de la 2ème my $b = $model->get($item2, 1); # colonne (indice 1) return $a cmp $b; } ); $list->set_headers_clickable(1); my $col = $list->get_column(1); # la colonne NOM, colonne numéro 2 $col->signal_connect('clicked', sub { my ($colonne, $model) = @_; $model->set_sort_column_id (1, 'ascending'); }, $mod ); my $select = $list->get_selection(); $select->set_mode('multiple'); $select->signal_connect('changed', \&row_to_entry, $list); $list->show(); my $saisie = Gtk2::HBox->new(); $table->attach_defaults($saisie, 0, 1, 4, 5); my $entree = Gtk2::VBox->new(0, 0); $saisie->pack_start_defaults($entree); my %info; my $current_hbox; for(0..3) { unless($_%2) { $current_hbox = Gtk2::HBox->new(); $entree->pack_start_defaults($current_hbox); } my $label = Gtk2::Label->new("$field[$_] :"); $current_hbox->pack_start_defaults($label); my $valeur = Gtk2::Entry->new(); $W{$field[$_]} = $valeur; $current_hbox->pack_start_defaults($valeur); $valeur->signal_connect('changed', sub{ my ($widget, $label) = @_; $info{$label} = $widget->get_text(); }, $field[$_] ); } my @bouton = qw/RAZ créer modifier/; my $button_box = Gtk2::VBox->new(1, 3); $saisie->pack_start_defaults($button_box); for(0..2) { my $b = Gtk2::Button->new($bouton[$_]); $button_box->pack_start($b, 0, 0, 0); $b->signal_connect('clicked', \&Button, $bouton[$_]); $W{$bouton[$_]} = $b; } $saisie->show_all(); my $buffer=Gtk2::TextBuffer->new(); my $view=Gtk2::TextView->new_with_buffer($buffer); $buffer->signal_connect( 'changed', sub{ my $buffer=shift; my $start=$buffer->get_iter_at_offset(0); my $end =$buffer->get_iter_at_offset(-1); $info{text}=$buffer->get_text($start,$end,0); } ); my $file_box=Gtk2::HBox->new(); my $file_label=Gtk2::Label->new('Fichier joint'); $file_box->pack_start_defaults($file_label); my $file_entry=Gtk2::Entry->new(); $file_box->pack_start_defaults($file_entry); $W{file_entry}=$file_entry; my $file_button=Gtk2::Button->new('Parcourir'); $file_box->pack_start_defaults($file_button); $file_button->signal_connect( clicked=>\&File_Select, $file_entry ); $file_box->show_all(); my $message_box=Gtk2::HBox->new(); my $message_label=Gtk2::Label->new('Fichier de sauvegarde'); $message_box->pack_start_defaults($message_label); my $message_entry=Gtk2::Entry->new(); $message_box->pack_start_defaults($message_entry); $W{message_entry}=$message_entry; my $message_button=Gtk2::Button->new('Parcourir'); $message_box->pack_start_defaults($message_button); $message_button->signal_connect( clicked=>\&File_Select, $message_entry ); $message_box->show_all(); my $send=Gtk2::Button->new('Envoyer'); $send->signal_connect('clicked',"Msg_Send",[\%W,\%info]); my $text_box=Gtk2::VBox->new(); my $entry_button_box=Gtk2::HBox->new(); my $entry_box=Gtk2::VBox->new(); $entry_box->pack_start($file_box,0,0,0); $entry_box->pack_start($message_box,0,0,0); $entry_button_box->pack_start_defaults($entry_box); $entry_button_box->pack_start($send,0,0,0); $text_box->pack_start_defaults($view); $text_box->pack_start($entry_button_box,0,0,0); $book->append_page( $text_box, 'courriel' ); $text_box->show_all(); $window->show(); Gtk2->main(); sub select_all { my $W=shift; $W->{list}->get_selection()->select_all(); } sub Fill_List { unless(@DATA) { open LIRE, "/home/president/adherents.csv" or die "$!\n"; while(<LIRE>) { chomp; push @DATA, [split "\t"]; } close LIRE; } my @row; if(@_) { @row = @_; } else { @row = (0..$#DATA); } @{$W{list}->{data}} = @DATA[@row]; } sub row_to_entry { my ($select, $list) = @_; my $row = $select->get_selected_rows(); return unless $row; $row = $row->to_string(); my %valeur; @valeur{@field} = @{$list->{data}[$row]}; $W{$_}->set_text($valeur{$_}) for @field; } sub Button { my ($b, $label) = @_; if($label eq 'RAZ') { $W{$_}->set_text('') for @field; } elsif($label eq 'créer') { my @baggy; push @baggy, $info{$_} for @field; push @{$W{list}->{data}}, [@baggy]; } else { my ($row) = $W{list}->get_selected_indices(); my @baggy; push @baggy, $info{$_} for @field; @{$W{list}->{data}[$row]} = @baggy; } } sub File_Select{ my ($widget,$entry)=@_; my $f=Gtk2::FileSelection->new('Fichier Joint'); $f->show(); $f->cancel_button->signal_connect( 'clicked', sub{ my ($button,$f)=@_; $f->destroy(); }, $f ); $f->ok_button->signal_connect( 'clicked', sub{ my ($widget,$arg)=@_; my ($f,$entry)=@$arg; my ($file)=$f->get_selections(); $f->destroy(); $entry->set_text($file); }, [$f,$entry] ); } sub Msg_Send { # ici on récupère les données rentrées par l'utilisateur via l'interface # on formate et envoie le courriel ensuite. # cf. Linux Magazine N°60 pour le détail du code d'envoi d'un mail à # partir des données obtenues ici. my ($widget,$arg)=@_; my ($W,$info)=@$arg; my $body=$info->{text}; my $file_save=$W->{message_entry}->get_text(); my $attach=$W->{file_entry}->get_text(); for($W->{list}->get_selected_indices()) { my %adherent; @adherent{@field}=@{$W->{list}->{data}[$_]}; # formatage et envoi } }
Copyright © Les Mongueurs de Perl, 2001-2011
pour le site.
Les auteurs conservent le copyright de leurs articles.