[couverture de Linux Magazine 70]

wxPerl: le mariage réussi de Perl et wxWidgets (1)

Article publié dans Linux Magazine 70, mars 2005.

Copyright © 2005 - Christophe Massaloux.

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

Chapeau

La bibliothèque wxWidgets est écrite en C++ et dédiée à la conception d'applications IHM (Interface Homme-Machine) multi plates-formes (Unix/Windows/Mac). Elle utilise s'il y a lieu les bibliothèques natives de ces plates-formes garantissant un « look and feel » homogène avec le reste des applications. Nous la présentons dans le cadre du langage Perl.

Introduction

Lors de précédents articles, les Mongueurs vous ont déjà présenté les modules Tk et GTK qui sont, avec Qt, les bibliothèques d'interface homme-machine (IHM) les plus courantes sous Linux. Comme il y a beaucoup à dire, nous engagerons dans cet article la découverte de wxPerl afin de vous en présenter l'essentiel, et nous garderons au chaud quelques morceaux choisis pour une dégustation une prochaine fois.

Présentation de wxWidgets (ex-wxWindows)

La bibliothèque wxWidgets fournit une API simple et cohérente dédiée à la conception d'applications IHM (Interface Homme-Machine) multi plates-formes (Unix/Windows/Mac).

Le programmeur peut écrire une unique version de son code source, le compiler sous Unix, Windows ou Mac OS avec son compilateur C++ préféré, et son application fonctionnera de façon identique sur chaque plate-forme. Elle adoptera automatiquement l'apparence de la plate-forme considérée.

En plus de ses classes dédiées à l'IHM, wxWidgets offre beaucoup d'autres fonctionnalités prêtes à l'emploi, qui en font un véritable « framework » de développement d'applications complètes et pour tous les domaines.

La genèse de wxWindows (le nom originel de wxWidgets) remonte à 1992, à l'Artificial Intelligence Applications Institute de l'Université d'Edimbourg, où un développeur du nom de Julian Smart avait besoin de disposer d'un toolkit de GUI à moindre frais. Il devait lui permettre le développement d'applications conjointement sous MS Windows et sous Unix/X11 (d'où le nom « w » pour Windows et « x » pour X11). Les seuls produits disponibles étant trop chers, il décida de le développer lui-même.

L'AIAI accepta de publier le code sur l'Internet, ce qui incita nombre de développeurs talentueux à se joindre au projet, permettant par la suite d'y ajouter de nombreuses fonctionnalités et de le porter vers d'autres plates-formes (Motif, GTK+, Mac OS...)

Pour la petite histoire, c'est sous la pression de Microsoft, détenteur de la marque « Windows » au Royaume-Uni, que l'équipe de wxWindows a décidé le changement de nom en février 2004, pour se rebaptiser « wxWidgets ».

La bibliothèque wxWidgets est distribuée selon les termes de 2 types de licences : la GNU LGPL version 2 et la wxWindows Library License version 3 qui en est une légère modification autorisant la distribution sous licence propriétaire de programmes sous format binaire intégrant la bibliothèque wxWidgets. Ceci afin de satisfaire aux contraintes de licences à la fois pour les développements sous GPL et pour ceux sous licence propriétaire.

