Après avoir abordé PyGTK et Glade dans mon premier tutoriel, j'ai décidé d'écrire un autre tutoriel plus complexe. Ne pouvant me décider sur le thème à aborder (Note : Si vous avez des suggestions pour d'autres tutoriels ou billets je se ravis que vous m'en touchiez deux mots), j'ai décidé de travailler à la création d'une application simple à partir de laquelle des sujets utiles seront dévoilés.

L'idée qui m'est venue (qui, heureusement, sera simple) est de créer un programme qui me permettra de faire le suivi des différents types de vin que je consomme ainsi que de connaître mes préférés. C'est quelque chose que je voulais écrire depuis une moment et je pense qu'il serait bien de faire cela avec PyGTK.

Le projet s'appellera PyWine. Les sources complètes et le fichier Glade de ce tutoriel sont stockés ici

Premièrement, démarrons une nouveau projet dans Glade, il sera enregistré dans un dossier PyWine. Créons une nouvelle fenêtre nommée "mainWindow" et modifions son titre en "PyWine". Puis nous ajoutons un gestionnaire sur le signal Destroy, exactement comme dans le premier tutorial (NdT : Créer des interfaces graphique avec PyGTK et Glade).

Ensuite, j'ajoute une Vertical Box à 4 rangées dans la fenêtre. Dans l'ordre, du haut vers le bas, nous aurons une rangée pour la barre de Menu, une pour la barre d'outils (Toolbar), une autre pour une liste arborescente (List or Tree View), et la dernière rangée contiendra une barre de status (Status bar). Le Tree View sera nommé "wineView". pywine_01.png

La liste arborescente que nous avons placé dans la fenêtre sera utilisée pour afficher les informations sur les vins. Donc, la première chose que nous voulons faire est de permettre à l'utilisateur d'ajouter un vin. Pour cela, nous utiliserons une option du menu ou un bouton de la barre d'outils.

Ajoutons un bouton dans la barre d'outils. Pour ce faire, il suffit de sélectionner Toolbar button dans la palette de Glade et de cliquer sur un bouton de la barre d'outils de notre fenêtre mainWindow. Ceci devrait poser un bouton à l'aspect étrange dans la barre d'outils. Editons les propriétés de ce bouton par le formulaire "Properties". Nommons le "tbAddwine", donnons lui le texte "Add Wine" et utilisons une icône Add. pywine_02.png

(NdT : Si vous ne trouvez pas Toolbar button dans la palette de Glade, c'est qu'il vous faut une version plus récente. Personnellement, j'utilise la version 2.10 de Glade).

Ajoutons maintenant un gestionnaire pour l'événement de clic sur le bouton tbAddWine, mais cette fois, changeons le nom par défaut "on_tbAddWine_clicked" en "on_AddWine".

La barre de menu

Travaillons maintenant sur le menu, comme nous voulons que l'utilisateur puisse aussi l'utiliser pour ajouter des éléments. Cliquons sur la barre de menu de notre fenêtre et réglons ses propriétés. Cliquez sur le bouton "Edit Menus..." pour modifier le contenu de la barre de menu.

Cliquez sur le bouton Add pour ajouter un nouvel élément dans le menu, et modifiez son label en "_Add". Ensuite, sélectionnez l'élément Add du menu et cliquez sur le bouton "Add Child". Ceci va créer un sous-menu dans le menu Add. Modifiez le label en "_Wine" et le gestionnaire en "on_AddWine".

Remarquez bien que le gestionnaire d'événement pour le menu Add->Wine et le bouton "Add" est le même "on_AddWine". C'est parce que ces deux widgets déclencheront la même action, ajouter un vin à notre liste.

pywine_03.png

Ce qui va apparaître quand l'utilisateur cliquera sur le bouton "Add Wine" ou l'option de menu Add->Wine, c'est un dialogue lui permettant de saisir les détails sur le vin. S'il choisit de valider le dialogue par "OK", alors son vin sera ajouté à la liste.

Création du dialogue

Bien, la chose à faire maintenant est de créer le dialogue d'ajout d'un nouveau vin. Pour cette exemple, nous resterons simple et l'utilisateur pourra saisir le nom du vin, son producteur, son année de mise en bouteille et la variété de raisin.

Pour créer un nouveau dialogue, il suffit de cliquer sur le bouton Dialog dans la palette de Glade. Ceci dervait afficher le formulaire de création de nouveau dialogue, choisissez l'option "Standard button layout" avec les boutons Cancel et OK. Modifiez le nom du dialogue en "wineDlg" et son titre en "Add Wine".

Nous utiliserons une table afin de gérer l'alignement des choses sur notre wineDlg. Ajoutez une table au dialogue (de la même manière que l'on ajoute un widget à une fenêtre) et réglez le nombre de rangées à 4 et le nombre de colonnes à 2. Ensuite, nous remplissons les emplacements de la table avec des Label et des Text Entry jusqu'à ce que notre dialogue ressemble à ceci : pywine_04.png

