Cours Python pour apprendre la création d'application avec wxPython
Cours Python pour apprendre la création d'application avec wxPython
...
- Introduction
Le document que vous avez entre les mains a été écrit initialement à la HEIG-VD dans le cadre d'un projet de trimestre. Son objectif est de vous présenter la librairie wxPython[2] en decrivant son fonctionnement ainsi que les fonctionnalités qu'elle met à disposition. Dans l'idéal, ce document devrait pouvoir accompagner un cours d'interface utilisateur, tel que celui enseigné en 2ème année de la voie "Informatique Logiciel" de l'HEIG-VD, en utilisant Python et wxPython comme outil de démonstration des concepts enseignés.
1.1. Présentation générale
L'API wxPython permet la création d'interface utilisateur basique et évoluée. Elle offre les différents objets que l'on peut s'attendre à rencontrer dans une application moderne, la possibilité de dessiner en deux dimensions, et supporte des interfaces de type MDI (Multiple Document Interface). wxPython est une API simple d'utilisation prévue pour être utilisée avec le langage Python. Il s'agit d'une adaptation d'une autre API à l'origine prévue pour le langage C++ nommée wxWidgets[3]. Ce dernier point implique que la structure de l'API (hiérarchie des classes, nom des méthodes, etc.) est presque exactement la même pour les deux et qu'il est donc possible de se référer aisément aux nombreux documents existant pour wxWidgets.
- Installation
2.1. Linux/Unix
Pour la majorité des distributions Linux, il existe des paquets qui feront tout pour vous. Des RPM sont disponibles sur le site de wxPython et les paquet libwxgtkX.Y-python sont présents dans les dépôt Debian (X et Y étant les numéros de version). Autrement, wxPython a comme dépendances les librairies "glib" et "gtk+" qui sont installée par défaut dans un environnement GNOME.
2.2. Windows
En vous rendant sur le site wxPython[2], vous trouverez un programme d'installation de la librairie pour Python 2.3, 2.4 et en ANSI ou unicode.
2.3. MacOS 9 et MacOS X
La librairie wxPython est installée par défaut depuis MacOS X 10.3 (Panther) (mais pour Python 2.3 néanmoins) et ne nécessite donc aucune installation spéciale. A la différence d'un programme python normal, il faudra lancer vos script wxPython avec l'interpreteur pythonw. Si vous posséder une version antérieure de MacOS, vous devrez installer wxPython a partir des paquetages externe[5].
- Concepts de base
3.1. Hello World!
L'apprentissage commence généralement par l'exemple le plus simple possible. Nous ne ferons pas exception à la règle. Vous pouvez ouvrir le premier exemple intitulé "hello.py" dans votre éditeur préféré et l'exécuter. Vous devriez obtenir une présentation semblable à cette dernière (il peut y avoir des variations suivant l'OS que vous utilisez).
Si le programme refuse de s'exécuter, reportez-vous au chapitre concernant l'installation sur votre système et reportez-vous a la documentation. Vous pouvez constater qu'il n'a pas fallu beaucoup de ligne de code pour faire cela. De plus, ce premier exemple est excessivement structuré en deux classes, ce qui n'est pas forcément obligatoire.
Analysons quelques lignes essentielles:
import wx
Toute application wxPython possédera au moins cette demande d'import. Il s'agit d'importer les classes mises à dispositions par le module dans notre application afin de pouvoir les utiliser. La classe MainWindow dérive de wx.Frame, la classe de base dans wxPython pour représenter une fenêtre à l'écran. Le constructeur de la classe appelle celui de son parent avec des paramètres dont vous pouvez déjà deviner l'utilité pour certains.
self.Show(True)
La méthode Show est héritée de wx.Frame et permet de dire à la fenêtre de s'afficher ou pas à l'écran suivant le paramètre fourni. wxPython utilise la classe particulière wx.App (plusieurs sont disponibles en réalité) pour définir l'application dans son ensemble. Cette encapsulation supplémentaire permet de gérer plus facilement certains concepts propres aux interfaces graphiques. Dans notre exemple, la classe MainApp en dérive et crée l'objet représentant notre fenêtre principale. Pour des raisons de gestion propre à wxPython, il faut redéfinir la méthode OnInit à la place du constructeur. Cette méthode créer une instance de notre fenêtre et lui demande de s'afficher en premier plan. Finalement, l'instance de notre application est créée uniquement si ce script est le programme principal. Nous expliquerons l'appel à MainLoop dans la partie concernant la gestion d'événements.
3.2. La classe wx.App
Au sous-chapitre précédent, nous avons vu un programme formé de deux classes. Ces deux classes ne sont pas là par hasard. Dans toutes application wxPython, les classe App et Frame sont les composants de base dont découle le reste. Bien qu'il soit possible d'instancier directement des objets de ces deux classes, l'usage veut qu'on les dérive tout comme cela a été fait dans le premier programme d'exemple. La classe Frame sera décrite en détail dans le chapitre 6. Ici, nous nous développerons plus en détail la classe App. Dans une application wxPython, il est nécessaire d'avoir une (et une seule uniquement) instance de App avant d'instancier le moindre composant graphique (à priori, la fenêtre principale).
Effectivement, cet objet particulier interagit directement avec le système et c'est lui qui prépare le terrain pour la création de vos composants graphiques. De ce fait, il est fortement déconseillé de redéfinir un constructeur pour les classes héritant de App, car vous risqueriez de perturber le fonctionnement interne. C'est pour cette raison que la classe App dispose d'une méthode OnInit() automatiquement invoquée une fois l'objet instancié. Pour être sur de ne pas créer de composant graphique avant d'avoir créer un objet App, nous prendrons l'habitude de créer les instances de fenêtres dans la méthode OnInit(), d'où l'intérêt de toujours avoir une sous-classe de App, même si ses fonctionnalités ne sont pas étendues.
En plus de préparer le système à la création de composant graphique, l'objet App possède le gestionnaire d'événement qui est démarré lors d'un appel à la méthode MainLoop(). Basiquement, le gestionnaire d'événement est une boucle infinie qui fait deux choses. En premier lieu, elle attend de recevoir des signaux de la part du système d'exploitation (un clic de souris par exemple) puis lorsqu'elle reçoit un tel signal, elle l'ajoute dans une file d'attente avec divers paramètres (type d'événement, source de l'événement, etc.). Ensuite, elle retire périodiquement un événement de la queue et invoque la méthode liée selon les paramètres qu'elle peut lire. Si aucune méthode n'est liée, elle oublie l'événement et retire le prochain de la file. Du coup, tout ce que le programmeur doit faire, c'est lié une méthode à un événement comme nous le verrons dans le sous-chapitre suivant. La classe App permet encore de faire plusieurs choses qui dépassent la portée de ce tutoriel. Vous pouvez vous référer au livre "wxPython in action"[1] (en Anglais) pour plus d'informations. Notez finalement que le gestionnaire d'événement, bien que l'on dise que sa boucle est infinie, s'interrompt si plus aucune instance de Frame existe dans le programme. L'instance de App est alors détruite et vous ne pourrez plus recréer de composant graphique avant d'avoir recréer une telle instance.
3.3. Liaison widget/code
Un événement est généré quand "quelque chose se passe sur un objet wxPython". Ce "quelque chose" peut être de nature très différente. Il peut avoir été généré par l'utilisateur, lors d'un clic sur un bouton par exemple ou avoir été généré directement par un objet quand il a changé d'état. Nous avons vu que les événements, une fois généré, étaient intercepté par un gestionnaire d'événement et que tout ce que nous avions à faire était de lié les actions à effectuer lorsque le gestionnaire détecte un événement précis. Le programme goodbye.py donne un bref exemple de réalisation. Sans nous attarder sur la création d'un Panel et d'un bouton qui seront décrit plus tard, regardons la méthode clé, Bind(). Cette méthode est définie dans la classe EvtHandler dont beaucoup de classe dérivent (Frame et Menu notamment, ainsi que App comme vous l'aurez sans doute deviné). Elle permet de faire une association entre un type d'événement, une méthode visible et un widget visible. Visible, dans le sens, de la visibilité du code et non pas du fait que l'objet est affiché ou pas à l'écran.
self.Bind(wx.EVT_BUTTON, self.OnClick, self.button)
wx.EVT_BUTTON est une des nombreuses constantes à disposition représentants les événements qui peuvent survenir. Celle-ci représente un clic de souris sur un bouton de la frame (raison pour laquelle un appel au Bind() de la frame est fait). Au sein de la documentation online de wxPython, vous trouverez, pour les différents widgets, la liste des événements en explorant la documentation de la classe wx.Event et ses sous classes. Dans cet exemple illustratif, nous avons une méthode OnClick() dans la classe MainWindow, qui demande à l'application de se fermer à l'aide de la fonction Close() qui sera expliquée plus tard. Cette méthode pourrait porter n'importe quel nom, par exemple ClickOnButton(). La méthode doit posséder un paramètre qui récupérera l'instance de l'événement. Ce paramètre doit être le deuxième dans le cas d'une méthode de classe (le premier étant self) ou le premier s'il s'agit d'une fonction "statique". Par convention, il s'appellera event.
def OnClick(self, event):
Notez, qu'en tant que paramètre d'une autre méthode, nous passons l'instance de la méthode OnClick() en paramètre, il ne faut donc pas mettre les parenthèses ! Finalement, le dernier paramètre est l'instance de l'objet source de l'événement à associer si l'appel de Bind() n'est pas fait directement sur l'objet provoquant l'événement comme dans ce cas (Le EVT_BUTTON est transmis depuis la frame, mais ne précise pas quel bouton a été cliqué). Voilà, avec cette unique ligne, nous avons associé une méthode qui termine l'application au clic d'un bouton précis. Cette façon de faire est généralisable à tous les objets qui peuvent avoir des événements associés. Bind() peut aussi prendre deux paramètres, id1 et id2 au lieu de l'objet source. Cela permet d'associer un même événements à une plage de plusieurs objets identifié par des id consécutifs. Néanmoins cette façon de faire est rarement utilisées. Il est préférable de manière générale de ne pas travailler avec les id. Pour information, chaque objet dans wxPython a une propriété id qui est un numéro unique. Finalement, le paramètre source (et les paramètres id1, id2) est optionnel. A utiliser à vos risques et périls.
3.4. Propriétés communes des widgets
Tous les objets affichables dans wxPython dérivent d'une classe d'origine qui est wx.Window.
Le constructeur de cette classe se présente sous la forme suivante et permet donc de définir des propriétés communes à tous les objets affichables.
wx.Window(self, parent, id=-1, pos=DefaultPosition, size=DefaultSize, style=0, name=PanelNameStr)
Le paramètre parent définit, comme son nom l'indique, l'objet parent de celui que vous créez. Ceci permet de conserver une hiérarchie des objets qui sont ainsi contenu les uns dans les autres. Si nous reprenons le code précédent, vous pouvez voir que la fenêtre (wx.Frame) contient un conteneur d'objet (wx.Panel pour lequel le parent est l'instance de wx.Frame) qui lui même contient un bouton. Il est évidement possible de naviguer dans l'arbre ainsi crée à l'aide des méthodes suivantes:
GetParent()
Permet d'obtenir une référence sur l'objet parent.
GetChildren()
Permet d'obtenir une liste des références sur les enfants de l'objet. Il est important de savoir que cette liste est une copie de la liste maintenue par wxPython et donc, que la création d'un nouvel enfant ne la met pas à jours. Cela peut révéler un intérêt pour certaine application ou dans le positionement à la volée des objets dans un panel. A vous de vous montrer créatifs. En plus de la structure arborescente, wxPython entretient un numéro d'identification UNIQUE pour chaque widget. Avoir deux objets contenant le même ID peut se révéler désastreux et amener l'application à des comportements inattendus.
Du coup, que mettre comme valeur pour la paramètre id ?
Si vous ne comptez pas utiliser explicitement ce numéro, autant laisser la valeur par défaut et passer les paramètres du constructeurs par nom, ou alors utiliser la valeur wx.ID_ANY si les paramètres sont passé par position. wxPython se chargera alors de générer automatiquement une ID valable pour votre widget. Sinon, il est possible de générer une id à l'aide de la fonction wx.NewId() pour enregistrer une ID unique dans une variable et la passer en paramètre du constructeur après coup. Finalement, quelque soit la méthode employée, tous les widgets disposent d'une méthode GetId() si le besoin s'en fait sentir.
Les paramètres pos et size du constructeur spécifient la position et la taille de départ de l'objet. Les constantes DefaultPosition et DefaultSize dépendent de l'objet qui est en train d'être créer. Bien que wxPython définisse deux classe wx.Point et wx.Size avec les méthodes appropriée, il est généralement plus simple d'utiliser un tuple de deux entier pour spécifier la position ou la taille d'un objet. Vous pouvez le constater dans l'exemple de code goodbye.py. wx.Window, mais aussi de nombreux widgets en dérivant, définissent des styles qui peuvent influencer l'apparence des objets à l'écran.
Ce sont des constantes listées dans le manuel de référence pour chaque widget. Ces constantes définissent un style atomique et si vous voulez mélanger plusieurs style, vous pouvez utiliser l'opérateur | (OU logique). L'opération inverse est aussi possible, à l'aide de l'opérateur ^ qui retire des éléments de style à un mélange de styles atomiques. Toujours dans notre exemple, goodbye.py, vous pouvez voir un exemple d'utilisation des opérateurs à la création de la fenêtre. Attention tout de même à deux choses.
Le manuel de référence est encore loin d'être complet et une recherche sur internet vous informera parfois mieux sur les styles à disposition. Le livre "wxPython in action"[1] donne aussi de bonne astuces. Les paramètres peuvent avoir des effets très différents en fonction de la plateforme d'exploitation. Ceci peut être gênant lors de l'utilisation du paramètre pos et nous verrons par la suite une méthode évoluée de positionnement.
Finalement, le paramètre name permet de donner un nom à l'objet et ne s'avère pas spécialement utile. Vous connaissez désormais les bases du fonctionnement de wxPython. Par la suite, nous allons voir les différents objets que vous pourrez utiliser dans vos applications graphiques.
- Fenêtrage
4.1. Frame et notion de Top-Level Window
Une interface graphique en wxPython contient au minimum un objet nommé «top-level window object» qui est généralement un objet de type wx.Frame (fenêtre). Cet objet n'est pas contenu dans un autre widget, c'est en quelque sorte la "racine" de l'application graphique. C'est généralement cet objet qui est défini comme la fenêtre principale de votre application et qui contient les autres objets graphiques. Vous pouvez avoir plusieurs objets sans parent (plusieurs «top-levels window»), toutefois vous devez préciser à wxPython lequel sera considéré comme le top-levels window principal. Cette opération s'effectue grâce à la méthode SetTopWindow(). Si vous ne spécifiez pas explicitement quelle frame sera la "top-levels window" principale, wxPython prendra la première frame définie dans votre wx.App.
4.2. Quitter l'application
L'application quitte une fois que toutes les fenêtres "top-level" sont fermées, c'est à dire, une fois que toutes les frames sans parent sont fermées et pas seulement la frame désignée par la méthode SetTopWindow(). Il est possible de fermer une frame en appelant la méthode Close(). Une autre façon de quitter l'application est d'utiliser la fonction globale wx.Exit(). Il est possible de définir un traitement de «nettoyage» juste avant que l'application quitte grâce à la méthode OnExit() de la classe wx.App. En effet, cette méthode est appelée lorsque la dernière Frame principale est fermée. Ceci peut être pratique pour fermer des connexions réseau ou à une base de données par exemple.
4.3. Positionnement
Il existe 2 approches différentes pour disposer les éléments graphiques dans une fenêtre. La première, utilise le positionnement absolu tandis que la deuxième utilise un élément spécial nommé «Sizer».
4.3.1. Absolu
Le positionnement absolu est un procédé très simple et intuitif pour placer les différents objets. Il suffit d'utiliser des coordonnées (X, Y) où l'origine se trouve en haut à gauche la frame. Cette méthode va très bien quand on possède un contrôle total sur la taille de la fenêtre et sur le nombre de widget la contenant. Toutefois, on arrive vite aux limites de cette approche, notamment lorsque la taille de la fenêtre n'est pas fixe. Il est dans ce cas, préférable d'utiliser les Sizers.
4.4. Sizers
4.4.1. Positionnement à l'aide de Sizers
Un Sizer est un objet dont le seul but est de gérer la disposition d'un jeu de widgets à l'intérieur d'un conteneur. Le Sizer n'est pas un conteneur ou un widget en soit. C'est en réalité une représentation d'un algorithme de positionnement. Tout les Sizers sont des instances d'une sous-classe de la classe abstraite wx.Sizer.
… … …
L'utilisation d'un sizer s'effectue en 3 étapes :
- Création du sizer dans un conteneur. Un sizer est appliqué à un widget conteneur en utilisant la méthode SetSizer(sizer) de l'objet wx.Window (la super-classe de tous les widgets affichable).
- Ajouter chaque widget enfant au sizer, via la méthode Add() de wx.Sizer.
- (Optionnel) activer le calcul automatique de la taille du sizer (appel en cascade de la méthode Fit(window) de wx.Window)
4.4.2. Ajouter des widgets au sizer
Pour ajouter des widgets au sizer, la solution la plus commune est d'utiliser la méthode Add().
Elle ajoute le widget à la fin de la liste de widgets enfant du Sizer. La méthode Add() est déclinée en 3 version:
Add(window, proportion=0, flag=0, border=0, userData=None)
Add(sizer, proportion=0, flag=0, border=0, userData=None)
Add(size, proportion=0, flag=0, border=0, userData=None)
La première est la plus utilisée, elle permet d'ajouter un widget (argument window) au sizer. La seconde permet d'imbriquer les sizers. La troisième est utilisée pour ajouter un espace vide dont la taille est définie par l'argument size (de type wx.Size). Les autres paramètres influencent la manière dont le widget sera disposé. Plusieurs de ses paramètres sont disponible que pour certain Sizer. L'argument proportion est seulement utilisé par le box sizer, est permet de spécifié comment l'élément sera redimensionnable quand le conteneur parent changera de taille. L'argument flag permet de spécifié des options d'affichage (comme l'alignement, la bordure ou encore le redimensionnement). L'argument border détermine la taille de la bordure si une bordure a été spécifié par le paramètres flag. Pour finir le paramètre userData peut être utilisé dans le cas ou vous définiriez votre propre Sizer. Les différentes constantes à passer au paramètre flag se trouve dans la documentation de l'API sous la description de la méthode Add de wx.Sizer.
Il existe d'autres méthodes pour ajouter des widgets au Sizer, la méthode Insert() permet d'ajouter un widget en spécifiant un index de positionnement, et la méthode Prepend(), identique à Add() mais qui ajoute cette fois ci, le widget en début de liste. Pour plus d'information, reportez-vous a la documentation de wxPython.