[couverture de Linux Magazine 71]

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

Article publié dans Linux Magazine 71, avril 2005.

Copyright © 2005 - Christophe Massaloux.

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

Chapeau

Au 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 !

Présentation des widgets de base

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.

[Figure 1]

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.

Les widgets de présentation et validation.

On peut uniquement spécifier le texte affiché sur le widget. Il ne retourne pas de valeur saisie par l'utilisateur :

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

Les widgets de saisie de valeur textuelle ou numérique.

Pour tous ceux-là, il existe des méthodes SetValue et GetValue pour changer/récupérer le contenu affiché/saisi :

    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.

Les widgets de sélection parmi un choix de valeurs pré-déterminées.

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.

    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é

Présentation rapide des widgets complexes

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.

[Figure 2]

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

Présentation des menus, barres d'outils et barres d'état

La barre de menus

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.

La barre d'outils

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.

La barre d'état

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.

Présentation des Dialogs standards

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

[Figure 3]

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 :

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

Les événements

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 mise en page, usage des sizers

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

Les traits communs

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 :

Les différents types de sizers

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 :

[Figure 4]

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.

[Figure 5]

Conclusion

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.

Auteur

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?$/

Quelques liens sur le web

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