[couverture de Linux Magazine 64]

Création d'interface utilisateur avec Perl et GTK (2)

Article publié dans Linux Magazine 64, septembre 2004.

Copyright © 2004 - David Elbaz.

[+ del.icio.us] [+ Developers Zone] [+ Bookmarks.fr] [Digg this] [+ My Yahoo!]

Chapeau

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

Introduction

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

Memorandum

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

Le tableau de valeurs

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

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

Les adhérents

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

Model/View/Controller

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

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

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

La sélection

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

La saisie d'informations

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

Le courriel

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 ?

MDI

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

La saisie de texte

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.

Ouvrir ou sauvegarder un fichier

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 :

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

Mise en forme

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.

Finalisation

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

Les fonctions de rappel

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

Conclusion

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.

Notes de l'article

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

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

Références de l'article

Récapitulatif du code

    #!/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
          }
       }

[IE7, par Dean Edwards] [Validation du HTML] [Validation du CSS]