wxGlade

(Tutoriel)

Le paquetage wxGlade, écrit en Python avec wxPython, sert à construire interactivement les interfaces graphiques utilisateur.

Il en existent d'autres applications similaires : Python Card, Boa Constructor, VisualWx (multi-langage), etc., y compris quelques commerciales. À cela nous pouvons ajouter des environnements intégrés du développement des programmes, contenant des éditeurs, lanceurs, visualiseurs d'images, analyseurs de données, etc., comme Eclipse (centré sur Java, mais très universel), mais ceci est moins intéressant pour nous.

Quel paquetage (libre) exploiter? Il n'y a pas de réponse ultime à cette question, actuellement tout présente des déficiences, tout est bogué, et évolue lentement, voire pas du tout... Nous avons choisi wxGlade car il est compact, et relativement complet, y compris les sizers, qui sont difficiles à apprivoiser. Le prix à payer est que wxGlade n'apporte pas beacoup d'aide concernant la génération automatique du code (surtout la gestion des événements).

 

Par contre, le code qui est engendré automatiquement et qui décrit les entités visuelles, est assez propre. Si vous voulez plus de support automatique, essayez Boa.

On commence

Attention. Ce texte est le support écrit du tutoriel, qui est présenté interactivement, avec beaucoup d'information transmise oralement, avec les réponses à vos éventuelles questions, etc. Donc, les absentéistes auront des sacrés problèmes pour profiter efficacement de ce tutoriel.

palette (4K) Vous avez à votre disposition la palette de composantes, d'où vous choisissez le widget qui doit être placé sur la fenêtre-cible.

Il faut commencer par une fenêtre "principale", placée sur l'écran en dehors des autres interfaces. Vous pouvez choisir soit un Frame, ou un Dialogue (pour la communication temporaire). Le modèle est dessiné sur l'écran et vous pouvez y déposer vos widgets. La fenêtre d'arborescence montre la hiérarchie des widgets déposés, et vous voyez que le Frame est accompagné "gratuitement" d'un sizer.

La fenêtre de propriétés contient le bouton "Preview", qui affiche l'interface réelle : dans le contexte actuel le Frame est vide, et aura une taille minimale.

 