J'ai ajouté 3 pixels d'espacement entre les rangées de la table. Ce réglage se trouve dans les proriétés de la table. Si vous avez des difficultés pour sélectionner la table, il est facile d'afficher l'arborescence des widgets par le menu de Glade View->Show Widget Tree, et de cliquer sur la table. Vous pouvez aussi enfoncer la touche Shift pendant que vous cliquez sur sur le dialogue. Ceci permet de parcourir les widgets de manière cyclique.

Il faut maintenant nommer tous les widgets d'édition du dialogue : enWine, enWinery, enGrape, et enYear.

The Python Code

Bien, entrons dans le vif du sujet et faisons fonctionner le code, nous appelons notre fichier source pywine.py et le créeons dans le dossier /projects.Pywine (le même dossier qui contient déjà nos fichiers Glade). Ce qui suit est le code source de base qui sera utilisé (récupéré du premier tutoriel - Créer des interfaces graphique avec PyGTK et Glade) :

#!/usr/bin/env python

import sys
try:
        import pygtk
        pygtk.require("2.0")
except:
        pass
try:
        import gtk
        import gtk.glade
except:
        sys.exit(1)

class pyWine:
        """This is the PyWine application"""

        def __init__(self):

                #Set the Glade file
                self.gladefile = "pywine.glade"
                self.wTree = gtk.glade.XML(self.gladefile, "mainWindow")

                #Create our dictionay and connect it
                dic = {"on_mainWindow_destroy" : gtk.main_quit
                                , "on_AddWine" : self.OnAddWine}
                self.wTree.signal_autoconnect(dic)

        def OnAddWine(self, widget):
                """Called when the use wants to add a wine"""

                print "OnAddWine"

if __name__ == "__main__":
        wine = pyWine()
        gtk.main()

