D�velopper des Applications Gnome avec Python (Partie 3)

ArticleCategory:

Software Development

AuthorImage:

Hilaire Fernandes

TranslationInfo:[Author + translation history. mailto: or http://homepage]

original in fr Hilaire Fernandes

AboutTheAuthor

Hilaire Fernandes est le vice-pr�sident d'OFSET, une organisation pour promouvoir le d�veloppement de logiciels �ducatifs libres pour le bureau Gnome. Il a aussi �crit Dr. Geo, un logiciel prim� de g�om�trie dynamique, et il est actuellement occup� avec Dr. Genius un autre logiciel �ducatif de math�matiques pour le bureau Gnome.

Abstract:

Cette s�rie d'articles est sp�cialement �crite pour des d�butants en programmation sous Gnome et GNU/Linux. Le langage de d�veloppement choisi, Python, �vite la surcharge habituelle avec des langages compil�s comme le C. Avant d'�tudier cet article quelques notions de programmation sous Python sont n�cessaires. Plus d'informations sur Python et Gnome sont disponbibles aux adresses http://www.python.org et http://www.gnome.org.

Articles pr�c�dents dans la s�rie :
premier article
second article

ArticleIllustration:

Gnome

ArticleBody:

Needed tools

Pour les besoins logiciels � l'ex�cution du programme d�crit dans cet article, vous pouvez vous r�f�rer � la liste de la m�me rubrique de la partie I de cette s�rie d'articles.

Aussi vous aurez besoin :

Pour l'installation et l'utilisation de Python-Gnome et LibGlade vous pouvez aussi vous r�f�rer � la partie I.


Mod�le de d�veloppement des exercices

Lors de la derni�re partie nous avions mis en place l'interface utilisateur -- Drill -- devant servir de cadre pour le d�ploiement d'exercices. Cette fois-ci, nous allons nous int�resser de plus pr�s au mod�le de d�veloppement interne des exercices devant s'ins�rer dans Drill. Ce sera l'occasion d'explorer plus en d�tail les possibilit�s de d�veloppement orient� objet du langage Python. Pour l'essentiel, cette partie traitera donc plus de d�veloppement Python pur que de d�veloppement Gnome en Python.

La derni�re fois, j'avais laiss� un exercice pratique en suspens. � savoir la transformation du petit jeu de couleur, r�alis� lors de la premi�re partie de cette s�rie d'articles, en un exercice � ins�rer dans Drill. Nous nous servirons de cet exemple pour illustrer notre expos� et, par la m�me occasion, nous donnerons une solution � cette exercice.

Le d�veloppement orient� objet

En quelques mots et sans pr�tention d'�tre exhaustif, le d�veloppement objet s'attache � caract�riser et cat�goriser -- en g�n�ral -- par des relations du type est-une-sorte-de des objets du monde r�el ou non. Cela peut �tre per�u comme une conceptualisation de ces objets par rapport � une probl�matique � laquelle nous nous int�ressons. Nous pouvons les comparer dans d'autres domaines, aux cat�gories d'Aristote, aux taxinomies ou ontologies. Dans tout ces cas il s'agit bien d'appr�hender, par une r�duction conceptuelle, une situation complexe. Ce mod�le de d�veloppement aurait aussi bien pu s'appeler d�veloppement orient� cat�gorie.

Dans ce mod�le de d�veloppement, les objets manipul�s par le programme, ou constituant le programme, sont appel�s des classes et des repr�sentants de ces objets conceptuels des instances. Les objets sont caract�ris�s par des attributs (des valeurs en g�n�ral) et des m�thodes. Les objets peuvent ne pas �tre totalement caract�ris�s, dans ce cas nous parlons de classes abstraites, c'est par exemple le cas lorsqu'une m�thode est d�clar�e mais non d�finie (nous parlons de m�thode virtuelle pure, le corps de la m�thode est vide). Pour cr�er une instance d'une classe, celle-ci ne doit pas �tre abstraite. Les classes abstraites permettent de sp�cifier la forme prise par les classes h�riti�res. Classes dans lesquelles les m�thodes virtuelles seront d�finies. Les classes sont rang�es entre elles par une relation du type est-une-sorte-de, dite relation d'h�ritage, nous parlons dans ce cas de classe(s) parente(s) d'une classe donn�e.

Selon les langages, il existe une plus o� moins grande finesse dans la caract�risation des objets. Cependant le plus petit d�nominateur commun semble �tre celui-ci :

  1. H�ritage des attributs et des m�thodes de la classe parente par la classe h�riti�re.
  2. Dans une classe h�riti�re, possibilit� de surcharger les m�thodes h�rit�es de la classe parente (i.e. red�finir une m�thode h�rit�e).
  3. Polymorphisme, une classe donn�e peut avoir plusieurs classes parentes.

Python et le d�veloppement orient� objet

En ce qui concerne Python, c'est ce plus petit d�nominateur commun qui a �t� choisi. Cela permet de s'initier au d�veloppement objet sans se perdre dans les d�tails de ce type de d�veloppement.

En Python, les m�thodes d'un objet sont toujours virtuelles. Cela signifie qu'elles peuvent toujours �tre surcharg�es par une classe h�riti�re -- ce que nous souhaitons faire en g�n�ral en d�veloppement objet -- cela simplifie l�g�rement la syntaxe mais ne permet pas de distinguer rapidement ce qui est effectivement surcharg� de ce qui ne l'est pas. Ensuite il n'est pas possible de rendre obscur un objet, c'est � dire rendre impossible l'acc�s � des attributs ou m�thodes depuis l'ext�rieur de l'objet. Les attributs d'un objet Python sont accessibles aussi bien en lecture qu'en �criture depuis l'ext�rieur de l'objet.

La classe parente exercice

Dans notre exemple (voir le fichier templateExercice.py , nous souhaitons caract�riser des objets de type exercice. Nous d�finissons donc naturellement un objet de type exercice. Cet objet sert de base conceptuel aux autres types d'exercices que nous cr�erons par la suite. L'objet exemple est la classe parente de tous les autres types d'exercices cr��s. Ces types d'exercices auront ainsi au minimum les m�mes attributs et m�thodes que la classe exercice. Ce minimum commun nous permettra de manipuler identiquement toutes les instances d'exercices, m�me dans leur plus grande diversit�, quelque soit l'objet dont ils sont une instance.

Par exemple, pour cr�er une instance de la classe exercice nous pourrions �crire :
from templateExercice import exercice

monExercice = exercice ()
monExercice.activate (ceWidget)

En fait il n'y a pas d'int�r�t � cr�er des instance de la classe exercice car elle n'est qu'un mod�le � partir duquel d'autre classes sont d�riv�es.

Les attributs

Si nous devions nous int�resser � d'autres aspects d'un exercice nous pourrions lui ajouter des attributs. Je pense par exemple au score sur un exercice, au nombre de fois qu'il a �t� fait, etc.

Les m�thodes

En terme de code Python cela donne la chose suivante :
class exercice:
    "A template exercice"
    exerciceWidget = None
    exerciceName = "No Name"
    def __init__ (self):
        "Create the exericice widget"
    def activate (self, area):
        "Set the exercice on the area container"
        area.add (self.exerciceWidget)
    def unactivate (self, area):
        "Remove the exercice fromt the container"
        area.remove (self.exerciceWidget)
    def reset (self):
        "Reset the exercice"

Ce code est inclus dans son propre fichier templateFichier.py, cela nous permet de clarifier les r�les sp�cifiques de chaque objet. Les m�thodes sont d�clar�es � l'int�rieur de la classe exercice, ce sont en fait des fonctions.

� propos de l'argument area, nous verrons par la suite que c'est une r�f�rence d'un widget GTK+ construit par LibGlade, c'est une fen�tre avec ascenseurs.

Dans cet objet, les m�thodes __init__ et reset sont vides, elle seront surcharg�es par des classes h�riti�res si n�cessaire.

labelExercice, premier exemple d'h�ritage

Cet exercice est presque un exercice vide. Il ne fait qu'une chose : afficher le nom de l'exercice dans la zone exercice de Drill. Il nous sert de pis-aller pour les exercices qui peuplent l'arbre de gauche de Drill mais qui ne sont pas encore cr��s.

Comme pour l'objet exercice, l'objet labelExercice est plac� dans son propre fichier, labelExercice.py. Ensuite, �tant donn� que cet objet est un h�ritier de l'objet exercice, nous avons besoin de lui indiquer les d�finitions de ce dernier. Cela ce fait simplement par une importation :

from templateExercice import exercice

Cela signifie litt�ralement que la d�finition de la classe exercice qui est dans le fichier templateExercice.py est import�e dans le code courant.

Nous arrivons maintenant � l'aspect le plus important, la d�claration de la classe labelExercice en tant que classe h�riti�re de exercice. Lors de la d�claration de labelExercice, cela se fait de la fa�on suivante :

class labelExercice(exercice):

Voil�, cela suffit pour que labelExercice h�rite de tous les attributs et toutes les m�thodes de exercice.

Bien s�r il nous reste du travail � faire, en particulier initialiser le widget de l'exercice. Nous le faisons en surchargeant la m�thode __init__ (i.e. en la red�finissant dans la classe labelExercice), celle-ci est appel�e lorsqu'une instance est cr��e. Aussi ce widget devra �tre r�f�renc� dans l'attribut exerciceWidget, de cette fa�on nous n'aurons pas besoin de surcharger les m�thodes activate et unactivate de la classe exercice.

    def __init__ (self, name):
        self.exerciceName = "Un exercice vide"
        self.exerciceWidget = GtkLabel (name)
        self.exerciceWidget.show ()

C'est la seule m�thode que nous surchargeons. Pour cr�er une instance de labelExercice il suffit de faire l'appel :

monExercice = labelExercice ("Un exercice qui ne fait rien")

Pour acc�der � ses attributs ou ses m�thodes :

# Le nom de l'exercice
print monExercice.exerciceName

# Placer le widget de l'exercice dans le container "area"    
monExerice.activate (area)

colorExercice, deuxi�me exemple d'h�ritage

Ici nous abordons la transformation du jeu de couleur, vu dans le premier article de cette s�rie, en une classe de type exercice, plus pr�cis�ment nous nommons cette classe colorExercice, il est plac� dans son propre fichier colorExercice.py dont le code source complet est en annexe de cet article.

Par rapport au code source initial, il s'agit essentiellement d'une redistribution des fonctions et variables en m�thodes et attributs dans la classe colorExercice.

Les variables globales sont transform�es en attributs d�clar�s au d�but de la classe :

class colorExercice(exercice):
    width, itemToSelect = 200, 8
    selectedItem = rootGroup = None
    # to keep trace of the canvas item
    colorShape = []

Comme pour la classe labelExercice, la m�thode __init__ est surcharg�e pour contenir la construction des widgets de l'exercice :

    def __init__ (self):
        self.exerciceName = "Le jeu de couleur"
        self.exerciceWidget = GnomeCanvas ()
        self.rootGroup = self.exerciceWidget.root ()
        self.buildGameArea ()
        self.exerciceWidget.set_usize (self.width,self.width)
        self.exerciceWidget.set_scroll_region (0, 0, self.width, self.width)
        self.exerciceWidget.show ()

Rien de nouveau par rapport au code initial, si ce n'est que le GnomeCanvas est r�f�renc� dans l'attribut exerciceWidget.

L'autre m�thode surcharg�e est reset, elle remet � z�ro le jeu, elle doit donc �tre sp�cialis�e au jeu de couleur :

    def reset (self):
        for item in self.colorShape:
            item.destroy ()
        del self.colorShape[0:]
        self.buildGameArea ()

Les autres m�thodes sont la transcription directe des fonctions, avec en plus l'utilisation de la variable self pour acc�der aux attributs et m�thodes de l'instance. Il existe juste une exception dans les m�thodes buildStar et buildShape o� le param�tre d�cimal k a �t� remplac� par un param�tre entier. J'ai not� un comportement �trange dans le document colorExercice.py o� les nombres d�cimaux saisis dans le code source sont tronqu�s. Ce probl�me semble �tre li� au module gnome.ui et au locale fran�ais (o� les nombres d�cimaux ont leur partie enti�re et leur partie d�cimale d�limit�es par une virgule et non un point). Je t�cherai de trouver la source du probl�me d'ici le prochain article.

Derniers ajustements dans Drill

Nous avons deux types d'exercice -- labelExercice et colorExercice. Nous en cr�ons des instances depuis les fonctions addXXXXExercice dans le code drill1.py. Les instances sont r�f�renc�es dans un dictionnaire exerciceList dont les cl�s sont �galement stock�es comme arguments des feuilles de chaque exercice dans l'arbre de gauche :

def addExercice (category, title, id):
    item = GtkTreeItem (title)
    item.set_data ("id", id)
    category.append (item)
    item.show ()
    item.connect ("select", selectTreeItem)
    item.connect ("deselect", deselectTreeItem)
[...]    
def addGameExercice ():
    global exerciceList
    subtree = addSubtree ("Jeux")
    addExercice (subtree, "Couleur", "Games/Color")
    exerciceList ["Games/Color"] = colorExercice ()

La fonction addGameExercice cr�e, par l'appel � la fonction addExercice une feuille dans l'arbre avec comme attribut id="Games/Color", ce m�me attribut est utilis� comme cl� de l'instance de l'exercice couleur -- cr��e par la commande colorExercice() -- dans le dictionnaire exerciceList.

Ensuite, et c'est l� toute l'�l�gance du polymorphisme dans le d�veloppement orient� objet, nous pouvons manipuler, depuis les fonctions de traitement qui utilisent les diff�rents objets exercices, les exercices quelque soit leur architecture interne. Seules les m�thodes d�finies dans la classe virtuelle de base exercice sont utilis�es, et elles font, par exemple, des choses diff�rentes dans chaque classe colorExercice ou labelExercice. Le programmeur "parle" � tous les exercices de la m�me fa�on, m�me si ces exercices sont un peu diff�rents. Pour ce faire nous combinons � la fois l'utilisation de l'attribut id des feuilles de l'arbre et le dictionnaire exerciceList ou la variable exoSelected qui r�f�rence l'exercice en cours d'utilisation. �tant donn� que tous les exercices sont des h�ritiers de la classe exercice, nous utilisons ses m�thodes comme autant de point de contr�le des exercices, dans toutes leurs vari�t�s.

def on_new_activate (obj):
    global exoSelected
    if exoSelected != None:
        exoSelected.reset ()

def selectTreeItem (item):
    global exoArea, exoSelected, exerciceList
    exoSelected = exerciceList [item.get_data ("id")]
    exoSelected.activate (exoArea)

def deselectTreeItem (item):
    global exoArea, exerciceList
    exerciceList [item.get_data ("id")].unactivate (exoArea)


Fig. 1 - Fen�tre principale de Drill, avec l'exercice couleur

Cela cl�t ici notre article. Nous avons donc d�couvert les attraits du d�veloppement orient� objet en Python dans le cadre d'application avec interface graphique. Dans les prochains articles nous continuerons la d�couverte des widgets Gnome � travers la r�alisation de nouveaux exercices que nous ins�rerons dans Drill.

Appendice: Le source complet

drill1.py
#!/usr/bin/python
# Drill - Teo Serie
# Copyright Hilaire Fernandes 2001
# Release under the terms of the GPL licence
# You can get a copy of the license at http://www.gnu.org


from gnome.ui import *
from libglade import *

# Import the exercice class
from colorExercice import *
from labelExercice import *

exerciceTree = currentExercice = None
# The exercice holder
exoArea = None
exoSelected = None
exerciceList = {}
 
def on_about_activate(obj):
    "display the about dialog"
    about = GladeXML ("drill.glade", "about").get_widget ("about")
    about.show ()
    
def on_new_activate (obj):
    global exoSelected
    if exoSelected != None:
        exoSelected.reset ()

def selectTreeItem (item):
    global exoArea, exoSelected, exerciceList
    exoSelected = exerciceList [item.get_data ("id")]
    exoSelected.activate (exoArea)

def deselectTreeItem (item):
    global exoArea, exerciceList
    exerciceList [item.get_data ("id")].unactivate (exoArea)

def addSubtree (name):
    global exerciceTree
    subTree = GtkTree ()
    item = GtkTreeItem (name)
    exerciceTree.append (item)
    item.set_subtree (subTree)
    item.show ()
    return subTree

def addExercice (category, title, id):
    item = GtkTreeItem (title)
    item.set_data ("id", id)
    category.append (item)
    item.show ()
    item.connect ("select", selectTreeItem)
    item.connect ("deselect", deselectTreeItem)
    

def addMathExercice ():
    global exerciceList
    subtree = addSubtree ("Math�matiques")
    addExercice (subtree, "Exercice 1", "Math/Ex1")
    exerciceList ["Math/Ex1"] = labelExercice ("Exercice 1")
    addExercice (subtree, "Exercice 2", "Math. Ex2")
    exerciceList ["Math/Ex2"] = labelExercice ("Exercice 2")

def addFrenchExercice ():
    global exerciceList
    subtree = addSubtree ("Fran�ais")
    addExercice (subtree, "Exercice 1", "French/Ex1")
    exerciceList ["French/Ex1"] = labelExercice ("Exercice 1")
    addExercice (subtree, "Exercice 2", "French/Ex2")
    exerciceList ["French/Ex2"] = labelExercice ("Exercice 2")

def addHistoryExercice ():
    global exerciceList
    subtree = addSubtree ("Histoire")
    addExercice (subtree, "Exercice 1", "Histoiry/Ex1")
    exerciceList ["History/Ex1"] = labelExercice ("Exercice 1")
    addExercice (subtree, "Exercice 2", "Histoiry/Ex2")
    exerciceList ["History/Ex2"] = labelExercice ("Exercice 2")

def addGeographyExercice ():
    global exerciceList
    subtree = addSubtree ("G�ographie")
    addExercice (subtree, "Exercice 1", "Geography/Ex1")
    exerciceList ["Geography/Ex1"] = labelExercice ("Exercice 1")
    addExercice (subtree, "Exercice 2", "Geography/Ex2")
    exerciceList ["Geography/Ex2"] = labelExercice ("Exercice 2")

def addGameExercice ():
    global exerciceList
    subtree = addSubtree ("Jeux")
    addExercice (subtree, "Couleur", "Games/Color")
    exerciceList ["Games/Color"] = colorExercice ()

    
def initDrill ():
    global exerciceTree, label, exoArea
    wTree = GladeXML ("drill.glade", "drillApp")
    dic = {"on_about_activate": on_about_activate,
           "on_exit_activate": mainquit,
           "on_new_activate": on_new_activate}
    wTree.signal_autoconnect (dic)           
    exerciceTree = wTree.get_widget ("exerciceTree")
    # Temporary until we implement real exercice
    exoArea = wTree.get_widget ("exoArea")
    # Free the GladeXML tree
    wTree.destroy ()
    # Add the exercice
    addMathExercice ()
    addFrenchExercice ()
    addHistoryExercice ()
    addGeographyExercice ()
    addGameExercice ()
    
initDrill ()
mainloop ()

templateExercice.py
# Exercice pure virtual class
# exercice class methods should be override
# when exercice class is derived
class exercice:
    "A template exercice"
    exerciceWidget = None
    exerciceName = "No Name"
    def __init__ (self):
        "Create the exericice widget"
    def activate (self, area):
        "Set the exercice on the area container"
        area.add (self.exerciceWidget)
    def unactivate (self, area):
        "Remove the exercice fromt the container"
        area.remove (self.exerciceWidget)
    def reset (self):
        "Reset the exercice"

labelExercice.py
# Dummy Exercice - Teo Serie
# Copyright Hilaire Fernandes 2001
# Release under the terms of the GPL licence
# You can get a copy of the license at http://www.gnu.org

from gtk import *
from templateExercice import exercice

class labelExercice(exercice):
    "A dummy exercie, it just prints a label in the exercice area"
    def __init__ (self, name):
        self.exerciceName = "Un exercice vide"
        self.exerciceWidget = GtkLabel (name)
        self.exerciceWidget.show ()

colorExercice.py
# Color Exercice - Teo Serie
# Copyright Hilaire Fernandes 2001
# Release under the terms of the GPL licence
# You can get a copy of the license at http://www.gnu.org

from math import cos, sin, pi
from whrandom import randint
from GDK import *
from gnome.ui import *

from templateExercice import exercice
       

# Exercice 1 : color game

class colorExercice(exercice):
    width, itemToSelect = 200, 8
    selectedItem = rootGroup = None
    # to keep trace of the canvas item
    colorShape = []
    def __init__ (self):
        self.exerciceName = "Le jeu de couleur"
        self.exerciceWidget = GnomeCanvas ()
        self.rootGroup = self.exerciceWidget.root ()
        self.buildGameArea ()
        self.exerciceWidget.set_usize (self.width,self.width)
        self.exerciceWidget.set_scroll_region (0, 0, self.width, self.width)
        self.exerciceWidget.show ()
    def reset (self):
        for item in self.colorShape:
            item.destroy ()
        del self.colorShape[0:]
        self.buildGameArea ()
    def shapeEvent (self, item, event):
        if event.type == ENTER_NOTIFY and self.selectedItem != item:        
            item.set(outline_color = 'white') #highligh outline
        elif event.type == LEAVE_NOTIFY and self.selectedItem != item:
            item.set(outline_color = 'black') #unlight outline
        elif event.type == BUTTON_PRESS:
            if not self.selectedItem:
                item.set (outline_color = 'white')
                self.selectedItem = item
            elif item['fill_color_gdk'] == self.selectedItem['fill_color_gdk'] \
                 and item != self.selectedItem:
                item.destroy ()
                self.selectedItem.destroy ()
                self.colorShape.remove (item)
                self.colorShape.remove (self.selectedItem)
                self.selectedItem, self.itemToSelect = None, self.itemToSelect - 1
                if self.itemToSelect == 0:
                    self.buildGameArea ()
        return 1    

    def buildShape (self,group, number, type, color):
        "build a shape of 'type' and 'color'"
        w = self.width / 4
        x, y, r = (number % 4) * w + w / 2, (number / 4) * w + w / 2, w / 2 - 2
        if type == 'circle':
            item = self.buildCircle (group, x, y, r, color)
        elif type == 'squarre':
            item = self.buildSquare (group, x, y, r, color)
        elif type == 'star':
            item = self.buildStar (group, x, y, r, 2, randint (3, 15), color)
        elif type == 'star2':
            item = self.buildStar (group, x, y, r, 3, randint (3, 15), color)
        item.connect ('event', self.shapeEvent)
        self.colorShape.append (item)

    def buildCircle (self,group, x, y, r, color):
        item = group.add ("ellipse", x1 = x - r, y1 = y - r,
                          x2 = x + r, y2 = y + r, fill_color = color,
                          outline_color = "black", width_units = 2.5)
        return item

    def buildSquare (self,group, x, y, a, color):
        item = group.add ("rect", x1 = x - a, y1 = y - a,
                          x2 = x + a, y2 = y + a, fill_color = color,
                          outline_color = "black", width_units = 2.5)
        return item

    def buildStar (self,group, x, y, r, k, n, color):
        "k: factor to get the internal radius"
        "n: number of branch"
        angleCenter = 2 * pi / n
        pts = []
        for i in range (n):            
            pts.append (x + r * cos (i * angleCenter))
            pts.append (y + r * sin (i * angleCenter))
            pts.append (x + r / k * cos (i * angleCenter + angleCenter / 2))
            pts.append (y + r / k * sin (i * angleCenter + angleCenter / 2))
        pts.append (pts[0])
        pts.append (pts[1])
        item = group.add ("polygon", points = pts, fill_color = color,
                          outline_color = "black", width_units = 2.5)
        return item

    def getEmptyCell (self,l, n):
        "get the n-th non null element of l"
        length, i = len (l), 0
        while i < length:
            if l[i] == 0:
                n = n - 1
            if n < 0:
                return i
            i = i + 1
        return i

    def buildGameArea (self):
        itemColor = ['red', 'yellow', 'green', 'brown', 'blue', 'magenta',
                     'darkgreen', 'bisque1']
        itemShape = ['circle', 'squarre', 'star', 'star2']
        emptyCell = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
        self.itemToSelect, i, self.selectedItem = 8, 15, None
        for color in itemColor:
            # two items of same color
            n = 2
            while n > 0:
                cellRandom = randint (0, i)
                cellNumber = self.getEmptyCell (emptyCell, cellRandom)
                emptyCell[cellNumber] = 1
                self.buildShape (self.rootGroup, cellNumber, 
		itemShape[randint (0, 3)], color)
                i, n = i - 1, n - 1