La fenêtre principale peut avoir des éléments qui ne sont pas des widgets, et qui sont gérés par les propriétés (surtout dans l'onglet Widget). Par exemple, vous avez la case de fermeture. Ajoutons alors les barres du statut, et des menus. La fenêtre de l'arborescence montre la composante active, vous pouvez toujours passer à une autre, et la rédiger à volonté.

La barre des menus vous invite à rédiger les menus individuels. Insérez un menu "bidon", dont l'étiquette (Label) s'appelle "File", "Fichier", ou autre chose standard. preview0 (2K) Ajoutez aux propriétés du Frame la couleur du fond (Background) quelconque. Cochez la case "Size" qui empêche le retrécissement de la fenêtre, en gardant la taille déclarée ou appliquée. Lancez la prévisualisation. Vous pouvez obtenir le résultat comme à droite.

Il est souhaitable de choisir la composante "Application" dans la fenêtre de la hiérarchie, et de générer le code. Ouvrez-le dans un éditeur, et lisez-le. Essayez de comprendre - dans la mesure du possible - sa structure, et son sens ! Ne vous découragez pas, quelques éléments seront compris plus tard.

Constatez que le code qui ne fait rien, est déjà assez volumineux, et il contient des éléments nommés comme __do_layout ou __set_properties, qui doivent être complétés par vos soins.

 

En principe il est possible de tasser plusieurs widgets dans le Frame principal, mais parfois il est utile d'y mettre un Panneau, un seul widget - fils, et placer les autres dedans, ce qui permet d'étendre la structure du Frame, sans écraser sa topologie.

La philosophie des sizers

Si vous voulez placer vos objets manuellement, et leur attribuer des dimensions quelconques, wxGlade n'est pas un bon choix, car il vous force à profiter des sizers, d'avoir la topologie assez standardisée du positionnement et des dimensions des widgets.

Chaque conteneur : Frame, Panneau, dialogue, etc., peut se voir attribué un sizer - un objet "virtuel", invisible, qui contrôle le positionnement des composantes de son conteneur. Des nouveaux widgets ont une "double appartenance" : ils sont attachés à la surface du conteneur, et placés dedans, mais aussi ils sont sous le contrôle du sizer, qui les positionne.

Le sizer le plus primitif est un "box sizer", qui tasse les composantes de manière contiguë, soit horizontalement, soit verticalement.

hiera0 (1K) (Le sizer créé par wxGlade pour un Frame est vertical, mais manuellement vous êtes totalement libre d'en créer un autre, ou aucun).

 

Manuellement on n'est nullement obligé d'utiliser les sizers, mais wxGlade vous force la main (vous ne pouvez pas insérer un bouton dans un Panel, seulement dans un Sizer qui y est attaché). Considérez ceci comme une contrainte qui vous empêche de faire des architectures bizarres.

Construisons une interface composite, avec deux panneaux principaux positionnés verticalement. Le supérieur aura deux boutons, l'inférieur se divisera en deux, mais placés horizontalement : le gauche sera le canevas, et le droit contiendra quelques widgets classiques, comme des boutons, etc.

Commençons par ajouter un slot au sizer vertical principal attaché au Frame. Cliquez sur le sizer sur la fenêtre des hiérarchies avec le bouton droit. Vous pouvez qinsi insérer deux panneaux ; changez leur couleurs de fond afin ne pas les confondre. hiera1 (1K) Dans le panneau supérieur ajoutez un Grid Sizer (pour les boutons). La situation devra se présenter plus ou moins comme sur les images.

prevsl0 (2K) Si vous faites - à présent - une prévisu­alisation, la fenêtre sera minuscule ; il faut remplir les panneaux, et avant la fin figer la taille du Frame (éventuellement du panneau qui servira de canevas). L'étape suivante est d'insérer en haut deux boutons et un spacer pour forcer la séparation des boutons.

 

Ensuite nous ajouterons un sizer horizontal au panneau inférieur, un panneau canevas à gauche, et n'importe quoi à droite. hiera2 (3K) Il vous faudra travailler soigneusement avec la fenêtre des propriétés, les rubriques "Layout", et "Widget", où vous pourrez preciser comment l'espace du sizer-propriétaire est reparti entre les descendant (proportion : 0 signifie la taille minimale, qui n'augmente pas) ; positionnement, possibilité de remplir l'espace (wxEXPAND) disponible ; couleurs, etc.

prevsl1 (3K) L'usage d'un construc­teur des interfaces ne vous libère pas de la nécessité de lire la documentation !

Voici la topologie de l'interface construite en accord avec la hiérarchie ci-dessus. Vous pouvez aller à la ligne "Application" et demander la création du code.
Lisez ce code ! Il vous faudra ajouter un document avec votre "modèle" d'application, et aussi ajouter votre code directement sous wxGlade.

 

Jetez aussi un coup d'oeil sur le fichier .wxg la représentation style XML de votre interface. (Mais vous n'allez pas travailler avec).

Les événements

Le code produit automatiquement par le constructeur ne sert qu'à afficher les widgets. Lors de la prévisualisation aucun widget ne marche.

Attachons à un bouton un callback, par exemple le bouton 3 doit détruire l'interface (et arrêter tout, comme la case de fermeture). Sous l'onglet "Events" du bouton tapez par ex. Stop. Le code généré aura dans le Frame la méthode :

  def Stop(self, event): # wxGlade: MyFrame.<event_handler>
    print "Event handler `Stop' not implemented!"
    event.Skip()
Si vous lancez l'application dans un contexte permettant de surveiller le flot de sortie standard, vous constaterez que le bouton "marche". Cependant, insérer le code souhaité, par ex. self.Destroy() est à vous. Attention. Si vous redemandez la sauvegarde du fichier .py, la réaction du système dépend de l'activation (ou non) de la clause "Overwrite existing sources" dans l'onglet "Application" de la fenêtre des propriétés. Vous pouvez détruire votre travail !

 

En général, wxGlade est très mal documenté (bien sûr, puisqu'il n'évolue pas, exactement comme les autres builders gratuits...), et plusieurs attributs du paquetage sont à découvrir expérimentalement. Vérifiez quand même ces quelques références - tutoriaux.

Ce tutoriel consacré à wx discute plusieurs éléments que vous pouvez / devez insérer manuellement dans votre code.


Une réalisation complète

Construisons une application complète, qui fait quelque chose. manda0 (4K) Ce sera un simple programme graphique, qui dessine une mandala, basée sur l'algorithe suivant : la plume trace un cercle, incrémentalement, segment par segment. Cependant, simultanément le centre de ce cercle bouge, et parcourt une trajectoire circulaire.

Comme dans le cas du tutoriel sur la programmation événementielle, nous essairons de maintenir les actions fondamentales de l'application indépendentes de l'interface. Le rôle de l'interface est le lancement et l'arrêt de la boucle, et la paramétrisation de l'application. Les paramètres seront variés, afin de vous donner l'idée comment contrôler des différents widgets.

 

Nous pourrons gérer le rapport entre les rayons des deux cercles (une valeur peut être absolue, de manière à ce que la mandala reste à l'intérieur du canevas). Ensuite, le $\Delta \phi$, l'incrément de l'angle qui détermine la longueur d'un segment tracé, et $\Delta \theta$ - le déplacement angulaire du centre de cercle - trajectoire, après un segment.

mandal0 (3K) À cela nous pouvons ajouter la largeur variable de la plume et sa couleur. Il faut prévoir au moins trois boutons : Start, Stop, et Clear, et quelques widgets d'ajustement numérique. Donc, le parametrage est assez riche !
Le point de départ ne doit jamais être l'interface, mais l'application. Son "noyau" est un point matériel qui bouge. Si possible, le mouvement doit se dérouler dans un espace abstrait, qui n'a rien à voir avec la fenêtre d'interfaçage.

from math import *
class Mand(object):
  tlr=0.9  # R. max. de la courbe ; (1 : taille du canevas)
  def __init__(self,dph=0.1,dte=0.01, ff=1.0):
    self.dph, self.dte = dph,dte
    self.ff = ff
    self.interf=None   # Pour l'instant...
    self.adjust()

 

Ici dph=$\Delta \phi$, dte=$\Delta\theta$, ff est le rapport des rayons, et interf - le Frame qui sera attachée au modèle pour tracer la courbe. La méthode adjust() ajuste la géométrie.
def adjust(self):
    self.running=False
    self.cot,self.sit = cos(self.dte),sin(self.dte)
    self.cop,self.sip = cos(self.dph),sin(self.dph)
    self.RR=self.tlr/(1.0+self.ff)
    self.r=self.RR*self.ff
    self.cx,self.cy = self.RR,0.0
    self.x0,self.y0 = self.cx + self.r, 0.0
    self.x,self.y = self.x0,self.y0
Elle calcule les sinus et les cosinus des angles incrémentaux afin d'accélerer la procédure de rotation, et calcule les rayons: RR est le rayon de l'orbite globale, du mouvement circulaire du centre (cx,cy)de l'orbite locale, et r - le rayon local du cercle tracé. Le centre du système entier est figé à (0,0).

Le mouvement consiste à faire tourner le point autour de (cx,cy), et ensuite de le tourner encore une fois, avec (cx,cy), autour de zéro. Voici deux fonctions globales, qui effectuent des rotations : absolue, et relative :

 

def turn(x,y,co,si):
    return (x*co-y*si,x*si+y*co)        
def trnab(x,y,cx,cy,co,si):
    x,y=turn(x-cx,y-cy,co,si)
    return(x+cx,y+cy)
Voici le reste du code :
  def move(self):
    self.x0,self.y0 = self.x,self.y
    x,y=trnab(self.x,self.y,self.cx,self.cy,self.cop,self.sip)
    self.cx,self.cy=turn(self.cx,self.cy, self.cot,self.sit)
    self.x,self.y = turn(x,y, self.cot,self.sit)
  def run(self):
    self.running=True
    while self.running:
      self.move()
      self.plot()
  def stop(self):
    self.running=False
  def plot(self):
    # print self.x,self.y    # Pour les tests sans interface
    self.interf.line(self.x0,self.y0,self.x,self.y)

 

L'application est terminée. Passons au document - interface, qui importera cette application. Le Frame se verra attribué la variable d'instance : model de classe Mand, et passera au modèle soi-même en tant qu'interf.

Mais nous reviendrons à l'application, pour y ajouter des dispositifs de paramétrage !

La construction d'une application complète avec l'interface créée avec un builder peut être délicate, car quand le code est généré automatiquement, il est assez long, et il faut s'orienter dedans, afin de le modifier. Ici la modularisation marche mal. En tout cas, évitez de placer un code quelconque entre les commentaires :

# begin wxGlade: ...
...
# end wxGlade
car les corrections pourront effacer votre travail. Il y a un certain nombre de règles de bon sens à respecter.

 

mand_int (3K) Voici notre tentative de faire l'interface avec un raisonnable ensemble de composantes. La hiérarchie correspond à la prévisualisation. Mais vous ne voyez pas tout, des innombrables propriétés : alignement, noms, attributs wxXXXX, etc. Il faut regarder la fenêtre des propriétés, après avoir chargé le fichier mandal.wxg, la structure de l'interface.
Ici vous ne voyez pas - par exemple, que le canevas est une souclasse privée (Surface) de wxPanel. Vous ne voyez pas l'attribution des gestionnaires des événements aux boutons, et autres widgets. Mais vous pouvez déjà demander la génération du code, et commencer à rédiger la sémantique de l'interfaçage. Il s'agit des callbacks, des propriétés du canevas/DC (la plume, etc.), de la méthode line, des menus (le choix de la couleur et une boîte d'information sur l'application, etc.).

L'interaction

Pour chaque widgets en question il faut déclarer - dans la rubrique "Events" des propriétés, le nom du gestionnaire. wxGlade ajoute automatiquement sa déclaration temporaire qui ne fait rien. Après la génération du code il faudra corriger cette méthode.

 

La liaison bilatérale entre l'interface et l'application se fait gràce au code suivant, dans la classe du Frame :
    def __init__(self, model, *args, **kwds):
        self.model=model
        model.interf=self
        ...
où le modèle est une instance de la classe Mand de notre application. L'interface est le programme principal, et déclare
from mandapp import *
...
mdl=Mand()
mandala = wx.PySimpleApp(0)
...
fenetre = MyFrame(mdl,None, -1, "") #Pars pour la superclasse
En principe on pourrait "libérer" le modèle d'accéder via interf à l'interface, mais ainsi l'interface devrait implémenter la boucle d'animation, ce qui est peu naturel. Avec les références à deux sens on peut partager le code de manière plus propre, et l'application peut appeler la méthode line pour l'affichage..

 

Les boutons. La case à cocher "Slow"

La structure événementielle des boutons à déjà été discutée, ces sont des fonctions à deux paramètres, self, et l'événement, souvent ignoré. Voici nos boutons
  def Start(self, event):
    print "Starting..."
    self.canvas.SetGeom()
    self.model.run()  # La boucle d'animation

  def Stop(self, event):
    print "Stopping..."
    self.model.stop()

  def Clear(self, event):
    dc=wx.ClientDC(self.canvas)
    dc.Clear()
Le canvas est le panneau principal du Frame, une instance de notre classe Surface, sous-classe de wx.Panel. La classe est construite automatiquement, mais il faut la compléter. Elle déclare parmi d'autres :

 

  def SetGeom(self):
    w,h = self.GetSizeTuple() # Largeur et hauteur
    self.w,self.h = w,h
    self.cx,self.cy=w/2.0,h/2.0  # Le CENTRE
Il ne faut pas essayer de mettre ces constructions dans l'initialisation, car il faut que ceci soit exécuté quand la fenêtre est déjà affichée. Ceci est une erreur assez fréquente - tentatives de gérér la géométrie de l'interface avant son affichage.
La gestion de la case à cocher et de quelques autres widgets (comme SpinCtrl appliqué à la gestion de la largeur de la plume) est simple :
  def Slow(self, event):
    self.delay=self.slow.GetValue()
La méthode GetValue() est assez universelle, implémentée par plusieurs widgets. La variable delay est utilisée dans la procédure d'affichage line.

La procédure d'affichage

Elle n'est pas très économique, mais trop d'optimisation précoce est nuisible pour la lisibilité. Aussi, il ne faut pas tenter de stocker statiquement le Device context.

 

  def line(self,x0,y0,x,y):
    cnv=self.canvas
    pen=cnv.pen
    dc=wx.ClientDC(cnv)
    dc.SetPen(pen) # Peut être plus persistant...
    cx,cy=cnv.cx,cnv.cy
    x0,y0=cx*(1+x0),cy*(1+y0) # Conversion géométrique
    x,y = cx*(1+x), cy*(1+y)
    dc.DrawLine(x0,y0,x,y)
    if self.delay: wx.MilliSleep(2)
    wx.Yield()
La gestion de la largeur de la plume est relativement triviale :
  def WidPen(self, event):
    v=self.width.GetValue()
    pen=self.canvas.pen
    pen.SetWidth(v)
    # self.Clear(event)    # si vous voulez...
La gestion des paramètres numériques dans les petits formulaires textuels (TextCtrl) est un peu plus compliqué, et sera décrite séparemment.

 

Quelques éléments du menu

Voici trois callbacks, montrant l'usage des dialogues stardard du wxPython. D'abord l'arrêt du programme par le menu, qui peut faire aussi un peu plus de nettoyage que la case de fermeture.
  def Exit(self, event):
    self.model.stop()
    self.Destroy()
        
  def SetCol(self, event):   # Couleur
    dlg = wx.ColourDialog(self)
    dlg.GetColourData().SetChooseFull(True) # Technique...
    if dlg.ShowModal() == wx.ID_OK:
        data = dlg.GetColourData()
        c=data.GetColour()
        pen=self.canvas.pen
        pen.SetColour(c)
    dlg.Destroy()
Le gestionnaire "About" est similaire, on y place ce que l'on veut.

 

        
  def About(self, event):
    info = wx.AboutDialogInfo()
    info.Name = "Mandala"
    info.Version = "0.1"
    info.Copyright = "(C) 2010 JK"
    info.Description = 
      "A simple composition of two\n circular movements"
    wx.AboutBox(info)
On n'a pas besoin de détruire la boîte explicitement.

Formulaires d'entrée textuelle

Ce sont des widgets de contrôle plus compliqués, car on peut interagir avec de manière riche, taper le texte, valider avec la touche Enter (ou pas), etc. De plus, dans notre cas l'interaction de l'interface avec l'application est plus complexe, car change les propriétés de l'animation, et la géométrie du modèle (et pas seulement de l'interface).

Le gestionnaire principal ici concerne l'événement EVT_TEXT_ENTER. C'est la validation du texte entrée. Nous allons traiter le cas de manière simplifiée, sans vérifier la validité du contenu, mais normalement il faut être plus vigilant.

 

Important. Il faut - sous l'onglet Widget des propriétés - cocher la case wxTE_PROCESS_ENTER, sinon "Enter" sera considéré comme tabulation.

Le callback en question, prenons Dphi, est :

  def Dphi(self, event): 
    print "Changing Dphi!"
    self.Stop(event)
    self.Clear(event)       
    v=self.dphi.GetValue() # Texte !
    v=float(v)
    self.model.setDph(v)
    # event.Skip()  # Un autre gestionnaire?
    # self.Start(event) # Ne faites pas ça !
L'application devra réagir, nous codons dans Mand :
  def setDph(self,v):
    self.dph=v
    self.adjust()
... et la procédure adjust recalcule la géométrie. Vous pouvez aisément construire d'autres gestionnaires similaires.

 

Notre tutoriel s'approche à la fin. Le dernier point est la réponse à la question pourquoi après avoir changé les paramètres de la géométrie, on ne doit pas redémarrer l'application automatiquement? Pourtant, quand on change la largeur de la plume pendant l'exécution, rien de mauvais ne se passe.

La réponse est : mon programme est relativement primitif, on devrait faire mieux, mais ceci demanderait le refactoring assez complet de la couche animation. Le reste est un sujet avancé et difficile !

Quand on change les paramètres principaux, la géométrie de l'application, on doit de préférence arrêter la boucle d'animation, afin de ne pas avoir des propriétés incongrues. Ici ce n'est pas trop gênant, mais en général, si. Cependant on ne peut "arrêter une boucle" de l'extérieur. La commande Stop désactive la variable running, et la boucle s'arrête "par sa volonté".
Vous devez comprendre bien, qu'en absence des threads, la Mainloop ne tourne pas en permanence, seulement quand l'application démarrée par le callback Start retourne. Mais, puisque nous voulons contrôler l'application via l'interface pendant l'animation, l'application lance de temps en temps wxYield(), ce qui appelle récursivement la boucle événementielle. Elle doit traiter vite tous les événéments en attente, et retourner à l'application (en fin de la procédure line). Si l'événement traité change quelque chose sans arrêter l'animation, ou l'arrête tout court sans la démarrer, après le retour vers l'application, le système continue à travailler sans rien de particulier.

 

Cependant, imaginez que le callback déclenché par Mainloop après le Yield(), démarre la boucle, lance run, qui appelle line, qui lance Yield()...
D'abord, vous vous mettez dans une descente récursive qui empilera les instances de Mand.run(...), et risque de provoquer des dégâts. Ceci n'arrivera pas, car quand Yield() est appelé récursivement, un mécanisme de contrôle proteste. Vous aurez une erreur d'exécution, qui, d'ailleurs n'arrête pas l'application, mais parfois peut la bloquer.


Quelle est la solution du dilemme?

Bien sûr, des techniques mixtes sont aussi possibles.

 

Sujets non-traités

Typiquement - comme dans notre tutoriel -, il faudrait discuter simultanément la manipulation des objets à travers le wxGlade, et les techniques de programmation en wxPython. Trop de choses... Par exemple :

Il ne faut oublier non plus d'autres styles de construction des interfaces. wxGlade construit un grand fichier avec le code wx, qui instaure les widgets. Mais PythonCard fait un dictionnnaire Python, avec la description complète de l'interface visuelle, et offre à l'utilisateur un module prédéfini qui lit ce dictionnaire, et l'interprète, traçant les fenêtres, boutons, etc. Autres builders font un fichier en XRC (variante de XML ; voir aussi...) à la place du dictionnaire, et vos programmes peuvent le lire. (Un concept similaire est à la base de la technologie XUL).

Une standardisation serait utile, depuis 10 ans, mais le progrès est lent.       FIN.