Il y a quelques ajouts à ce code. Le premier ajout est un gestionnaire pour le signal "on_AddWine", si vous exécutez ce programme, vous constaterez qu'un message apparaît sur la console si vous cliquez sur le bouton Add Wine ou que vous activez le menu Add->Wine. L'autre ajout au code est le passage du nom de la fenêtre principale à gtk.glade.XML. Ceci permet de ne charger que cette fenêtre et ses enfants. (NdT : On évite ainsi d'afficher toutes les fenêtres contenues dans le fichier Glade).

Ce que nous faisons ensuite, c'est de créer une classe Wine qui sera utilisée pour stocker les informations sur les vins :

class Wine:
        """This class represents all the wine information"""

        def __init__(self, wine="", winery="", grape="", year=""):

                self.wine = wine
                self.winery = winery
                self.grape = grape
                self.year = year

Maintenant, créeons une classe que nous utiliserons afin d'afficher notre dialogue wineDlg et appelons-la wineDialog :

class wineDialog:
        """This class is used to show wineDlg"""

        def __init__(self, wine="", winery="", grape="", year=""):

                #setup the glade file
                self.gladefile = "pywine.glade"
                #setup the wine that we will return
                self.wine = Wine(wine,winery,grape,year)

Ajoutons maintenant une méthode à notre classe wineDialog afin de charger le widget wineDialog depuis le fichier glade et de l'afficher. Nous voulons aussi que cette fonction nous retourne le résultat du dialogue, qui sera un gtk.RESPONSE, vous pourrez en apprendre plus sur le site web de PyGTK.

Voici la méthode run :

def run(self):
        """This function will show the wineDlg"""

        #load the dialog from the glade file
        self.wTree = gtk.glade.XML(self.gladefile, "wineDlg")
        #Get the actual dialog widget
        self.dlg = self.wTree.get_widget("wineDlg")
        #Get all of the Entry Widgets and set their text
        self.enWine = self.wTree.get_widget("enWine")
        self.enWine.set_text(self.wine.wine)
        self.enWinery = self.wTree.get_widget("enWinery")
        self.enWinery.set_text(self.wine.winery)
        self.enGrape = self.wTree.get_widget("enGrape")
        self.enGrape.set_text(self.wine.grape)
        self.enYear = self.wTree.get_widget("enYear")
        self.enYear.set_text(self.wine.year)

        #run the dialog and store the response
        self.result = self.dlg.run()
        #get the value of the entry fields
        self.wine.wine = self.enWine.get_text()
        self.wine.winery = self.enWinery.get_text()
        self.wine.grape = self.enGrape.get_text()
        self.wine.year = self.enYear.get_text()

        #we are done with the dialog, destroy it
        self.dlg.destroy()

        #return the result and the wine
        return self.result,self.wine

Remarquez que nous chargeons la fenêtre du dialogue de la même manière que la fenêtre principale. Nous appelons gtk.glade.CML() et lui passons le nom du widget que nous voulons charger. Ceci affichera automatiquement le dialogue (comme pour notre fenêtre principale), mais ce n'est pas suffisant, car il faut impérativement que la mèthode run attende que l'utilisateur quitte le dialogue avant de continuer son traitement. Pour cela, il nous faut récupérer le dialogue depuis l'arborescence des widgets (self.dlg = self.wTree.get_widget("wineDlg")) et appeler la fonction run des GTkDialogs. Voici ce que la documentation de PyGTK raconte à propos de cette fonction :

La méthode run() exécute une boucle récursive jusqu'à ce que le dialogue émette un signal "response", ou soit détruit. Si le dialogue est détruit, la méthode run() retourne gtk.RESPONSE_NONE; sinon, elle retourne l'identifiant de la réponse du signal "response" émis. Avant d'entrer dans sa boucle, la méthode run() appelle pour vous la méthode gtk.Widget.show(). Notez bien qu'il vous faudra éventuellement afficher vous même les enfants du dialogue.

Durant l'exécution de la méthode run(), le comportement par défaut de "delete_event" est désactivé; si le dialogue reçoit un "delete_event", il ne sera pas détruit comme une fenêtre le serait habituellement, et la méthode run() retournera gtk.RESPONSE_DELETE_EVENT. D'ailleurs, pendant 'exécution de la méthode run(), le dialogue sera modal. Vous pouvez forcer l'arrêt de la méthode run() en appelant response() pour émettre un signal "response". Détruire le dialogue pendant l'exécution de run() est une très mauvaise idée, parce que votre code suivant l'appel de run() ne saura pas si le dialogue a été détruit ou pas.

Après le retour de la méthode run(), vous devez vous même cacher ou détruire le dialogue, selon vos besoins.

Le bouton Ok va retourner gtk.RESPONSE_OK et le bouton Cancel retournera gtk.RESPONSE_CANCEL. Le traitement principal sera de prendre en compte les informations sur le vin retournées par le dialogue, si l'utilisateur a cliqué sur le bouton Ok.

Vous constaterez aussi que nous récupérons les widgets GTKEntry du dialogue pour lire ou modifier leurs textes. Cette fonction reste fort simple.

Tree Views et List Stores

Maintenant que nous avons les caractéristiques du vin de l'utilisateur, nous devons l'ajouter à la liste, dans le gtk.TreeView.

La caractéristique principale des GTKTreeViews est qu'ils affichent leurs données de la manière dont leur modèle leur dit de les afficher. Ils peuvent utiliser un gtk.ListStore, un gtk.TreeStore, un gtk.TreeModelSort, ou un gtk.GenericTreeModel. Dans cette exemple, nous utiliserons le gtk.ListStore.

La relation entre le Tree View et son modèle est un peu compliqué, mais une fois qu'on vous en aurez utilisé un, vous comprendrez pourquoi ils ont fait ça comme ça. En simplifiant outrageusement, le modèle représente les données, et le Tree View est une façon de les afficher. Ainsi, vous pouvez avoir de multiples façons d'afficher les mêmes données (modèle). On peut lire, dans le manuel de référence Gtk+ :

Pour créer une arborescente ou une liste en GTK+, utilisez l'interface GtkTreeModel avec le widget GtkTreeView. Ce widget est conçu en Modèle/Vue/Contrôleur et comprend quatre couches : Le widget de vue arborescente (GtkTreeView) La vue en colonne (GtkTreeViewColumn) Les afficheurs de cellules (GtkCellRenderer etc.) L'interface modèle (GtkTreeModel)

La vue est composée des trois premiers objets, tandis que le dernier est le Model. Le premier bénéfice que l'on retire de la conception MVC, c'est que plusieurs vues peuvent être créées pour un même modèle. Par example, un modèle basé sur le système de fichier peut être créé pour un gestionnaire de fichiers. Beaucoup de vues différentes peuvent être créées pour afficher différentes parties du système de fichiers, mais une seule devra être conservée en mémoire.

Le première chose que nous avons besoin de faire est d'ajouter un peut de code dans la méthode init de la classe pyWine, juste l'endroit où l'on connecte le dictionnaire à l'arborescence des widgets.

#Here are some variables that can be reused later
self.cWine = 0
self.cWinery = 1
self.cGrape = 2
self.cYear = 3

self.sWine = "Wine"
self.sWinery = "Winery"
self.sGrape = "Grape"
self.sYear = "Year"

#Get the treeView from the widget Tree
self.wineView = self.wTree.get_widget("wineView")
#Add all of the List Columns to the wineView
self.AddWineListColumn(self.sWine, self.cWine)
self.AddWineListColumn(self.sWinery, self.cWinery)
self.AddWineListColumn(self.sGrape, self.cGrape)
self.AddWineListColumn(self.sYear, self.cYear)

Ce code se lit d'une traite. Premièrement, nous créeons quelques variables qui nous servirons plus tard à faciliter les changements. Ensuite nous récupérons notre gtk.TreeView depuis l'arborescence des widgets. Après cela nous appelons une nouvelle fonction pour ajouter les colonnes nécessaires à la liste. AddWineListColumn est une petite fonction qui nous évitera de dupliquer du code à chaque création d'une colonne :

def AddWineListColumn(self, title, columnId):
        """This function adds a column to the list view.
        First it create the gtk.TreeViewColumn and then set
        some needed properties"
""

        column = gtk.TreeViewColumn(title, gtk.CellRendererText()
                , text=columnId)
        column.set_resizable(True)
        column.set_sort_column_id(columnId)
        self.wineView.append_column(column)

Ce code est un peu plus compliqué, tout d'abord, nous créeons une nouvelle gtk.TreeViewColumn qui utilise un afficheur de texte gtk.CellRendererText en tant qu'afficheur de cellules gtk.CellRenderer. Voici une information plus générale en provenance du manuel de référence GTK+ :

Une fois que le widget GtkTreeView dispose d'un modèle, il a besoin de savoir comment afficher ce dernier. Il fait cela avec des colonnes et des afficheurs de cellules.

Les afficheurs de cellules sont utilisés pour dessiner la donnée du modèle d'une certaine manière. Il existe un certain nombre d'afficheurs de cellules dans GTK+ 2.x, incluant GtkCellRendererText, GtkCellRendererPixbuf et GtkCellRendererToggle. Il est relativement facile d'écrire son propre afficheur de cellules.

Un GtkTreeViewColumn est l'objet qu'utilise GtkTreeView pour organiser les colonnes verticalement dans l'affichage arborescent. Il a besoin de connaître le nom de la colonne à afficher à l'utilisateur, quel type d'afficheur de cellules utiliser, et quel partie des données il faut lire depuis le modèle pour une rangée déterminée.

Ce que nous allons faire est donc de créer un colonne avec son titre, indiquer qu'elle utilisera un gtk.CellRendererText (pour afficher du texte simple), et lui donner l'élément du modèle auquel elle sera attachée. Nous la rendons ensuite redimensionnable, et permettons à l'utilisateur de trier par un clic sur l'entête de la colonne. Finalement, nous ajoutons la colonne au Tree View.

Maintenant que le visuel est fait, nous devons créer notre Model. Ceci sera fait dans la méthode init de la classe pyWine :

#Create the listStore Model to use with the wineView
self.wineList = gtk.ListStore(str, str, str, str)
#Attatch the model to the treeView
self.wineView.set_model(self.wineList)

Nous créeons simplement un gtk.ListStore avec quatres éléments (NdT: qui correspondent aux colonnes à afficher) de type chaîne de caractères. Enfin, nous lions le modèle à la vue et c'est tout ce qu'il y a à faire pour notre gtk.TreeView.

Rassembler les pièces du puzzle

Pour boucler notre projet, il nous reste à écrire la fonction OnAddWine (appelée depuis le menu ou la barre d'outils) de la clase pyWine. C'est une fonction assez simple :

def OnAddWine(self, widget):
        """Called when the use wants to add a wine"""
        #Create the dialog, show it, and store the results
        wineDlg = wineDialog();
        result,newWine = wineDlg.run()

        if (result == gtk.RESPONSE_OK):
                """The user clicked Ok, so let's add this
                wine to the wine list"
""
                self.wineList.append(newWine.getList())

Nous instancions notre dialogue wineDialog et l'exécutons pour ensuite mémoriser son résultat et les informations sur le vin que l'utilisateur aura saisit. Nous testons le résultat, qui doit être gtk.RESPONSE_OK (l'utilisateur ayant cliqué sur le bouton Ok) et alors nous ajoutons le vin dans notre gtk.ListStore. Ce dernier sera automatiquement affiché dans le gtk.TreeView car nous les avons connecté ensemble.

Nous utilisons une fontion getList() dans la classe pour faciliter la lecture du code source :

def getList(self):
        """This function returns a list made up of the
        wine information.  It is used to add a wine to the
        wineList easily"
""
        return [self.wine, self.winery, self.grape, self.year]

pywine_05.png

Notre application est terminée, pour peu que l'on accepte qu'elle ne sauvegarde aucune information. Elle représente bien les étapes nécessaires à la construction d'application complète en PyGTK.

Le source complet et le fichier glade peuvent être trouvés ici. Vous pouvez aussi lire le code source ci-dessous :

#!/usr/bin/env python

import sys
try:
        import pygtk
        pygtk.require("2.0")
except:
        pass
try:
        import gtk
        import gtk.glade
except:
        sys.exit(1)

class pyWine:
        """This is an PyWine application"""

        def __init__(self):

                #Set the Glade file
                self.gladefile = "pywine.glade"
                self.wTree = gtk.glade.XML(self.gladefile, "mainWindow")

                #Create our dictionay and connect it
                dic = {"on_mainWindow_destroy" : gtk.main_quit
                                , "on_AddWine" : self.OnAddWine}
                self.wTree.signal_autoconnect(dic)

                #Here are some variables that can be reused later
                self.cWine = 0
                self.cWinery = 1
                self.cGrape = 2
                self.cYear = 3

                self.sWine = "Wine"
                self.sWinery = "Winery"
                self.sGrape = "Grape"
                self.sYear = "Year"

                #Get the treeView from the widget Tree
                self.wineView = self.wTree.get_widget("wineView")
                #Add all of the List Columns to the wineView
                self.AddWineListColumn(self.sWine, self.cWine)
                self.AddWineListColumn(self.sWinery, self.cWinery)
                self.AddWineListColumn(self.sGrape, self.cGrape)
                self.AddWineListColumn(self.sYear, self.cYear)

                #Create the listStore Model to use with the wineView
                self.wineList = gtk.ListStore(str, str, str, str)
                #Attache the model to the treeView
                self.wineView.set_model(self.wineList)

        def AddWineListColumn(self, title, columnId):
                """This function adds a column to the list view.
                First it create the gtk.TreeViewColumn and then set
                some needed properties"
""

                column = gtk.TreeViewColumn(title, gtk.CellRendererText()
                        , text=columnId)
                column.set_resizable(True)
                column.set_sort_column_id(columnId)
                self.wineView.append_column(column)

        def OnAddWine(self, widget):
                """Called when the use wants to add a wine"""
                #Cteate the dialog, show it, and store the results
                wineDlg = wineDialog();
                result,newWine = wineDlg.run()

                if (result == gtk.RESPONSE_OK):
                        """The user clicked Ok, so let's add this
                        wine to the wine list"
""
                        self.wineList.append(newWine.getList())

class wineDialog:
        """This class is used to show wineDlg"""

        def __init__(self, wine="", winery="", grape="", year=""):

                #setup the glade file
                self.gladefile = "pywine.glade"
                #setup the wine that we will return
                self.wine = Wine(wine,winery,grape,year)

        def run(self):
                """This function will show the wineDlg"""

                #load the dialog from the glade file
                self.wTree = gtk.glade.XML(self.gladefile, "wineDlg")
                #Get the actual dialog widget
                self.dlg = self.wTree.get_widget("wineDlg")
                #Get all of the Entry Widgets and set their text
                self.enWine = self.wTree.get_widget("enWine")
                self.enWine.set_text(self.wine.wine)
                self.enWinery = self.wTree.get_widget("enWinery")
                self.enWinery.set_text(self.wine.winery)
                self.enGrape = self.wTree.get_widget("enGrape")
                self.enGrape.set_text(self.wine.grape)
                self.enYear = self.wTree.get_widget("enYear")
                self.enYear.set_text(self.wine.year)

                #run the dialog and store the response
                self.result = self.dlg.run()
                #get the value of the entry fields
                self.wine.wine = self.enWine.get_text()
                self.wine.winery = self.enWinery.get_text()
                self.wine.grape = self.enGrape.get_text()
                self.wine.year = self.enYear.get_text()

                #we are done with the dialog, destory it
                self.dlg.destroy()

                #return the result and the wine
                return self.result,self.wine


class Wine:
        """This class represents all the wine information"""

        def __init__(self, wine="", winery="", grape="", year=""):

                self.wine = wine
                self.winery = winery
                self.grape = grape
                self.year = year

        def getList(self):
                """This function returns a list made up of the
                wine information.  It is used to add a wine to the
                wineList easily"
""
                return [self.wine, self.winery, self.grape, self.year]

if __name__ == "__main__":
        wine = pyWine()
        gtk.main()

NdT : Je ne suis pas un traducteur professionnel, aussi je vous demande un peu d'indulgence :)