Article publié dans Linux Magazine 70, mars 2005.
Copyright © 2005 - Christophe Massaloux.
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.
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.
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 :
l'éditeur wxDesigner[5] est disponible sur Win32, Unix, Mac OS X. Produit du code C++, Perl, Python et C#.
l'éditeur wxGlade[6], disponible sur Win32, Unix. Produit du code C++, Perl et Python.
l'EDI wxWorkshop[7] est disponible sur Unix, Win32. Produit du code C++, Python et Perl.
VisualWx[8], disponible sur Win32. Produit du code C++, Perl, Python et Lua.
Anjuta[9], disponible sur Unix. Produit du code C++, Java, Perl, Pascal, etc.
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.
Le premier avantage majeur de wxWidgets par rapport à ses concurrents est son support multi plates-formes natif. wxWidgets version 2 fonctionne sous :
Unix/Linux avec Motif et GTK+ et également directement sur la couche X11 ;
MS Windows toutes versions depuis la 3.1 jusqu'à XP, sauf pour CE qui est en cours de portage ;
MacOS Classic (8.x/9.x) et Mac OS X pour la version de développement.
Le portage vers OS/2 est en cours;
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 :
Communications réseaux. Tout ce qu'il vous faut pour développer facilement des serveurs et des clients réseaux TCP ou UDP.
Gestionnaires d'impression. Permet de gérer de façon identique l'accès aux imprimantes Windows pour les applications Windows et aux imprimantes PostScript pour les applications Unix/Linux.
Outils de manipulation d'images, avec le support des principaux formats d'images, et d'outils de dessin tels que des palettes de couleurs, des pinceaux et brosses, etc.. À noter également, l'interface avec le monde OpenGL.
Affichage de documents HTML & XML. Outils de gestion de l'aide en ligne et de génération de Wizards d'accompagnement.
En vrac : Gestion du presse-papiers (Copier/Coller) et Drag'n Drop. Interface vers des bases de données. Le MultiThreading, et bien d'autres choses encore.
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 :
Python avec wxPython[3] qui est sans doute le portage de wxWidgets le plus complet. Python est un langage de script interprété orienté objet assez populaire au sein de la communauté Open Source. Son caractère orienté objet s'accorde bien avec wxWidgets qui est écrit en C++ au départ, donc orienté objet également.
Perl avec wxPerl[2], objet de cet article, qui est décrit en détail dans ce qui suit.
L'inteface wxPerl utilise abondamment les aspects objets de la programmation en
Perl. À cet égard, le lecteur est invité à (re-)parcourir la
documentation « objet » de Perl (perldoc perlobj
, perldoc perlboot
,
perldoc perltoot
, perldoc perltooc
et perldoc perlbot
),
ainsi que l'article de Sylvain Lhullier paru dans LM n°47 paru en février 2003,
consacré à ce sujet.
Basic avec wxBasic. C'est l'un des ancêtres des langages de programmation interprétés sur lequel nombre d'entre nous ont fait leurs premières armes.
Ruby avec wxRuby. De même que Python, Ruby est un langage interprété orienté objet. Il est cependant plus proche du concept pur de programmation orientée objet, s'inspirant en cela de Smalltalk ou Eiffel.
Eiffel avec wxEiffel. Langage strictement orienté objet, Eiffel est un langage compilé.
La bibliothèque wx4j est un portage de wxWidgets sous Java
Citons également quelques autres curiosités : Lua avec wxLua, Euphoria avec wxEuphoria, Haskell avec wxHaskell, JavaScript avec wxJS.
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à.
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.
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
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 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.
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.
#!/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.
Une application en wxPerl se compose au minimum des packages suivants :
Le package main
, qui contient l'appel à la boucle de traitement des
événements $app->MainLoop
;
Le package Application
, qui contient l'appel au constructeur de la
Frame
principale ;
Le package Frame
, qui décrit toute la structure statique de la
fenêtre principale de l'application, et contient toutes les méthodes de
callback qui décrivent son comportement dynamique ;
Éventuellement, tout autre package de Frame
ou de Dialog
,
décrivant chacun une
fenêtre de dialogue supplémentaire.
Penchons-nous sur ces différents packages.
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 :
Les inclusions (use
) des modules externes nécessaires à l'application,
fonctionnalités métier développées par vous-même ou modules standard du
CPAN ;
Les initialisations diverses qui doivent avoir lieu avant l'ouverture de l'IHM ;
La création d'un objet Application
dérivé de la classe
Wx::App
, qui va entraîner la création en cascade de tout
le reste de l'IHM, fenêtre principale, widgets, dialogues, etc. ;
L'appel à la méthode MainLoop()
sur cet objet va lancer
la boucle d'attente et de traitement des événements. L'occurrence d'un
événement provoque l'exécution du callback qui lui est associé. Il
va dérouler son code, lancer les éventuels appels aux actions métier
de l'application à travers son API, provoquer éventuellement le déclenchement
d'autres événements qui vont s'insérer dans une file d'attente.
Enfin, il rend la main à la boucle principale qui va reprendre son cycle et
vérifier la présence éventuelle d'autres événements à traiter dans la file d'attente.
Et ainsi de suite...
Généralement, on quitte la boucle MainLoop()
en détruisant
la fenêtre principale via la méthode Destroy()
, ce qui détruit
en cascade toutes les fenêtres filles et leurs widgets associés ;
Enfin, les appels postérieurs à MainLoop()
, à des fonctions métier à
exécuter avant clôture de l'application (par exemple pour fermer
proprement une connexion à une base de données).
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 Dialog
s, 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.
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 :
Une partie statique, qui décrit toute la constitution du squelette de l'IHM,
c'est à dire la création et le positionnement des widgets les uns par rapport
aux autres dans la Frame
;
Une partie dynamique, qui rassemble le code des callbacks, et
réalise concrètement le comportement de la Frame
en réponse aux
sollicitations de l'utilisateur.
Commençons par la partie statique du package, découpé comme suit :
L'import des constantes ;
L'énumération des identifiants de widgets ;
Le constructeur de Frame
.
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 );
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.
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
.
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 }
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.
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
.
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.
http://www.activestate.com/Products/Download/Download.plex?id=ActivePerl
http://nic-nac-project.de/~skypher/me/writings/fm-article-toolkits/article.html
Copyright © Les Mongueurs de Perl, 2001-2011
pour le site.
Les auteurs conservent le copyright de leurs articles.