II. Construire une application avec PyGTK et Glade▲
Où l'on va commencer l'écriture d'une application simple et utile.
Ce billet est la traduction, librement adaptée, du billet anglophone Building an Application with PyGTK and Glade.
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".
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.
(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".
II-A. 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.
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.
II-B. 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 :
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.
II-C. 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.
II-D. 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.
II-E. 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]
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
(
)