Article publié dans Linux Magazine 71, avril 2005.
Copyright © 2005 - Christophe Massaloux.
Wx::PlValidator
Dialogs
standardsAu menu de ce mois-ci, une présentation des principaux widgets de wxPerl, les validateurs, les menus, les barres d'outils, les barres d'état, la gestion des évènements et la mise en page avec les Sizers. Miam... on va se régaler !
wxWidgets offre toute la panoplie des widgets de base les plus classiques, que
l'on s'attend à trouver dans un framework graphique digne de ce nom. Dans la
terminologie wxWidgets, on les appelle des controls.
La figure 1 montre une Frame
avec les principaux controls.
La syntaxe de leur constructeur correspond pratiquement tout le temps au schéma suivant :
my $control = Wx::package-du-control->new( $parent, $ID, $texte_visible, $position, $taille, $style, $validator, $nom );
$parent
est une référence au widget parent du contrôle, en général une
Frame
ou un Dialog
. $ID
est l'identifiant unique attribué au contrôle.
$texte_visible
est la chaîne de caractères qui apparaîtra sur/dans/à côté du
contrôle, en fonction du type de contrôle.
$style
contient une valeur entière qui décrit le style visuel du contrôle
(bords en relief, bordure simple ou double, etc..). Je vous renvoie à la doc de
la classe wxWindow
et de chacun des widgets concernés pour plus d'info.
$validator
est un objet de type Wx::Validator
qui sert aux échanges de données
entre les couches IHM et les structures de données « métier » comme
expliqué en détail plus loin.
$name
est un nom optionnel attribué au widget, et qui peut être utilisé
par la suite pour le retrouver via la méthode
Wx::Window->FindWindowByName
.
On peut uniquement spécifier le texte affiché sur le widget. Il ne retourne pas de valeur saisie par l'utilisateur :
Wx::StaticText
Wx::StaticBitmap
Il utilisera un objet Wx::Bitmap
qui devra être déclaré par ailleurs.
Wx::StaticBox
On l'utilisera de préférence en conjonction avec Wx::StaticBoxSizer
.
Wx::Button
Et ses variantes Wx::BitmapButton
et Wx::ToggleButton
.
my $label = Wx::StaticText->new( $frame, $ID_LABEL, "Texte du label", wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE ); my $bitmap = Wx::Bitmap->new( "/path/to/images/BitmapsFile.png", wxBITMAP_TYPE_PNG ); my $staticbitmap = Wx::StaticBitmap->new( $frame, $ID_BITMAP, $bitmap, wxDefaultPosition, wxDefaultSize ); my $staticbox = Wx::StaticBox->new( $frame, -1, "Titre de l'encadrement" ); my $staticboxsizer = Wx::StaticBoxSizer->new( $staticbox, wxHORIZONTAL ); # L'ID du widget positionné à -1 sera fixé par wxWidget my $button = Wx::Button->new( $frame, $ID_BUTTON, "Mon Bouton", wxDefaultPosition, wxDefaultSize, 0 );
Pour tous ceux-là, il existe des méthodes SetValue
et GetValue
pour changer/récupérer le contenu affiché/saisi :
Wx::TextCtrl
.
Elles peuvent être multi-lignes, et intègrent par défaut certains comportements traditionnels des éditeurs de texte comme la navigation au sein du texte avec les flèches. De nombreux flags de styles autorisent différentes variantes de ce widget, comme la saisie des mots de passe ou un accès en lecture seule par exemple.
Wx::CheckBox
et Wx::RadioBox
.
Wx::SpinButton
et Wx::SpinCtrl
Wx::Slider
et Wx::Gauge
.
Curseurs et jauges sont orientables horizontalement ou verticalement
grâce au paramètre $style
qui peut prendre les valeurs wxSL_HORIZONTAL
,
wxSL_VERTICAL
pour un curseur et wxGA_HORIZONTAL
, wxGA_VERTICAL
pour les jauges.
my $textentry = Wx::TextCtrl->new( $frame, $ID_TEXTENTRY, "", wxDefaultPosition, [80,-1], wxTE_PROCESS_ENTER ); # La zone de texte aura ici une dimension de 80 pixels de long, et une hauteur # calculée par défaut en fonction de la taille de la police de caractères. my $ckeckbox = Wx::CheckBox->new( $frame, $ID_CHECK, "Check", wxDefaultPosition, wxDefaultSize, 0 ); $checkbox->SetValue(1); # initialise l'état de la boîte. my $var = $checkbox->GetValue(); # récupère l'état de la boîte. my $radiobox = Wx::RadioBox->new( $frame, $ID_RADIO, "Switch", wxDefaultPosition, wxDefaultSize, ["ON","OFF"] , 1, wxRA_SPECIFY_COLS ); my $var = $radiobox->GetSelection(); # récupère le no du bouton enfoncé # La liste des choix apparaît sous la forme d'une liste anonyme, ici C<["ON","OFF"]>. # Les deux paramètres suivants indiquent le nombre de colonnes (ou de lignes # avec wxRA_SPECIFY_ROWS) dans le cas de boîte à 2 dimensions. my $spin = Wx::SpinCtrl->new( $frame, $ID_SPIN, $default, wxDefaultPosition, [100,-1], $style, $min, $max, $initial ); # $min et $max spécifient les valeurs limites acceptées par le contrôle lors de la saisie. # $initial spécifie la valeur positionnée lors de la création du contrôle. # $default ne sert à rien dans ce cas. my $slider = Wx::Slider->new( $frame, $ID_SLIDER, $initial, $min, $max, wxDefaultPosition, [150,-1], $style ); # Le curseur varie entre $min et $max et commence à $initial. my $gauge = Wx::Gauge->new( $frame, $ID_GAUGE, $range, wxDefaultPosition, wxDefaultSize, $style ); # La jauge pourra varier entre 0 et $range.
Ils descendent tous de la classe Wx::ControlWithItems
, et offrent donc les
méthodes Append
, Insert
, Delete
, Clear
pour gérer leurs éléments.
Pour positionner la sélection on fera appel à SetSelection
ou SetStringSelection
,
et pour récupérer le choix de l'utilisateur, on utilisera GetSelection
ou GetStringSelection
.
Wx::ListBox
et ses variantes Wx::CheckListBox
et Wx::VListBox
.
Les éléments de la liste sont renseignés via une liste anonyme, comme pour les boutons radios.
Wx::Choice
À noter sa variante Wx::ComboBox
moitié « dropdown », moitié saisie de texte.
my $listbox = Wx::ListBox->new( $frame, $ID_LISTBOX, wxDefaultPosition, [150,100], ["ListItem1","Elément2","Choix3","Objet4"] , wxLB_SINGLE ); # Les dimensions de la zone visible doivent être spécifiées, ici [150,100]. my $choice = Wx::Choice->new( $frame, $ID_CHOICE, wxDefaultPosition, [100,-1], ["BLEU","BLANC","ROUGE"] , 0 ); $choice->Append("VERT"); # Ajoute un élément en fin de liste $choice->Insert("JAUNE",2); # Insère un élément entre "BLANC" et "ROUGE" my $position = $choice->GetSelection; # récupère l'index de la sélection my $element = $choice->GetStringSelection; # récupère le texte de l'élément sélectionné
wxWidgets intègre également des widgets complexes qui implémentent un
comportement complet, spécifique à leur type, que le programmeur devra enrichir
en dérivant la classe de base, afin de répondre à ses besoins spécifiques. Nous
allons juste les passer rapidement en revue, car une description complète de
chacun d'entre eux remplirait un livre entier.
La figure 2 montre une application qui utilise un Wx::TreeCtrl
, un Wx::Grid
,
séparés par un Wx::SplitterWindow
, et le tout dans un Wx::TabCtrl
.
Wx::ListCtrl
Comme son nom ne l'indique pas, le widget Wx::ListCtrl
n'est pas juste une liste
de choix. Les widgets de base ListBox
et Choice
sont là pour ça.
Wx::ListCtrl
permet de réaliser des applications comme des gestionnaires de
fichiers, avec affichage d'une liste de fichiers sous forme de noms de fichiers
simples ou détaillée, de petites ou grandes icônes.
Wx::TreeCtrl
Ce widget permet de manipuler des structures de données sous forme arborescente, à la manière du panneau de navigation d'un gestionnaire de fichiers.
Wx::Grid
La grille de tableur est le premier exemple qui vient à l'esprit pour décrire un
widget Wx::Grid
, mais ce contrôle reste un choix intéressant pour manipuler
toute sorte de données structurées en tableau à 2 dimensions.
Wx::CalendarCtrl
Un contrôle très simple d'utilisation et qui peut rendre de grands services. On peut faire ressortir une liste de jours comme les jours fériés, choisir un début de semaine le lundi ou le dimanche, changer couleurs et polices de caractères, etc..
Wx::TabCtrl
On l'utilisera conjointement avec le Wx::NotebookSizer
. Très pratique pour
concentrer un grand nombre de contrôles dans un même dialogue en évitant de
produire une interface trop fournie.
Wx::STC
Ce widget est un wrapper de la classe C++ wxStyledTextCtrl
elle-même
basée sur la bibliothèque Scintilla, qui offre toutes les fonctionnalités
d'un éditeur de texte avancé (coloration syntaxique, complétion automatique,
marqueurs de marge, affichage/occultation de blocs de code, etc..).
Wx::HtmlWindow
Ce widget permet l'affichage de contenus HTML. Vous pouvez programmer une visionneuse de contenus web, mais pas un navigateur complet, car il ne gère pas les formulaires et encore moins les JavaScripts.
Wx::PlValidator
Le concept de validateur (Validators) apporte une valeur ajoutée très utile à la programmation d'IHM. On peut très bien s'en passer pour commencer, mais à l'usage ça simplifie la vie du programmeur. Un validateur est un objet qu'on associe à un contrôle, et qui assume les rôles suivants : Assurer le transfert des valeurs entre les contrôles de l'IHM et les variables internes à l'application et inversement. Valider les données saisies par l'utilisateur dans les contrôles d'IHM, et afficher un message d'erreur quand la saisie utilisateur n'est pas conforme.
Le principe est le suivant : quand un Dialog
est affiché via Show
ou ShowModal
,
une recherche est lancée au sein du Dialog
, pour trouver tous les Validators
des widgets enfants du Dialog
, et pour chacun d'eux, la méthode TransferDataToWindow
est appelée, dont le rôle est de copier dans les contrôles du Dialog
les valeurs
stockées dans les variables Perl internes qui leur sont associées.
Ainsi, le Dialog
est pré-rempli avec des valeurs consistantes, pour peu bien sûr
que les variables internes du programme aient été correctement initialisées.
Par la suite, l'utilisateur interagit avec le Dialog
et modifie le contenu des champs
de saisie, puis valide son action en cliquant sur un bouton "OK" ou en refermant
le Dialog
d'une façon ou d'une autre.
Il suffit alors d'appeler la méthode Validate
du Dialog
qui va automatiquement
propager la validation auprès de chacun des Validators
de ses widgets enfants.
Chaque Validator
vérifie s'il y a lieu la validité des données présentes dans
le contrôle qu'il gère, et retourne 1 pour valider ou 0 pour signaler un problème.
Tous les Validators
enfants doivent retourner 1 pour que Dialog::Validate
retourne 1;
Le programmeur devra vérifier cette valeur retournée. En cas de succès,
on peut passer à l'étape suivante qui consiste à recopier dans les variables Perl
internes les contenus des contrôles d'IHM avec la méthode TransferDataFromWindow
du Dialog
. Cet appel est propagé à tous les Validators
enfants qui se chargent
de recopier la valeur du contrôle qu'ils gèrent vers sa variable interne associée.
Ainsi, on s'évite de coder un callback pour chaque contrôle du Dialog
, et
on peut isoler dans le Validator
tout le code de vérification, que l'on peut
personnaliser à volonté en fonction des besoins.
En C++, wxWidgets propose 2 classes de validateurs prêtes à l'emploi
(wxTextValidator
et wxGenericValidator
), mais qui n'ont pas été intégrées
à wxPerl pour l'instant. Nous allons donc devoir définir nous-mêmes nos
propres Validators
en dérivant la classe de base Wx::PlValidator
comme ceci :
package MyValidator; use strict; use Wx; use base "Wx::PlValidator"; # Classe de base use Wx qw(wxOK wxICON_ERROR); # Constructeur sub new { my $class = shift; my $var = shift; # Ref de la variable associée my $this = $class->SUPER::new; $this->{var} = $var; # Pour consultation ultérieure return $this; } # Indispensable pour les Validators car ils sont clonés en interne sub Clone { my $this = shift; # On transmet la référence à la variable associée my $new = MyValidator->new( $this->{var} ); return $new; } # Appelée par Wx::Dialog->Validate sub Validate { my( $this, $dialog ) = @_; # La méthode GetWindow retourne le widget concerné my $var = $this->GetWindow->GetValue; # Vérification personnalisée # ici uniquement des caractères alphabétiques majuscules et l'espace if ($var !~ /^[A-Z ]+$/) { # Affichage d'un Pop-up informatif Wx::MessageBox( "Erreur: $var n'est pas une valeur admissible.", "Error", wxOK|wxICON_ERROR, undef ); # Signale notre problème au Dialog return 0; } 1; # Signale que tout va bien } # Copie le contenu du contrôle dans la variable associée sub TransferFromWindow { my $this = shift; ${$this->{var}} = $this->GetWindow->GetValue; 1; # Signale que tout va bien } # Copie le contenu de la variable associée dans le contrôle sub TransferToWindow { my $this = shift; $this->GetWindow->SetValue( ${$this->{var}} ); 1; # Signale que tout va bien } package MyDialog; # Dans le constructeur de Dialog my $textentry = Wx::TextCtrl->new( $dialog, $ID_TEXTENTRY, "", wxDefaultPosition, [80,-1], wxTE_PROCESS_ENTER ); $textentry->SetValidator( MyValidator->new( \$main::text_value )); # Normalement, on aurait utilisé ceci: # use Wx::Event qw(EVT_TEXT); # EVT_TEXT( $dialog, $ID_TEXTCTRL, \&OnText ); # Hors constructeur, liste de callbacks # callback traditionnel - même plus besoin ;-) # sub OnText { # my( $this, $event ) = @_; # my $text_value = $this->GetTextctrl->GetValue(); # if ( $text_value =~ /^[A-Z]+$/ ) { # $main::text_value = $text_value # } else { # Wx::MessageBox( "Erreur: $text_value n'est pas une valeur admissible.", # "Error", wxOK|wxICON_ERROR, $this ); # } # } # On effectue tout le travail juste avant de fermer le Dialog sub OnCloseWindow { my( $dialog, $event ) = @_; # Les deux opérations nécessaires sont effectuées dans la foulée return unless ($dialog->Validate and $dialog->TransferDataFromWindow); # A ce niveau, la variable $main::text_value contient la chaîne de caractères # saisie par l'utilisateur, et on est sûr qu'elle est constituée de lettres majuscules $this->Destroy(); } package main; # déclaration et initialisation de la variable interne associée au Validator $main::text_value = "VALEUR INITIALE";
J'ai fais apparaître en commentaire les parties de code qui sont devenues inutiles.
On peut malgré tout décider d'implémenter un callback OnText
si on veut,
mais pour un simple échange utilisateur/application, il n'est plus nécessaire.
Le procédé devient vraiment intéressant quand on a une dizaine de Wx::TextCtrl
à gérer
dans un Dialog
, et l'économie en terme de code et de déverminage également.
Il me reste deux choses à ajouter à propos des validateurs.
D'abord il faut penser à associer à un contrôle un validateur qui lui corresponde.
Ce qui sous-entend qu'il faut coder autant de validateurs qu'il y a de familles
de contrôles dans votre Dialog
.
Les Wx::TextCtrl
implémentent les méthodes GetValue
et SetValue
utilisées
dans notre exemple ci-dessus dans TransferFromWindow
et TransferToWindow
, mais
les Wx::ListBox
ne l'implémentent pas. Vous devrez donc adapter le code de votre
validateur pour appeler GetSelection
et SetSelection
à la place.
Enfin, si les validateurs sont utiles pour vérifier la saisie texte d'un utilisateur,
cet aspect est beaucoup moins utile pour des contrôles comme des Wx::SpinCtrl
, des
Wx::Slider
, des cases à cocher ou des boîtes de boutons radios. En effet, il sera
très difficile à l'utilisateur de retourner une valeur interdite par l'intermédiaire
de ces types de contrôles "bornés".
Ceci étant dit, il n'en reste pas moins vrai que les validateurs permettent de coder
facilement et rapidement les échanges entre contrôles d'IHM et variables internes, quel
que soit leur type.
On ne négligera donc pas l'usage d'un validateur "générique" qui ne vérifie pas
la saisie utilisateur, mais implémente juste TransferFromWindow
et TransferToWindow
.
Seules les Frames peuvent inclure barre de menu, barre d'outils et barre d'état. Les Dialogs et les Panels n'en ont pas.
La création d'une barre de menu se fait à trois niveaux : la barre de menu, les menus proprement dits, et les éléments de menus. Vous pouvez créer tous ces éléments dans l'ordre qui vous plaira, il faut juste que l'élément conteneur existe déjà lors de l'insertion d'un sous-élément.
Traditionnellement, la création de la barre de menu se situe dans le
constructeur de Frame, mais vous pouvez aussi l'isoler dans une fonction
dédiée qui retourne une référence à l'objet Wx::MenuBar
, et faire appel à
cette fonction au sein du constructeur de Frame.
Il est possible de spécifier quelques paramètres en argument des constructeurs,
mais le plus simple est de suivre l'exemple suivant. Les méthodes Append
et
Insert
permettent l'insertion de nouveaux menus dans la barre. Le nom du
menu peut être déclaré à cette étape s'il n'a pas été précédemment spécifié au
constructeur. L'inclusion du caractère "et commercial" ("&"
) dans le titre du
menu permet de déclarer un raccourci clavier par la touche Alt sur la touche
suivant le « & ».
Par exemple $menubar->Append($menumacro,"M&acro");
déclare le raccourci
clavier « Alt-A » pour dérouler ce menu. Cette astuce ne fonctionne que pour
le menu.
Les éléments de menu ne sont pas créés explicitement, mais l'appel à la méthode
Append
les génère automatiquement et les insère dans le menu en une seule
opération. Les arguments fournis à Append
sont l'identifiant de l'élément de
menu, le texte affiché quand le menu se déroule, suivi éventuellement d'un
raccourci clavier que l'on fera précéder par une tabulation ("\t"
), et enfin,
un texte de description qui s'affichera dans la barre d'état lors du survol de
l'élément de menu par la souris. La méthode AppendSeparator
permet d'insérer
une barre de séparation entre éléments de menu.
Il est possible de créer des menus plus « exotiques », en changeant les couleurs du texte et de l'arrière-plan, ou en changeant la police de caractères, mais vous devrez créer les éléments de menus explicitement et modifier leur paramètres standard par l'emploi de méthodes appropriées. Enfin, il est également possible d'insérer des boutons radios et des cases à cocher dans les menus, mais je vous laisse découvrir tout ça par vous-mêmes en parcourant la documentation.
package MyFrame; # Déclaration des identifiants utilisés dans les menus use Wx qw(wxID_HIGHEST); my ($ID_OPEN, $ID_SAVE, $ID_SAVE_AS, $ID_PRINT, $ID_QUIT, $ID_ABOUT, ) = ( wxID_HIGHEST+1 .. wxID_HIGHEST+100 ); # Dans le constructeur de Frame # Création de la barre de menus my $menubar = Wx::MenuBar->new(); # Création du menu "File" my $menufile = Wx::Menu->new(); # Insertion des éléments de menus dans le menu "File" $menufile->Append( $ID_OPEN, "Open\tCtrl-O", "Ouvre un fichier" ); $menufile->Append( $ID_SAVE, "Save\tCtrl-S", "Enregistre un fichier" ); $menufile->Append( $ID_SAVE_AS, "Save As", "Change de nom et enregistre" ); $menufile->AppendSeparator(); $menufile->Append( $ID_PRINT, "Print\tCtrl-P", "Imprime le fichier" ); $menufile->Append( $ID_QUIT, "Quit\tCtrl-Q", "Quitte le programme" ); # Insertion du menu "File" dans la barre $menubar->Append( $menufile, "&File" ); # Même chose pour le menu "Help" my $menuhelp = Wx::Menu->new(); $menuhelp->Append( $ID_ABOUT, "About", "A propos de cette application" ); $menubar->Append( $menuhelp, "&Help" ); # Finalement, affecte la Menubar à la Frame $this->SetMenuBar( $menubar );
Dans la suite du constructeur de Frame, on devra déclarer des liaisons "évènement/callback" pour chaque élément de menu ajouté à la barre de menus.
# Encore dans le constructeur de Frame use Wx::Event qw(EVT_MENU); EVT_MENU( $frame, $ID_OPEN, \&OnOpen ); EVT_MENU( $frame, $ID_SAVE, \&OnSave ); EVT_MENU( $frame, $ID_SAVE_AS, \&OnSaveAs ); EVT_MENU( $frame, $ID_PRINT, \&OnPrint ); EVT_MENU( $frame, $ID_QUIT, \&OnQuit ); EVT_MENU( $frame, $ID_ABOUT, \&OnAbout );
Il est possible d'activer/désactiver les menus et éléments de menu via la méthode Enable
,
et quand le besoin s'en fait sentir, le plus simple est d'utiliser les pseudo-évènements
EVT_UPDATE_UI
qui sont générés automatiquement par wxWidgets, et qui permettent de modifier
les paramètres de l'interface en fonction de l'état de l'application.
Dans cet exemple, l'état de l'application est résumé dans la variable d'instance
$this->{DOC_OPENED}
. Au départ à 0, l'ouverture d'un fichier la positionne
à 1.
# Toujours dans le constructeur de Frame use Wx::Event qw(EVT_UPDATE_UI); EVT_UPDATE_UI( $frame, $ID_SAVE, \&OnUpdateSave ); EVT_UPDATE_UI( $frame, $ID_SAVE_AS,\&OnUpdateSaveAs ); EVT_UPDATE_UI( $frame, $ID_PRINT, \&OnUpdatePrint ); # Hors constructeur # On active l'élément "Print" uniquement quand un fichier est ouvert. sub OnUpdatePrint { my( $this, $event ) = @_; $event->Enable( $this->{DOC_OPENED} ); } # Il n'est fait mention nulle part au widget menu ou menuitem. # c'est l'objet $event qui "connaît" le widget qui l'a émis, # et $event->Enable() suffit à activer/désactiver le bon menuitem.
Le principe est globalement le même pour les barres d'outils, même si les méthodes
appelées sont différentes. On crée la barre d'outils via la méthode CreateToolBar
de la classe Wx::Frame
, ce qui évitera par la suite de lui affecter la barre d'outils.
Les éléments de la barre d'outils sont ajoutés via la méthode AddTool
. Elle prend en
arguments l'identifiant de l'élément, l'objet bitmap concerné, un deuxième bitmap pour
les éléments commutables (toggable) entre l'état enfoncé et l'état relâché, le type
(commutable ou non) de l'élément, un paramètre que vous laisserez à undef
et finalement
un texte d'aide qui s'affichera dans une bulle lors du survol de l'outil par la souris.
Il existe des liaisons "évènement/callback" EVT_TOOL
dédiées aux éléments
de barre d'outils, mais qui sont en réalité des synonymes de EVT_MENU
.
# Dans le constructeur de Frame # Déclare un style pour la toolbar my( $style ) = wxTB_HORIZONTAL | wxTB_DOCKABLE; # Crée une toolbar et mémorise sa référence dans $toolbar my $toolbar = $this->CreateToolBar( $style, $ID_TOOLBAR ); # Ajoute une peu d'espace autour des icônes $toolbar->SetMargins( 4, 4 ); # On utilise des fichiers images pour générer les bitmaps. # Dans l'exemple, "new.xpm","open.xpm",etc.. sont stockés dans le sous-répertoire "bitmaps/" my( %bitmaps ); foreach ( qw(new open save print help) ) { $bitmaps{$_} = Wx::Bitmap->new( "bitmaps/$_[0].xpm", wxBITMAP_TYPE_XPM ); } $toolbar->AddTool( $ID_NEW, $bitmaps{new}, wxNullBitmap, wxITEM_NORMAL, undef, 'New File' ); $toolbar->AddTool( $ID_OPEN, $bitmaps{open}, wxNullBitmap, wxITEM_NORMAL, undef, 'Open File' ); $toolbar->AddTool( $ID_SAVE, $bitmaps{save}, wxNullBitmap, wxITEM_NORMAL, undef, 'Save File' ); $toolbar->AddTool( $ID_PRINT, $bitmaps{print}, wxNullBitmap, wxITEM_CHECK, undef, 'Print File' ); $toolbar->AddSeparator(); $toolbar->AddTool( $ID_HELP, $bitmaps{help}, wxNullBitmap, wxITEM_CHECK, undef, 'Help' ); # Dernière étape, génération de la toolbar à partir des éléments insérés. $toolbar->Realize(); # Puis liaisons évènements/callbacks use Wx::Event qw( EVT_TOOL ); EVT_TOOL( $this, $ID_NEW, \&OnNew ); EVT_TOOL( $this, $ID_OPEN, \&OnOpen ); EVT_TOOL( $this, $ID_SAVE, \&OnSave ); EVT_TOOL( $this, $ID_PRINT, \&OnPrint ); EVT_TOOL( $this, $ID_HELP, \&OnHelp );
Il est possible d'insérer des contrôles dans la barre d'outils, tels que des listes déroulantes généralement utilisées pour changer facilement les tailles de polices ou les styles de texte. La barre d'outil peut être orientée horizontalement ou verticalement, et être dockable, ce qui signifie qu'on peut la détacher de son emplacement d'origine. Encore une fois, je vous encourage à consulter la documentation pour en découvrir toutes les subtilités.
En comparaison avec ce que l'on vient de voir, la gestion de la barre d'état est un jeu d'enfant.
La barre d'état est une petite bande de texte qui apparaît en bas d'une Frame
, et qui permet
d'afficher des messages informatifs. La barre d'état peut être scindée en plusieurs compartiments
lesquels peuvent être de longueur fixe ou variable comme nous allons le voir.
Une seule ligne de code suffit à ajouter une barre d'état à une Frame
.
Même s'il existe un constructeur de Wx::StatusBar
, dans la pratique
on préfère utiliser la méthode CreateStatusBar
de la classe Wx::Frame
.
L'argument numérique qu'on lui passe indique le nombre de compartiments à créer.
Les tailles des compartiments sont spécifiées via la méthode SetStatusWidths
qui prend en arguments
une taille en pixels pour les compartiments à taille fixe, et un nombre négatif pour les compartiments
à taille variable, qui sert de paramètre de pondération pour le calcul de la répartition de l'espace
restant entre les différents compartiments à taille variable restants. L'exemple qui suit vous aidera
à comprendre.
Les méthodes SetStatusText
et GetStatusText
permettent de changer et de récupérer
le texte affiché dans un compartiment selon son numéro. Attention, la numérotation commence à zéro.
# Dans le constructeur de Frame # Ajoute une barre d'état à 3 compartiments. $this->CreateStatusBar( 3 ); # Le 1er compartiment a une taille de 200 pixels # Les 2ème & 3ème compartiments sont à taille variable # Le 2ème occupe les 2/3 (66%) de la largeur restante # Le 3ème occupe 1/3 (33%) de la largeur restante $this->SetStatusWidths( 200, -2, -1 ); # Affiche un message dans le 2ème compartiment $this->SetStatusText( 'Welcome to wxPerl!', 1 );
Notons que la classe Wx::StatusBar
offre également des méthodes PushStatusText
et PopStatusText
de gestion de pile pour empiler, stocker et désempiler les messages.
Dialogs
standardsLes Dialogs
standards sont des objets prêts à l'emploi fournis par wxWidgets,
qui offrent les fonctionnalités habituelles des Dialogs
qu'on trouve
traditionnellement dans des applications graphiques.
Ce sont tous des sous-classes de Wx::Dialog
qui doivent être affichés
par ShowModal
uniquement.
L'aspect visuel ne sera pas forcément le même d'une plate-forme à une autre,
et wxWidgets utilisera les Dialogs
natifs quand ils sont disponibles, mais
l'API de programmation reste partout la même pour un Dialog
donné.
Wx::ColourDialog
est le sélecteur de couleur, Wx::FontDialog
le sélecteur
de police de caractères, Wx::FileDialog
le sélecteur de fichier pour les opérations
d'ouverture et d'enregistrement, Wx::DirDialog
le sélecteur de répertoire (voir figure 3).
Wx::TextEntryDialog
est un petit Dialog
avec une ligne de texte permettant de
poser une question simple à l'utilisateur, et à ce dernier de saisir sa réponse.
Nous avons déjà rencontré Wx::MessageDialog
qui permet d'afficher un message
informatif à l'attention de l'utilisateur.
Wx::SingleChoiceDialog
et Wx::MultiChoiceDialog
permettent de faire une
sélection unique ou multiple parmi une liste de choix.
Notons enfin qu'il existe aussi un Dialog
d'impression et de configuration
des imprimantes, mais qui ne se construit pas à partir d'une classe Wx::PrintDialog
.
Le framework d'impression de wxWidgets est un sous-ensemble de la bibliothèque, et
nécessiterai un article entier pour en faire une présentation exhaustive, aussi je
me contenterai de vous conseiller la lecture du Printing Overview si le sujet
vous intéresse.
Voyons un petit exemple avec Wx::FileDialog
que vous utiliserez certainement
dans vos applications.
Les constructeurs des Dialogs
standards admettent une liste d'arguments qui diffère
selon le type de Dialog
. Il n'y a pas d'API unifiée d'un Dialog
à l'autre.
Pour le Wx::FileDialog
, on doit fournir :
Une référence au parent du Dialog
, en général la Frame
courante.
Un titre qui apparaît dans la bordure de la fenêtre.
Un nom de fichier qui peut être vide, qui s'affichera dans la ligne de texte, et qui aura une action filtrante sur la liste des fichiers affichés. Il est possible et recommandé d'utiliser des wildcards à ce niveau.
Un nom de répertoire (full path) sur lequel le Dialog
va se positionner
à l'ouverture. Très pratique pour retourner automatiquement au dernier répertoire
ouvert.
Une chaîne de patterns de filtrage sur les extensions de noms de fichiers
qui permet de sélectionner dans une liste déroulante (en bas à gauche) les types
de fichiers à afficher. Chaque pattern est constitué d'une chaîne descriptive
(Text Files (*.txt)
) et d'un motif de wildcard associé (*.txt
), séparés
par une barre verticale (Pipe). Les patterns sont concaténés et séparés
par des pipes.
Il d'usage de finir la liste avec "All files (*.*)|*.*
" si l'on veut pouvoir
afficher tous les fichiers.
Une liste de flags combinés par des "OU" logiques (encore des pipes),
et qui permet d'influer sur le comportement du Dialog
. wxMULTIPLE
permet de sélectionner plusieurs fichiers à la fois. Sélectionnez un groupe
de fichiers avec la touche Shift. Ajoutez/retirez les fichiers un à un avec
la touche Ctrl.
wxOVERWRITE_PROMPT
affiche un pop-up de confirmation quand l'utilisateur
spécifie un fichier déjà existant. Il ne fonctionnera qu'avec wxSAVE
.
wxFILE_MUST_EXIST
fait la vérification inverse et affiche un pop-up
d'avertissement quand l'utilisateur entre un nom de fichier inexistant.
Il ne fonctionne qu'avec wxOPEN
.
L'appel à ShowModal
provoque l'affichage du Dialog
et retourne uniquement
quand l'utilisateur clique "OK" ou "CANCEL" ou ferme le Dialog
.
La valeur retournée est soit wxID_OK
, soit wxID_CANCEL
.
On récupère la sélection de l'utilisateur via les méthodes GetPath
ou GetPaths
si wxMULTIPLE
a été spécifié.
package MyFrame; use File::Basename; use Wx qw(:filedialog wxID_CANCEL); # Je passe sur le constructeur de Frame. # Vous connaissez ça par coeur maintenant. # Disons simplement que l'élément de menu "File/Open" # appelle le callback "OnOpen" ci-dessous { # Variables de classe pour conserver la trace # du dernier fichier ouvert entre deux appels my $prevdir; my $prevfile; sub OnOpen { my( $this, $event ) = @_; # Appel au Dialog Wx::FileDialog my $dialog = Wx::FileDialog->new ( $this, # Parent du Dialog, ici la Frame "Ouverture d'un fichier", # Titre du Dialog $prevdir, $prevfile, # Nom de fichier et répertoire par défaut à l'ouverture "BMP files (*.bmp)|*.bmp|Text files (*.txt)|*.txt|All files (*.*)|*.*", # Pattern de filtrage des fichiers affichés wxOPEN|wxMULTIPLE ); # Flags de style if( $dialog->ShowModal != wxID_CANCEL ) { Wx::LogMessage( "Wildcard: %s", $dialog->GetWildcard); my @paths = $dialog->GetPaths; # avec wxMULTIPLE # my $path = $dialog->GetPath; # sans wxMULTIPLE if( @paths > 0 ) { foreach my $file ( @paths ) { # Faire quelque chose avec $file } # En présence de wxMULTIPLE, Wx::FileDialog->GetPath retourne # le dernier fichier de la liste my ($filename, $dirname) = &File::Basename::fileparse($dialog->GetPath); $prevdir = $dirname; # Mémorisation du dernier fichier ouvert $prevfile = $filename; # et de son répertoire } } $dialog->Destroy; # Fermeture & destruction du Dialog } }
Un objet Wx::Event
est créé pour l'occasion quand un événement est déclenché
par quelque chose (un clic de souris sur un bouton, une interruption de timer,
une socket réseau qui a reçu des données à lire, etc.), et véhicule
des informations sur la nature de l'événement, sur l'objet (bouton, timer,
socket, etc.) qui l'a émis, ainsi éventuellement que quelques données transportées.
Cet évènement est d'abord soumis à l'objet qui en est l'émetteur, par exemple
le contrôle Wx::Button
qui a été cliqué, ou l'objet Wx::Socket
qui reçoit
une connexion réseau.
Il est alors transmis au callback qu'on lui a associé dans la liste des liaisons
évènements/callback de l'objet considéré (le Wx::Button
ou la Wx::Socket
).
À ce niveau, il faut distinguer le cas des évènements de type Wx::CommandEvent
,
qui regroupe toutes les actions sur des contrôles, des éléments de menus ou
de barre d'outils, bref des actions directes de l'utilisateur sur l'IHM.
Pour ceux-là, et tant que wxWidgets n'a pas trouvé de callback associé,
l'évènement est propagé au widget parent direct, et ainsi de suite jusqu'à
trouver un callback qui corresponde ou à aboutir à l'objet Wx::App
parent de tous les objets de l'application.
Dans les Dialogs
toutefois, les évènements ne se propagent pas jusqu'à l'objet
Wx::App
, ils s'arrêtent à l'objet Wx::Dialog
.
Il n'y a pas de propagation pour tous les autres types d'évènements
( fermeture ou redimensionnement de fenêtre, appui de touche,
évènement "souris", etc.), mais on peut également la forcer en surchargeant
la méthode ProcessEvent
de l'objet considéré. Par exemple, pour propager
jusqu'à la Frame
principale des appuis de touches de type "raccourci clavier"
qui autrement, seraient captés et interprétés par une zone de texte.
La déclaration d'un callback personnalisé pour un évènement occulte
le traitement qu'un contrôle assure par défaut, et qu'il est parfois souhaitable
de conserver. Il suffit pour ce faire d'appeler la méthode Wx::Event->Skip
en fin de callback. Il est ainsi possible de programmer un comportement
spécifique pour une Wx::ListCtrl
pour gérer les appuis sur certaines touches
du clavier, mais de conserver la navigation traditionnelle par les touches Home,
End,PageUp,PageDown.
La liste exhaustive de toutes les fonctions de liaison événement/callback peut être trouvée dans le fichier Event.pm de la distribution de wxPerl, qui en dénombre plus de 230, raison pour laquelle je ne vais pas toutes les décrire ici. En voici néanmoins un extrait qui en présente les principales :
# Événements liés aux contrôles EVT_BUTTON ( $parent, $ID, \&callback ); # Clic sur un bouton classique EVT_CHECKBOX ( $parent, $ID, \&callback ); # Clic dans une case à cocher EVT_CHOICE ( $parent, $ID, \&callback ); # Sélection dans une liste déroulante EVT_LISTBOX ( $parent, $ID, \&callback ); # Sélection dans une liste "fenêtrée" EVT_TEXT ( $parent, $ID, \&callback ); # Modification du texte contenu # dans une zone de texte EVT_TEXT_ENTER ( $parent, $ID, \&callback ); # Appui sur Enter avec focus # sur une zone de texte EVT_TREE_ITEM_ACTIVATED( $parent, $ID, $callback); # Double-click sur un élément # de TreeCtrl (liste arborescente) EVT_MENU ( $parent, $ID, \&callback ); # Choix d'un élément de menu EVT_TOOL ( $parent, $ID, \&callback ); # Click sur un bouton de barre d'outils # Événements liés au comportement de la fenêtre EVT_CLOSE ( $parent, \&callback ); # L'utilisateur a cliqué # sur la petite croix de fermeture de fenêtre EVT_ICONIZE ( $parent, \&callback ); # L'utilisateur a cliqué sur le souligné # de réduction de fenêtre EVT_SIZE ( $parent, \&callback ); # L'utilisateur a redimensionné la fenêtre EVT_MOVE ( $parent, \&callback ); # L'utilisateur a déplacé la fenêtre # Événements divers et variés EVT_TIMER ( $parent, $ID, \&callback ); # Le timer considéré s'est déclenché # à l'expiration de son timeout EVT_SOCKET_CONNECTION ( $parent, $ID, \&callback ); # Une demande de connexion cliente # vient d'arriver # sur la socket considérée EVT_SOCKET ( $parent, $ID, \&callback ); # Un paquet a été reçu sur # la socket réseau considérée EVT_IDLE ( $parent, \&callback ); # Le dernier évènement en attente vient d'être # traité. La file est vide. C'est le début # d'une période d'inactivité # Événements liés au clavier et à la souris EVT_CHAR ( $parent, \&callback ); # Une touche clavier a été pressée et relâchée EVT_MOTION ( $parent, \&callback ); # Déplacement de la souris EVT_MOUSEWHEEL ( $parent, \&callback ); # Rotation de la roulette de la souris # Les trois fonctions ci-dessous existent également dans leurs variantes "RIGHT" et "MIDDLE" EVT_LEFT_DOWN ( $parent, \&callback ); # Le bouton gauche de la souris a été pressé EVT_LEFT_UP ( $parent, \&callback ); # Le bouton gauche de la souris a été relâché EVT_LEFT_DCLICK ( $parent, \&callback ); # Double-clic sur le bouton gauche de la souris
La description qui suit est librement adaptée de la doc de wxDesigner. Merci à Robert Roebling son aimable autorisation.
Le moyen le plus "basique" de gérer l'agencement des widgets à l'intérieur d'une
Frame
est de spécifier leur position et leur taille dans l'appel au
constructeur comme suit :
my $button = Wx::Button->new( $parent, $ID, $label, \@position, \@size );
@position
et @size
sont des listes contenant deux entiers. La syntaxe
impose de spécifier une référence à ces listes. On peut également l'écrire avec
des listes anonymes. Par exemple: "$label, [10,20], [$largeur,$hauteur] ..."
Si on se contente de cette méthode pour positionner les widgets, la tâche
devient vite fastidieuse. Il faut calculer la position de chaque widget en
fonction des autres. D'autre part, quand la Frame
change de taille, il faut
tout recalculer et repositionner tous les widgets enfants de la Frame
via
la méthode &OnSize
que l'on associe traditionnellement à l'événement
EVT_SIZE
évoqué plus haut.
Afin de s'épargner ces complications, les développeurs de wxWidgets ont introduit les "Sizers" dont le rôle est de gérer tout ça automatiquement.
Sizer est le mot utilisé ici pour désigner tout objet de la classe wxSizer
ou d'une de ses sous-classes. Sous wxWidgets, les Sizers sont
indiscutablement la meilleure méthode pour contrôler la disposition des widgets
dans les boîtes de dialogue, grâce à leur capacité à pouvoir s'adapter
automatiquement aux changements de taille et de style d'une plate-forme à
l'autre, et de générer des boîtes de dialogue visuellement agréables sous tous
les environnements à partir d'un code source unique.
Les sizers de wxWidgest sont les proches parents des systèmes utilisés dans d'autres frameworks d'IHM tels que AWT de Java, GTK+ ou Qt. L'idée de base est de demander à chaque contrôle individuel sa taille minimale et sa capacité à s'étendre ou à se rétrécir si la taille de la fenêtre parent change. Ceci implique le plus souvent que le développeur ne fixe pas la taille initiale de la boîte de dialogue mais lui affecte un sizer qui la déterminera dynamiquement à l'exécution. Pour cela, le sizer demandera à chacun des ses widgets enfants (qui peuvent être des fenêtres normales ou d'autres sizers) sa taille initiale, et chacun des sizers enfants propage la demande à ses propres enfants, etc. Ainsi, toute une hiérarchie de sizers se construit et s'adapte automatiquement à tout changement. Remarquez que la classe wxSizer ne dérive pas de wxWindow et que donc un sizer n'est pas une fenêtre. Un sizer consomme très peu de ressources comparé à une vraie fenêtre.
Les sizers sont extrêmement bien adaptés aux boîtes de dialogue cross-plates-formes car chaque contrôle ne calcule que sa propre taille et donc l'algorithme peut sans problème gérer les disparités de tailles de widgets d'une plate-forme à l'autre. Par exemple, si la taille de la police standard sous Linux/GTK est plus grande que sous Windows, la boîte de dialogue apparaîtra automatiquement plus grande sous Linux/GTK que sous Windows.
Quand on utilise les Sizers, on ne spécifie pas de position ou de taille lors
de l'appel au constructeur du widget. En revanche, on utilise les constantes
wxDefaultPosition
et wxDefaultSize
comme suit :
my $button = Wx::Button->new( $parent, $ID, $label, wxDefaultPosition, wxDefaultSize, $style, $validator, $name );
Tous les sizers sont des conteneurs d'un ou plusieurs sous-éléments. Ces éléments sont couramment appelés les enfants du sizer et peuvent être de natures différentes, mais tous les enfants ont certaines propriétés en commun :
Cette taille est normalement égale à la taille initiale et, pour des widgets, peut être spécifiée explicitement en utilisant le paramètre wxSize du constructeur, ou déterminée automatiquement par wxWidgets si vous utilisez la valeur de -1 pour la hauteur et la largeur du contrôle.
Notez que les widgets ne peuvent pas tous calculer leur taille. Par exemple, une boîte à cocher pourra le faire tandis qu'une liste n'a pas de hauteur ou largeur naturelle et vous devrez donc lui spécifier explicitement une taille. De plus, quelques contrôles peuvent déterminer leur hauteur, mais pas leur largeur (une ligne de saisie, par exemple). (Voir figure 4 - A).
C'est juste un espace vide qui est utilisé pour séparer les éléments dans une boîte de dialogue. La bordure peut entourer un contrôle de tous les côtés ou être présente le long de certains côtés seulement, comme dessus et dessous par exemple. La largeur de la bordure doit être spécifiée explicitement, la valeur typique étant de 5 points. (Figure 4 - B).
Souvent, un contrôle obtient plus de place que strictement nécessaire. En fonction des drapeaux d'alignement utilisés pour ce contrôle, il peut soit s'agrandir pour remplir tout l'espace qui lui a été alloué, soit être déplacé pour s'aligner d'un côté de cet espace ou en son centre. (Figure 4 - C).
Si un sizer a plus d'espace que la somme des tailles de ses enfants, il doit décider comment distribuer l'excès d'espace parmi ses enfants. Pour qu'il puisse le faire, on associe à chaque enfant un facteur d'agrandissement. La valeur par défaut de ce facteur est 0 et signifie que le contrôle n'obtiendra jamais plus de place que sa taille minimale requise. Autrement, la valeur de ce facteur détermine la partie relative de l'espace en excès qui sera attribué à cet enfant. Par exemple, s'il y a deux enfants et le facteur d'agrandissement de chacun est égal à 1, alors chaque enfant obtiendra la moitié de l'espace en trop indépendamment du rapport de leurs tailles minimales respectives. (Figure 4 - D).
Il y a actuellement 5 types de sizers différents. Chacun représente une façon différente de positionner les contrôles dans une boîte de dialogue. Chaque type de sizer est décrit rapidement ci-dessous, et la doc de wxWidgets fera le reste :
Wx::BoxSizer
Ce sizer positionne ses enfants soit verticalement, soit horizontalement, en fonction du drapeau spécifié dans son constructeur. Quand la version verticale est utilisée, chaque enfant peut être centré ou aligné à droite ou à gauche. Dans la version horizontale, chaque enfant peut être centré ou aligné en bas ou en haut. Le facteur d'agrandissement décrit ci-dessus s'applique seulement dans la direction principale du sizer, c'est-à-dire pour un sizer horizontal il détermine comment un élément s'agrandit dans la direction horizontale. (Figure 4 - E).
Wx::StaticBoxSizer
Ce sizer est identique à Wx::BoxSizer
entouré par une boîte statique, c'est à dire
avec un petit liséré encadrant le sizer, et un petit label de titre. (Figure 4 - F).
Wx::GridSizer
Ce sizer est bidimensionnel. Il détermine la taille minimale du plus grand de ses enfants, selon les deux directions horizontale et verticale, et applique ces dimensions à toutes les « cases » de la grille. (Figure 4 - G).
À la création du Wx::GridSizer
, il faut fixer, soit le nombre de colonnes, soit
le nombre de lignes, et le sizer s'allongera dans l'autre direction
quand des nouveaux éléments sont ajoutés.
Wx::FlexGridSizer
Un autre sizer bidimensionnel dérivé de Wx::GridSizer
. Ici, la largeur de chaque
colonne et la hauteur de chaque ligne est calculée individuellement pour chacune
en retenant l'enfant le plus large/haut de la colonne/ligne.
De plus, les colonnes et les lignes peuvent s'agrandir (comme si elles étaient
des éléments individuels avec le facteur d'agrandissement de 1). (Figure 4 - H).
Wx::NotebookSizer
Ce sizer s'utilise exclusivement avec Wx::Notebook
. Il calcule la taille de
chaque page en ajustent sa taille sur la plus grande page.
Voyons un rapide exemple de code :
# Création d'un simple BoxSizer Horizontal et d'un bouton my $sizer = Wx::BoxSizer->new( wxHORIZONTAL ); my $button = Wx::Button->new( $frame, $ID_BUTTON, "OK", wxDefaultPosition, wxDefaultSize, 0 ); # Ajout du bouton dans le sizer $sizer->Add( $button, # Référence au widget inséré 1, # Facteur d'agrandisement wxALIGN_CENTER|wxALL, # liste de flags d'alignement # et de position de bordure (wxALL) 15 # Taille de bordure en pixels ); # Affectation du sizer à la Frame $frame->SetSizer( $sizer );
Maintenant que nous avons fait le tour des Sizers
et de leurs caractéristiques,
il me reste un conseil à vous donner : utilisez-les systématiquement, mais n'allez
pas vous embêter à les coder à la main.
Il existe des outils spécialisés pour cette tâche qui vous simplifieront le travail
et vous feront gagner un temps précieux, tel que wxDesigner (Figure 5).
J'ai déjà cité les principaux EDI lors de mon précédent article. Il y aussi quelques
éditeurs de dialogues intéressants à découvrir sur la page Resources/Useful Tools
du site web de wxWidgets. La plupart d'entre eux ne produiront pas du code Perl, mais
il sera toujours possible de sauvegarder vos créations sous le format XRC,
qui est un format universel, basé sur XML et qui permet le chargement de la description
de l'IHM à l'exécution et non pas à la compilation.
Nous voici au terme de cette présentation qui aura su, je l'espère, vous convaincre
que wxPerl est un outil exceptionnel de conception d'applications IHM, complet,
moderne et rapide à mettre en œuvre.
Je n'ai pas pu, faute de place, aborder comme je l'avais prévu, la gestion des icônes,
les objets Wx::Config
et les timers, et je m'en excuse.
Mais je vous invite à parcourir la documentation et les exemples de code pour les
découvrir, ainsi que d'autres aspects intéressants comme les classes Documents/Views
,
les Sockets
réseaux, le framework d'impression, le Drag'n Drop,
le Splash Screen, etc... la liste est trop longue pour continuer.
Bonne découverte et bonne programmation.
Christophe Massaloux - <cmassaloux@free.fr>
Un grand merci à tous les relecteurs du groupe articles des Mongueurs pour leurs /^(conseil|remarque|encouragement)s? (avisé|pertinent|apprécié)e?s?$/
Copyright © Les Mongueurs de Perl, 2001-2011
pour le site.
Les auteurs conservent le copyright de leurs articles.