La communauté wxWidgets est nombreuse et très active. Le site web de wxWidgets[1] est une source inépuisable de ressources et d'informations sur le sujet. On peut notamment y trouver des listes d'applications basées sur wxWidgets, d'outils d'aide au développement (environnements de développement intégrés -- EDI ou IDE --, outils de développement rapide d'applications -- RAD --, éditeurs de dialogues, etc).

Parmi ceux-ci, on peut citer les suivants, également utilisables pour wxPerl et wxPython :

Quelques Éditeurs de dialogues :

Avantages de wxWidgets par rapport à ses concurrents

Vous trouverez en [10] l'adresse d'un site web qui présente une étude comparative très intéressante des différents toolkits de GUI sous Unix/Linux.

Multi plates-formes

Le premier avantage majeur de wxWidgets par rapport à ses concurrents est son support multi plates-formes natif. wxWidgets version 2 fonctionne sous :

[Figure 1]

Une boîte à outil complète

Les bibliothèques graphiques modernes ne se limitent plus au support des simples classes de fenêtres, de barres de menus et autres boutons ou cases à cocher. wxPerl n'échape pas à la règle et intègre nativement le support de fonctionalités non-graphiques :

Multi-langages

Comme GTK, Qt et autres Tk WxPerl bénéficie de couches d'adaptation (« wrapper ») en anglais qui facilitent le support de divers langages de programmation. Les différentes couches disponibles n'offrent pas toutes un portage complet de toutes les classes wxWidgets. Certaines en offrent plus que d'autres selon le degré d'avancement des travaux de leur équipe d'intégration.

La liste des langages concernés est aujourd'hui la suivante :

Comme expliqué plus haut, wxPerl est le wrapper de wxWidgets pour le langage Perl. wxPerl dispose de son propre site web hébergé chez SourceForge.net[2].

La première version de wxPerl a vu le jour en novembre 2000, et le site web en juillet 2001, qui vit la publication de wxPerl 0.07.

Le développeur principal de wxPerl s'appelle Mattia Barbon. Il a 26 ans, est italien et programmeur Java de profession. Citons également dans le désordre : Graciliano M. P., Simon Flack, Marcus Friedlaender, Crazyinsomniac.

Toutes les classes wxWidgets les plus importantes sont supportées par l'API wxPerl. Certaines choses devraient pouvoir être améliorées ou complétées, mais tout est là.

L'installation sous Unix/Linux

Installation RPM

Sous Mandrake, l'installation de wxWidgets se réduit à exécuter la commande

    # urpmi wxGTK-devel libwxgtk2.4-devel

qui devrait installer tout ce dont vous aurez besoin, grâce au jeu des dépendances. Vous pouvez aussi ajouter libwxgtkgl2.4 pour intégrer OpenGL dans vos applications. Il vous suffira ensuite d'installer le paquetage perl-Wx-0.21-1_wxgtk2.4.2.i386.rpm que vous trouverez sur le site web de wxPerl.

La Debian stable (woody) en est restée pour l'instant à la version 2.2.9.2 de wxWidgets. La testing (sarge) et l'unstable (sid) intègrent les deux versions proposées sur le site web de wxWidgets, à savoir 2.4.2.6 (stable release) et 2.5.3.2 (development snapshot). Utilisez apt-get install libwxgtk2.4 libwxgtk2.4-dev libwxgtk2.4-contrib libwxgtk2.4-contrib-dev pour installer tous les paquetages nécessaires, dont les fichiers d'en-tête et les bibliothèques sont indispensables pour la suite.

Enfin apt-get install wxwin2.4-doc pour la doc. Vous devrez néanmoins finir l'installation en compilant wxperl à partir des sources, car il n'y a pas de paquetage Debian pour wxPerl à ce jour.

Installation à partir des sources

A partir de la page « Download » du site web de wxPerl, téléchargez l'archive CPAN contenant les sources Wx-0.21.tar.gz. A noter que Matia Barbon a préparé une archive spéciale pour la compilation sous Debian. Le reste est classique :

    perl Makefile.PL
    make
    make test
    make install

L'installation sous Windows

Bien que cet article soit destiné à paraître dans Linux Magazine France, il me paraît judicieux de faire un rapide aparté à l'attention de nos amis Windowsiens, qui souhaiteront profiter des exceptionnelles capacités multi plate-formes de wxPerl.

Pour utiliser wxPerl sous MS Windows, il faut préalablement télécharger et installer ActivePerl[4], qui est la distribution de Perl pour Windows, développé et maintenu par la société ActiveState.

Préférez la dernière version 5.8.n (5.8.6 à la date de rédaction de cet article), qui améliore sensiblement certains aspects de Perl par rapport aux versions 5.6, notamment le multi-threading. L'outil PPM livré avec ActivePerl vous permetra de gérer l'installation, la suppression, et la gestion des paquetages Perl sous Windows.

Téléchargez la dernière version de l'archive de wxPerl pour votre version d'ActivePerl et dézippez-la dans un répertoire temporaire.

Wx-0.21.ppd est le fichier de paramètres du paquetage, qui va être lu et traité par PPM. Le fichier .tar.gz contient le paquetage proprement dit, avec tous les fichiers qui vont être installés sous l'arborescence de C:\Perl\site\lib\...

Ouvrez une fenêtre de commande DOS et lancez l'exécutable ppm pour entrer sous PPM en mode interactif. Ajoutez un « repository » en pointant vers votre répertoire temporaire :

    ppm> rep add "Mon Rep sur C:" C:\Temp

Et lancez l'install avec :

    ppm> install Wx-0.21.ppd

La commande help vous guidera si vous êtes perdus.

Vérifiez que le paquetage est bien installé en tapant sous DOS :

    C:\Perl\bin> perl -e 'use Wx'

Pour lancer directement un script wxPerl sans ouvrir de console, il ne vous reste plus qu'à associer l'extension .wpl par exemple avec le binaire C:\Perl\bin\wperl.exe qui est une version modifiée de l'interpréteur à usage dédié aux application graphiques. Il ne vous reste plus qu'à nommer vos scripts wxPerl avec l'extension .wpl.

La documentation

La bibliothèque wxPerl est livrée avec assez peu de documentation. Vous trouverez sur le site web une petite doc sous l'appellation wxPerl Manual, mais qui s'avère vraiment sommaire. A contrario, les exemples et démos livrés dans l'archive de wxPerl, ainsi que les tutoriels accessibles sur le site web seront très appréciés du novice.

En revanche, la documentation de wxWidgets est particulièrement abondante, exhaustive, précise, concise et indexée dans tous les sens. Le seul reproche qu'on puisse lui faire est qu'elle soit en anglais. La partie vraiment utile de la doc est la "wxWidgets Reference" que vous pourrez consulter en HTML, en format WinHelp ou wxHTML Help, ou en PDF.

Foncez directement sur « Alphabetical class reference » ou sur Classes by categories pour découvrir tous les détails des widgets (contraction de Window gadget, élément de fenêtre) qui vous intéressent. Faites un détour sur « Functions » pour en apprendre plus sur les fonctions globales appelables de n'importe où dans le code. Exemple wxMessageBox que nous verrons dans le hello world inévitable qui vous attend plus loin. Enfin, installez-vous confortablement avant d'attaquer la lecture de « Topic overviews » qui se lit comme un roman (enfin, presque...) et qui vous apportera une vue plus ciblée de telle ou telle partie du « framework ». Par exemple, « Interprocess communication overview » décrit à fond l'utilisation des classes wxSocket et apparentées, et comment les mettre en œuvre pour écrire des applications client-serveur réseau.

Bien qu'elle soit rédigée dans un contexte C++, elle est cependant complètement utilisable par les développeurs Perliens et Pythoniens. En effet, à part quelques exceptions qui sont systématiquement signalées, les noms et prototypes des méthodes sont les mêmes en C++, Perl et Python, de même que les valeurs retournées.

Pour la classe C++ wxWindow, la doc nous renseigne ainsi sur la méthode GetParent :

    wxWindow::GetParent
    virtual wxWindow* GetParent() const
    Returns the parent of the window, or NULL if there is no parent.

La 1ère ligne rappelle le nom de la classe et de la méthode. La 2ème ligne spécifie le prototype de la méthode. Cette dernière ne prend aucun argument et retourne un objet de type wxWindow* (pointeur vers un wxWindow). Le reste donne quelques détails.

La classe (package) correspondante sous Perl aura pour nom Wx::Window. La méthode correspondante sera Wx::Window->GetParent, ne prend aucun argument non plus et retourne une référence vers un objet de classe Wx::Window, ou undef s'il n'existe pas de parent.

Autre exemple, toujours pour la classe wxWindow, mais cette fois pour la méthode GetClientSize :

    wxWindow::GetClientSize
    virtual void GetClientSize(int* width, int* height) const
    wxPerl note: In wxPerl this method takes no parameter and returns a 2-element
            list ( width, height ).
    virtual wxSize GetClientSize() const

Cette méthode C++ existe sous deux formes. La première prend deux arguments de type int*, ne retourne rien, et modifie le contenu des deux variables passées en argument avec les valeurs de largeur et hauteur de la fenêtre. La deuxième ne prend pas d'argument et retourne un objet de type wxSize. Une note spécifique décrit le comportement de la méthode sous wxPerl, pas d'argument et retourne une liste à deux éléments.

Parfois, certaines méthodes C++ n'ont pas été emballées (« wrappées ») pour wxPerl. Dans ce cas, une note le spécifie explicitement.

Hello World !

Un script contenant le minimum vital pour faire tourner une application wxPerl tiendrait en une dizaine de lignes, mais ne présenterait pas un grand intérêt pédagogique. Aussi, je préfère vous en montrer un peu plus, quitte à aborder un peu tôt certains aspects non-triviaux sur lesquels sera apporté plus loin l'éclairage qui convient.

Un bout de code valant souvent mieux qu'un long discours, rentrons dans le vif du sujet sans plus attendre.

[Figure 2]

    #!/usr/bin/perl
    use strict;
    use Wx;    # indispensable pour faire du wxPerl.

    # tout programme wxPerl doit déclarer une classe "application" dérivée de Wx::App
    package MyApp;
    use vars qw(@ISA);
    @ISA = qw(Wx::App);    # déclaration de l'héritage en Perl

    # Pas besoin de déclarer une méthode "new", la version par défaut
    # de la classe parente Wx::App convient amplement.
    # Par contre, la methode OnInit est appelée automatiquement juste
    # après la création de l'objet MyApp, et c'est ici qu'on déroule
    # les actions d'initialisation à effectuer
    sub OnInit {
        my ($this) = shift;

        # Crée un objet MyFrame et passe les arguments suivants au constructeur
        my ($frame) = MyFrame->new(
            undef,              # Objet sans widget parent
            -1,                 # Identifiant déterminé dynamiquement
            'Hello, world!',    # Titre de la fenêtre
            [ 20,  20 ],        # Position sur le bureau
            [ 300, 200 ]        # Taille de la fenêtre à la création
        );

        # le rend visible
        $frame->Show(1);
    }

    # classe dérivée de Wx::Frame. Représente une fenêtre
    package MyFrame;
    use vars qw(@ISA);
    @ISA = qw(Wx::Frame);

    # Importe quelques constantes utilisées par la suite
    use Wx qw( wxDefaultSize wxDefaultPosition wxVERTICAL wxALL
      wxGROW wxALIGN_CENTER wxOK wxICON_INFORMATION );

    # Déclare quelques identifiants uniques affectés aux différents
    # widgets et menus créés plus loin.
    use vars qw($ID_BUTTON);
    $ID_BUTTON = 10000;
    use vars qw($ID_TEXT);
    $ID_TEXT = 10001;
    use vars qw($ID_ABOUT);
    $ID_ABOUT = 10002;
    use vars qw($ID_QUIT);
    $ID_QUIT = 10003;

    # Constructeur de MyFrame
    sub new {

        # Appelle le constructeur de la classe parent et lui transmet
        # les paramètres d'initialisation reçus en arguments
        my ($this) = shift->SUPER::new(@_);

        # Un peu de cosmétique. Affiche l'icone wxWidgets dans le coin
        # gauche de la barre de titre.
        $this->SetIcon( Wx::GetWxPerlIcon() );

        # Le positionnement des widgets enfants de la fenêtre sera délégué
        # à un objet "Sizer" dont c'est le rôle principal.
        my $main_sizer =
          Wx::BoxSizer->new(wxVERTICAL);    # Crée un sizer à empilement vertical
        $this->SetSizer($main_sizer);       # L'affecte à la fenêtre

        # crée un bouton
        my $button = Wx::Button->new(
            $this,                # référence vers le parent. ici l'objet MyFrame
            $ID_BUTTON,           # Identifiant unique du bouton
            "Cliquez-moi",        # Texte affiché sur le bouton
            wxDefaultPosition,    # Position et taille seront ajustés
            wxDefaultSize,        # automatiquement par le sizer
            0
        );                        # Flags optionnels. Inutilisés ici.
                                  # Empile le bouton dans le sizer
        $main_sizer->AddWindow( $button, 0, wxGROW | wxALL, 5 );

        # Crée un label dont le texte est vide pour l'instant.
        # Sa taille est fixée à 200 pixels de large sur 20 de haut
        my $label =
          Wx::StaticText->new( $this, $ID_TEXT, "", wxDefaultPosition, [ 200, 20 ],
            0 );
        $main_sizer->AddWindow( $label, 0, wxALIGN_CENTER | wxALL, 5 );

        # Sauve une référence sur le widget label
        $this->{label} = $label;

        # Création d'une barre de menus
        my $menubar = Wx::MenuBar->new();
        $this->SetMenuBar($menubar);    # On l'affecte à la fenêtre
        my $menufile = Wx::Menu->new(); # Création d'un menu
        $menubar->Append( $menufile, "File" )
          ;                             # On l'attache à la barre et le nomme "File"
                                        # Ajout d'éléments de menu
        $menufile->Append(
            $ID_ABOUT,                  # Identifiant unique
            "About\tCtrl-h"
            ,    # Texte affiché et raccourcis clavier, séparés par une tabulation
            "A propos"
        );       # Texte affiché dans la statusbar lors d'un survol de la souris
        $menufile->Append( $ID_QUIT, "Quit\tCtrl-q", "Quitte l'application" );

        # Création d'une barre d'état et mise à jour du texte affiché
        $this->CreateStatusBar(1);
        $this->SetStatusText( "Welcome to wxPerl!", 0 );

        # Importe la gestion des événements qui nous intéressent
        use Wx::Event qw(EVT_MENU EVT_BUTTON);

        # Déclare des gestionnaires d'événements appelés quand l'utilisateur
        # clique sur le bouton ou sélectionne un élément de menu
        EVT_BUTTON( $this, $ID_BUTTON, \&OnButton );
        EVT_MENU( $this, $ID_ABOUT, \&OnAbout );
        EVT_MENU( $this, $ID_QUIT,  \&OnQuit );

        # Retourne une référence à l'objet MyFrame créé
        return $this;
    }

    # Méthode appelée par un click sur le bouton. On parle aussi de "callback"
    sub OnButton {
        # Récupère une référence à la fenêtre qui a reçu l'événement. Ici
        # l'objet MyFrame, et une autre à l'objet événement lui-même (inutilisé
        # ici).
        my ( $this, $event ) = @_;

        # Accède à la référence du label précédemment sauvegardée et met son
        # texte à jour via la méthode SetLabel
        $this->{label}->SetLabel("Hello World !");
    }

    # Méthode appelée par l'élément de menu "About"
    sub OnAbout {
        my ( $this, $event ) = @_;

        # Crée une fenêtre de dialogue "prête à l'emploi"
        # dédiée à l'affichage d'informations
        Wx::MessageBox(
            "Welcome to HelloWorld 1.0\n(C)opyright Christophe Massaloux"
            ,                        # texte du message
            "About HelloWorld",      # titre du dialogue
            wxOK |
              wxICON_INFORMATION,    # Affiche une icône "ampoule" et un bouton "OK"
            $this
        );                           # Objet parent du dialogue
    }

    # Méthode appelée par l'élément de menu "Quit"
    sub OnQuit {
        my ( $this, $event ) = @_;

        # Appelle la méthode "Destroy" de l'objet MyFrame.
        # Comme MyFrame ne déclare pas cette méthode, c'est la méthode
        # par défaut de la classe parente qui sera appelée et qui va
        # provoquer la fermeture de la fenêtre, la sortie de $app->MainLoop()
        # et la fin du programme.
        $this->Destroy();
    }

    # Programme principal
    package main;

    # Crée une instance d'objet application MyApp
    my ($app) = MyApp->new();

    # Lance la boucle d'attente des événements
    # L'utilisateur peut faire sortie le programme de Mainloop soit en
    # sélectionnant le menu "File/Quit", soit en fermant la fenêtre via le menu
    # du window manager (menu système sous Win32), accessible en cliquant sur
    # l'icône de fenêtre, ou par un raccourci clavier (en général Alt-F4)
    $app->MainLoop();

Maintenant que vous avez une meilleure idée de ce qu'est un programme wxPerl, nous pouvons en détailler la structure pas à pas.

Structure traditionnelle d'un code wxPerl

Une application en wxPerl se compose au minimum des packages suivants :

Penchons-nous sur ces différents packages.

Le package main

Le package main est le point d'entrée du programme. Traditionnellement localisé à la fin du fichier .pl dans le cas d'un script mono-fichier, de telle sorte que l'interpréteur Perl ait déjà lu et compilé le reste du code au moment de l'appel à MyApp->new. Il est bien sûr possible de l'isoler dans son propre fichier .pm pour des applications plus conséquentes.

Voyons ce à quoi cela peut ressembler :

    # Exemple de package "main" minimal
    package main;
    my ($app) =
      MonAppli->new();  # création de l'objet application et de tous ses enfants
    $app->MainLoop();   # début d'attente & traitement des événements

    # Exemple de package "main" plus complet
    package main;

    # import d'un module métier
    use ObjetMetier qw( fonction_metier );

    use MLDBM 'DB_File';    # autre exemple, import d'un module Core de Perl
    tie( my %CONFDB, 'MLDBM', 'config.db' )
      or die "Erreur de tie: $!";

    # appel à une fonction métier d'initialisation
    &ObjetMetier::fonction_metier();
    my ($app) =
      MonAppli->new();  # création de l'objet application et de tous ses enfants
    $app->MainLoop();   # début d'attente & traitement des événements
    untie %CONFDB;      # action post-MainLoop de nettoyage avant clôture

Voyons tout ça plus en détails :

Le package Application

Le package Application décrit une classe dérivée de Wx::App, et qui représente l'application au sens wxPerl du terme. Souvent on l'appelle MyApp mais n'importe quel nom peut faire l'affaire.

Voici un exemple de code d'un package Application :

    package MyApp;

    use strict;
    use vars qw(@ISA);
    @ISA = qw(Wx::App);    # déclaration de l'héritage

    sub OnInit {
        my $this = shift;
        Wx::InitAllImageHandlers();    # exemple de fonction d'init globale
        my ($dialog) =
          Mon_Dialogue->new(           # création d'une fenêtre de type "Dialog"
            undef,                     # fenêtre principale de l'application,
                                       # donc pas de parent
            -1,                        # ID attribué dynamiquement
            "Joli dialogue",           # Titre de la fenêtre
            [ 20,  20 ],               # Dimension & position
            [ 500, 340 ]
          );
        $dialog->Show(1);              # affichage de la fenêtre à l'écran
        1;                             # doit retourner "true" pour continuer,
                                       # ou "false" pour quitter l'appli
    }

L'objet instancié à partir de ce package est l'objet parent de tous les autres.

On ne déclare pas de méthode constructeur new pour ce package car la méthode par défaut implémentée dans la classe mère Wx::App est suffisante et ne nécessite pas de surcharge.

Au contraire, le code d'initialisation de l'objet devra être localisé dans la méthode OnInit() qui est appelée automatiquement juste après la création de l'objet Application.

Un certain nombre de directives globales peuvent être appelées à ce niveau pour initialiser certaines choses, ou pour contrôler le comportement de MainLoop(). Par exemple, Wx::InitAllImageHandlers() initialise les gestionnaires internes de formats de fichier images, permettant par la suite d'utiliser des fichiers .jpg ou .png pour ajouter des icônes ou des bitmaps.

C'est dans OnInit() qu'on va créer et afficher la fenêtre principale, qui pourra être de type Frame (dérivation de Wx::Frame) ou de type Dialog (dérivation de Wx::Dialog).

Une Frame est une fenêtre dont la taille et la position peuvent être modifiées par l'utilisateur (habituellement car il est possible de spécifier une taille et une position figées). Une Frame possède (habituellement aussi) des bords épais et une barre de titre, et peut contenir une barre de menus (menubar), une barre d'outils (toolbar) et une barre d'état (statusbar).

Un Dialog est une fenêtre avec une barre de titre, et éventuellement un menu système, mais pas de barre de menus, de barre d'outils ni de barre d'état. Un Dialog peut être déplacé sur l'écran mais pas redimensionné. Il peut contenir toute sorte de widgets de contrôle (boutons, sliders, zone de texte, etc.), et est généralement utilisé pour permettre à l'utilisateur de faire des choix, d'interagir avec l'application. De plus, il offre par défaut une navigation entre les contrôles par appuis successifs sur la touche Tabulation.

Il y a deux sortes de Dialogs, modal ou non-modal (modeless en anglais). Un Dialog de type modal bloque l'exécution du programme principal et interdit les actions sur les autres fenêtres de l'application jusqu'à ce que l'utilisateur valide une action et referme le Dialog. Un Dialog non-modal se comporte plus comme une Frame, autorisant le programme principal à continuer son exécution, et l'utilisateur à agir sur les autres fenêtres. Un Dialog est appelé en mode modal avec la méthode ShowModal(), sinon c'est la méthode Show() qui est utilisée, et qui provoque son affichage en mode non-modal, comme pour une Frame.

Notons enfin l'existence et l'utilité des Panels (dérivation de Wx::Panel), pas indispensables en théorie, mais qui apportent aux Frames les fonctionnalités des Dialog. Ils permettent de les considérer comme des groupes de contrôles, plus faciles à réutiliser ailleurs, dans une autre fenêtre ou même une autre application.

Les packages Frame

Les packages Frame ou Dialog décrivent une classe dérivée de Wx::Frame ou de Wx::Dialog respectivement, et représente les fenêtres de l'application. C'est en général, la fenêtre principale qui s'affiche la première, mais pas toujours car on peut commencer par afficher un « splashscreen » pendant quelques secondes par exemple.

La fenêtre principale est l'objet parent des autres fenêtres et widgets, et sa fermeture ou sa destruction provoque la sortie de MainLoop() et la destruction en cascade de tous ses widgets enfants.

Typiquement, son code est scindé en deux parties :

Commençons par la partie statique du package, découpé comme suit :

L'import des constantes

On importe une sélection de constantes diverses et variées exportées par le module Wx, qui seront utilisées plus loin, lors de la création des widgets. Par exemple :

    use Wx qw( wxDefaultSize wxDefaultPosition wxID_OK wxID_CANCEL wxID_YES );
    use Wx qw( wxVERTICAL wxHORIZONTAL wxALL wxLEFT
      wxRIGHT wxTOP wxBOTTOM wxCENTRE wxGROW );

Le package Wx exporte des constantes pour tout un tas de choses, et il serait trop long de tout décrire ici. Le fichier Wx_Exp.pm de la distribution de wxPerl en contient la liste exhaustive qui en dénombre plus de 2000, regroupés par thème et chaque groupe identifié par une balise que l'on peut spécifier lors d'un use. Par exemple :

    use Wx qw( :color );

permet d'importer les constantes wxNullColour, wxRED, wxGREEN, wxBLUE, wxBLACK, wxWHITE, wxCYAN, wxLIGHT_GREY.

Vous pouvez aussi décider de tout importer d'un seul coup pour ne pas risquer de subir les désagréments de messages d'erreur lors de la compilation. Gardez cependant à l'esprit que se limiter à un import sélectif des constantes effectivement utilisées permet d'optimiser la consommation en mémoire.

    use Wx qw( :everything );

Énumération des identifiants de widgets

Sous wxWidgets (et donc sous wxPerl par extension), chaque widget se voit attribuer un identifiant numérique unique, qui peut servir à identifier un élément de menu ou à informer un « callback » de l'identité du widget à l'origine d'un événement. Le programmeur peut décider d'attribuer lui-même cet identifiant, auquel cas il devra s'assurer de son unicité, ou bien laisser wxWidgets en attribuer un automatiquement en spécifiant la valeur -1 à la place. wxWidgets utilise déjà certaines valeurs entières qui doivent lui rester réservées. Aussi le programmeur qui souhaite spécifier ses propres valeurs d'identifiants devra prendre garde à les choisir en dehors de cet intervalle réservé, entre wxID_LOWEST et wxID_HIGHEST qui sont actuellement fixées à 4999 et 5999. Par exemple :

    ( $ID_OPEN, $ID_SAVE, $ID_PRINT, $ID_EXIT, $ID_ABOUT, $ID_BOUTON_TEST, ) =
      ( wxID_HIGHEST + 1 .. wxID_HIGHEST + 100 );

De la sorte, quand vous voulez ajouter un nouveau bouton à votre interface, il suffit de lui choisir un petit nom (par exemple ID_MONBOUTON) et d'ajouter un $ID_MONBOUTON en fin de liste.

Le constructeur de Frame

Le constructeur de l'objet Frame commence généralement par un appel au constructeur de la classe parente, à qui on fait suivre les éventuels arguments transmis (titre de la fenêtre, taille et position).

On trouve ensuite la génération de tous les widgets qui vont peupler la fenêtre, dans un ordre qui permet d'assurer une hiérarchie cohérente au niveau de l'imbrication des widgets conteneurs (les Sizers que nous verrons en détail dans l'article suivant), et de l'agencement des widgets traditionnels (boutons, cases à cocher, zones de texte, etc.). Lors de la création de chacun des widgets, on passera en argument à new, l'identifiant unique $ID_MONBOUTON qui lui a été précédemment attribué.

La création de la barre de menu et de la barre d'outils (« toolbar ») (que verrons aussi une prochaine fois) y ont également leur place, ainsi que la barre d'état qui ne nécessite qu'une ou deux lignes de code :

    # Création d'un menu
    my $help = Wx::Menu->new;                       # Création d'un menu
    $help->Append( $ID_ABOUT, '&About', 'About' );  # Insertion dans ce menu d'un élément
                                                    #  "About" avec l'ID $ID_ABOUT
    my $menu = Wx::MenuBar->new;                    # Création d'une barre de menu
    $menu->Append( $help, "&Help" );                # Insertion du menu dans la barre
                                                    #   et affectation du libellé "Help"
    $this->SetMenuBar( $menu );                     # Affectation de la barre à la fenêtre

    $this->CreateStatusBar( 2 );             # Crée une barre d'état avec 2 compartiments
    $this->SetStatusText("Hello World", 0);  # Écrit quelques mots dans le 1er compartiment

On peut également spécifier une icône qui apparaîtra dans le coin gauche de la fenêtre, ainsi que dans la barre des tâches quand l'application est réduite. La fonction Wx::GetWxPerlIcon qui retourne l'icône par défaut de wxWidgets, et qu'on peut utiliser comme suit :

    $this->SetIcon( Wx::GetWxPerlIcon() );   # Utilise l'icône de wxPerl

Les membres d'instance véhiculent des informations propres à l'application, qu'il est généralement plus simple de stocker dans l'instance de la Frame principale. Fidèles aux bonnes habitudes de la POO en Perl, nous les mémorisons en tant que clefs du hachage de l'objet Frame. Ainsi :

    # exemple pour une application réseau
    $this->{CONNECT}    = 0;
    $this->{NB_CLIENTS} = 0;

On peut également utiliser cette méthode pour accéder facilement à certains objets enfants de la Frame :

    $this->{STATUSBAR} = $this->GetStatusBar();
    $this->{LABEL} = Wx::StaticText->new( $this, $ID_TEXT, "", wxDefaultPosition, [200,20], 0 );

À noter que la classe Wx::Window qui est une superclasse de tous les Widgets, Frames et autres Dialogs implémente la méthode FindWindow($ID) qui permet de retrouver dans une Frame un widget enfant à partir de son ID. C'est cette méthode qui est employée pour coder les fonctions « d'accesseurs » (mauvaise traduction du terme technique anglais accessor), qui sont de simples fonctions Get_ceci() ou Set_cela() permettant d'accéder simplement aux widgets intéressants.

Voici ci-dessous un exemple mixant les deux façons de faire :

    package MyFrame;
    @ISA = qw(Wx::Frame);
    ($ID_BUTTON_OK) = ( wxID_HIGHEST + 1 .. wxID_HIGHEST + 100 );

    sub new {
        my ($class) = shift;
        my ($this)  = $class->SUPER::new(@_);
        $this->{BUTTONOK} =
          Wx::Button->new( $this, $ID_BUTTON_OK, "OK", wxDefaultPosition,
            wxDefaultSize, 0 );
        $this->AddWindow( $this->{BUTTONOK}, 1, wxALIGN_CENTER | wxALL, 15 );
    }

    # Fonction "accesseur" pour accéder au bouton OK
    sub GetButtonOK { $_[0]->FindWindow($ID_BUTTON_OK); }

    # Fonction quelconque qui utilise l'accesseur et/ou le membre d'instance
    sub FaireChose {
        my $this     = shift;
        my $buttonOK = $this->GetButtonOK;    # référence par l'accesseur
       #my $buttonOK = $this->{BUTTONOK};     # référence par le membre d'instance

        $buttonOK->SetDefault;    # Spécifie que le bouton est le widget par
                                  # défaut de la frame i.e. un appui sur
                                  # Enter équivaut à cliquer sur le bouton
    }

Les outils IDE/RAD optent en général pour l'utilisation des fonctions d'accesseur.

Enfin, on termine le constructeur par la mise en place des liaisons entre les événements intéressants et les callbacks (les gestionnaires d'événements) dont le code apparaît après le constructeur. Les événements qualifiés d'intéressants sont ceux que la Frame devra intercepter, afin de permettre à l'application de leur répondre par des actions appropriées. Les callbacks sont de simples méthodes de l'objet Frame qui attendent comme premier argument, une référence à l'objet Frame (comme toutes les autres méthodes d'ailleurs), et une référence à un objet événement en second argument. Les liaisons entre événement et callback sont déclarées grâce à l'usage de fonctions dédiées du package Wx::Event, spécialisées par type d'événement. Exemple :

    use Wx::Event qw(EVT_BUTTON EVT_RADIOBOX EVT_SPINCTRL EVT_TIMER EVT_CLOSE);
    EVT_BUTTON  (  $this, $main::ID_BT_START,    \&OnBtStart );
    EVT_RADIOBOX(  $this, $main::ID_RD_AUTOPOLL, \&OnRdAutoPoll );
    EVT_SPINCTRL(  $this, $main::ID_SP_SYMB,     \&OnSpSymb );
    EVT_TIMER   (  $this, 1,                     \&OnTimer );
    EVT_CLOSE   (  $this,                        \&OnClose );

Comme on peut le voir, le nombre d'arguments à fournir dépend du type d'événement. Le premier argument est toujours une référence à la Frame qui doit intercepter l'événement. Pour les événements émis par des widgets, le second argument est l'identifiant attribué au widget lors de sa création. Le dernier argument est une référence à la fonction callback. Les événements et les fonctions de liaison événement-callback feront l'objet d'une description plus détaillée dans le prochain article.

Comme tout bon constructeur qui se respecte, sa dernière ligne renvoie une référence à l'objet Frame créé à l'aide d'un simple return $this.

L'implémentation des callbacks

Passons maintenant à la partie dynamique. Une belle IHM c'est bien, mais c'est encore mieux quand elle sert à quelque chose. La partie dynamique rassemble le code qui gère le comportement de l'application. Il s'agit d'une collection de méthodes de l'objet concerné (ici, l'objet Frame). Chacune d'entre elles va répondre spécifiquement à un certain stimulus attendu (événement) parmi la liste de ceux pour lesquels une fonction de liaison événement-callback a bien été déclarée.

En d'autres termes, si le programmeur n'a pas déclaré de fonction telle que :

    Wx::Event::EVT_BUTTON ( $this, $main::ID_MONBOUTON, \&OnMonBouton );

Un clic de souris sur le bouton "MonBouton" déclenchera bien un événement correspondant, celui-ci sera bien intercepté par la Frame, mais c'est le comportement par défaut qui sera appelé, et rien ne se passera.

Le programmeur doit implémenter une fonction et une seule pour chacune des liaisons "événement-callback" déclarée dans le constructeur. En général, selon une convention établie depuis l'origine de la POO, les callbacks ont des noms du genre "OnCeci" ou "OnCela". Ils sous-entendent chez les anglophones qu'ils sont appelés lors de l'occurrence de tel événement "Ceci" ou de tel autre événement "Cela".

Ces fonctions reçoivent au moins deux arguments, dont le premier est une référence à l'objet gestionnaire de l'événement (celui qui l'a intercepté), et le deuxième est une référence à un objet wx::Event qui représente l'événement proprement dit, et qui véhicule un certain nombre d'informations sur le type d'événement, le widget qui l'a émis, etc. Il est parfois nécessaire de récupérer ces informations, comme par exemple, dans le cas d'un événement clavier dont le programmeur souhaite extraire le code ASCII de la touche pressée via la méthode Wx::Event->GetKeyCode.

Quelques exemples de callbacks :

  # Dans le constructeur
  # EVT_CLOSE est déclenché par la fermeture de la fenêtre par le window manager
    EVT_CLOSE( $this, \&OnCloseWindow );

    # callback &OnCloseWindow
    sub OnCloseWindow {
        my ( $this, $event ) = @_;

        # Vous pouvez mettre ici le code de nettoyage de l'appli avant fermeture
        &Ma_fonction_de_nettoyage();

        # Destruction "propre" de la fenêtre. Les widgets enfants sont détruits
        # récursivement et wxWidgets attend que la file d'événements soit vide
        # avant de commencer la destruction, afin d'éviter que des événements
        # ne soient envoyés à des fenêtres inexistantes.
        $this->Destroy;
    }

À noter que l'appel à la méthode Close ne détruit pas la fenêtre, mais génère un événement EVT_CLOSE pour cette fenêtre.

    # Toujours dans le constructeur.
    EVT_MENU( $this, $ID_ABOUT, \&OnAbout );

    # callback &OnAbout
    sub OnAbout {
        my( $this, $event ) = @_;

        # Affiche une petite fenêtre "pop-up" d'information
        Wx::MessageBox(
          "==  Démo  ==\nRelease Version 0.01\n(c)2003 C.Massaloux",  # Texte informatif
          "==  Démo ==",                                              # Titre de la fenêtre
          wxOK|wxICON_INFORMATION       # Affichage d'un bouton OK et d'une icône d'information
          $this );                      # Fenêtre parente
    }

Il est souvent nécessaire lors des traitements de callbacks, d'accéder à un widget enfant de la Frame, qui n'est pas le même que celui qui a généré l'événement déclencheur. C'est à ce moment-là que les fonctions d'accesseur prennent tout leur sens. Exemple : Un bouton "Clear Text" sert à effacer le contenu d'une zone de texte :

    Wx::Button->new( $this, $ID_BT_TEXTCLEAR, "Clear Text" );    # dans le constructeur
    Wx::Event::EVT_BUTTON( $this, $main::ID_BT_TEXTCLEAR, \&OnTextClear );  # dans le constructeur

    sub GetTextCtrl {    $_[0]->FindWindow( $ID_TEXTCTRL );}     # Fonction "accesseur" pour accéder à la zone de texte

    # callback &OnTextClear
    sub OnTextClear {
        my( $this, $event ) = @_;
        $this->GetTextCtrl->Clear;        # Cet accesseur efface le contenu de la zone de texte
    }

Les accesseurs et autres fonctions utilitaires

J'ai déjà expliqué plus haut ce que sont les fonctions d'accesseurs. J'ajouterai simplement qu'il est plus simple de les regrouper toutes au même endroit, soit juste après le constructeur et avant les callbacks, soit en fin de package, après les callbacks.

Enfin, vous ajouterez toutes les autres fonctions/méthodes utiles pour l'application, mais qui ne sont pas stricto sensu des callbacks.

Le cas des fonctions qui gèrent le "copier-coller" est un bon exemple : Voici trois callbacks, qui sont traditionnellement appelés par les éléments Copy/Cut/Paste du menu Edit :

    sub OnCopy {                # callback OnCopy
        my( $this, $event ) = @_;
        my $item = $this->{CUR_ITEM};        # Cible de la copie
        $this->{CLIPBOARD} =  $item;        # Mémorise une référence à l'item copié
    }

    sub OnCut {                # callback OnCut
        my( $this, $event ) = @_;
        my $item = $this->{CUR_ITEM};        # Cible de la suppression
        $this->{CLIPBOARD} =  $item;        # Mémorise une référence à l'item copié
        $this->DelItem($item);            # Suppression de l'item cible via une fonction utilitaire
    }

    sub OnPaste {                # callback OnPaste
        my( $this, $event ) = @_;
        my $item = $this->{CUR_ITEM};        # Cible du collage
        my $new_item = $this->{CLIPBOARD};    # Récupération de l'item à coller
          $this->InsItem($item, $new_item);    # Insertion via une fonction utilitaire
    }

    sub DelItem {                # Fonction utilitaire de suppression d'un item
        my ( $this, $item ) = @_;
        # Faire quelque chose pour supprimer l'item au niveau des couches métier
        #.../...
    }

    sub InsItem {                # Fonction utilitaire d'insertion d'un item
        my ( $this, $item, $new_item ) = @_;
        # Faire quelque chose au niveau des couches métier
        # pour insérer le nouvel item "sur" l'item cible
        # .../...
    }

On voit dans cet exemple que le traitement des événements par les callbacks (OnCut, OnCopy, OnPaste) et le vrai traitement au niveau des couches métier de l'application (DelItem, InsItem) sont séparés. Cela simplifiera d'autant la maintenance du logiciel. Les deux fonctions « utilitaires » DelItem et InsItem n'en sont pas moins de vrais méthodes de l'objet Frame, et sont donc incluses dans le package.

Dans le prochain article sur wxPerl

La prochaine fois, nous verrons les principaux widgets de base (boutons, cases à cocher, liste déroulantes, barre de menus, etc..), nous paserons en revue les Sizers et leurs caractéristiques, nous évoquerons les événements et leur gestion par wxWidgets, et nous finirons par quelques curiosités incontournables : les icônes, les objets Wx::Config, les Wx::Timer, les Wx::Validator.

Auteur

Christophe Massaloux - <cmassaloux@free.fr>

Un grand merci à tous les relecteurs du groupe articles des Mongueurs pour leurs conseils avisés et leurs encouragements appréciés.

Quelques liens sur le web

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