III. Améliorer notre application PyGTK▲
Où il sera décidé d'enrichir l'expérience utilisateur en terminant l'application PyWine.
Ce billet est la traduction, librement adaptée, du billet anglophone Extending our PyGTK Application.
Améliorer notre application PyGTK - Chap.3
Dans ce tutoriel, nous allons améliorer notre application PyWine pour rendre fonctionnelle l'édition des éléments ajoutés dans la liste, ainsi que le chargement et la sauvegarde de la liste des vins.
Le source complet du tutoriel peut être téléchargé ici.
Si l'application PyWine ne vous est pas familière ou que vous ne connaissez pas Glad et PyGTK, je vous suggère de lire les deux premiers tutoriels sur le sujet :
III-A. L'interface graphique - Glade▲
La première chose que nous allons faire, c'est d'ouvrir notre projet glade PyWine. Nous y ajoutons alors un bouton "Edit" dans sa barre d'outils :
- Il faut créer la place nécessaire dans la barre d'outils. Sélectionnons là et passons la valeur de sa propriété Size à 2.
- Ajoutons un Toolbar Button dans l'espace vide ainsi créé.
- Appelons ce bouton btnEditWine, modifions son libellé pour "Edit wine", et son icône pour un stock icon Edit. Ensuite, ajoutons un gestionnaire sur l'événement clicked.
- Nous allons un peu modifier le menu. Au lieu d'avoir un menu Add -> Wine, nous aurons Wine -> Add et Wine -> Edit. Il suffit de faire comme nous l'avons vu dans le précédent tutoriel et de créer le gestionnaire d'événement pour Wine -> Edit clicked rattaché à la même fonction que celle du clic sur le bouton btnEditWine.
III-B. Le code source▲
Maintenant, il faut développer le traitement effectué par le bouton Edit. Il faut d'abord récupérer le vin qui est sélectionné dans le gtk.TreeView. Il y a deux façons de le faire. Soit nous lisons toutes les données affichées par les colonnes, à la recherche des informations sélectionnées; soit nous stockons l'objet Wine dans le gtk.ListStore (notre modèle) mais nous ne l'afficherons pas dans le gtk.TreeView.
Cette dernière approche est plus simple et plus utile pour la suite, si notre classe Wine doit contenir de nouvelles informations, ou si nous laissons l'utilisateur libre de modifier les colonnes du gtk.TreeView. Il nous faut donc changer un peu le code.
D'abord, ajoutons la nouvelle colonne à la liste des variables les identifiant dans la mèthode init de pyWine :
self.cWineObject =
0
self.cWine =
1
self.cWinery =
2
self.cGrape =
3
self.cYear =
4
Notez que nous plaçons l'objet wine en position 0 de la liste, il faut alors modifier le code de création du modèle gtk.ListStore :
#Create the listStore Model to use with the wineView
self.wineList =
gtk.ListStore
(
gobject.TYPE_PYOBJECT
, gobject.TYPE_STRING
, gobject.TYPE_STRING
, gobject.TYPE_STRING
, gobject.TYPE_STRING)
La seule chose qui change est que le premier élément du gtk.ListStore est maintenant un objet python. Pour que ce code compile, il faut importer le module adéquat, au début du fichier source :
import
gobject
La prochaine étape consiste à modifier la manière d'ajouter un vin au gtk.ListStore afin d'inclure l'objet Wine. Heureusement, dans notre précédent tutoriel, nous avions ajouté une méthode getList() à la classe Wine. Elle retourne la liste à ajouter au gtk.ListStore, il suffit alors d'éditer ceci :
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, self.wine, self.winery, self.grape, self.year]
C'est une modification mineure, nous avons simplement fait en sorte que getList() retourne l'objet Wine en tête de liste.
Pour la suite, nous allons permettre à l'utilisateur de modifier un vin mais avant, un changement doit être fait. Le premier tutoriel utilisait une mèthode init qui acceptait en paramètres les attributs d'un vin :
def
__init__
(
self, wine=
""
, winery=
""
, grape=
""
, year=
""
):
C'est très bien s'il y a peu de paramètres, mais si la classe Wine évolue, l'initialisation par init va devenir pénible. Nous allons donc transmettre une classe Wine :
def
__init__
(
self, wine=
None
):
"""Initialize the class.
wine - a Wine object"""
#setup the glade file
self.gladefile =
"pywine.glade"
#setup the wine that we will return
if
(
wine):
#They have passed a wine object
self.wine =
wine
else
:
#Just use a blank wine
self.wine =
Wine
(
)
Ensuite, il faut gérer la saisie du vin, ce qui sera fait par une fonction appelée on_EditWine(). Cette méthode est associée à l'événement clicked du bouton Edit Wine, et à l'option de menu Wine->Edit.
def
on_EditWine
(
self, widget):
"""Called when the user wants to edit a wine entry"""
# Get the selection in the gtk.TreeView
selection =
self.wineView.get_selection
(
)
# Get the selection iter
model, selection_iter =
selection.get_selected
(
)
if
(
selection_iter):
"""There is a selection, so now get the the value at column
self.cWineObject, the Wine Object"""
wine =
self.wineList.get_value
(
selection_iter, self.cWineObject)
# Create the wine dialog, based off of the current selection
wineDlg =
wineDialog
(
wine);
result,newWine =
wineDlg.run
(
)
if
(
result ==
gtk.RESPONSE_OK):
"""The user clicked Ok, so let's save the changes back
into the gtk.ListStore"""
self.wineList.set(
selection_iter
, self.cWineObject, newWine
, self.cWine, newWine.wine
, self.cWinery, newWine.winery
, self.cGrape, newWine.grape
, self.cYear, newWine.year)
La première ligne de la méthode est un appel à gtk.TreeView.get_selection() pour récupérer l'objet gtk.TreeSelection associé au gtk.TreeView. Nous récupérons alors le modèle gtk.TreeModel avec un appel à gtk.TreeSelection.get_selected() ainsi que le noeud sélectionné désigné par gtk.TreeIter. C'est ce noeud qui nous intéresse.
Si rien n'est sélectionné, alors get_selected() retourne None, sinon, nous utilisons le gtk.TreeIter pour obtenir l'objet Vin sélectionné par l'appel à gtk.TreeModel.get_value(). Avec l'objet Vin le reste du traitement est simple, nous créons l'objet wineDialog, l'affichons, et si on a cliqué sur son bouton OK, nous mettons à jour l'élément sélectionné dans le gtk.TreeView par l'appel à la fonction gtk.ListStore.set().
La fonction gtk.ListStore.set() est intéressante à plus d'un titre. Elle prend un premier paramètre gtk.TreeIter (la position pour mettre à jour les valeurs) et les paramètres suivants sont des paires (numéro de colonne, nouvelle valeur) ! Ma seule déception a étéait de ne pas trouver une fonction qui utilise une liste de la même manière que la fonction gtk.ListStore.append().
C'est tout pour la mdofication d'un vin ! Comme nous ne voulons pas avoir à tout re-saisir à chaque fois que nous démarrons l'application, il est temps de s'occuper de la sauvegarde et du chargement de notre liste de vins.
III-C. Enregistrer et charger la liste des vins▲
Empruntant tout d'abord deux fonctions utiltaires à l'outil de blogging offline WordPy :
def
show_error_dlg
(
self, error_string):
"""This Function is used to show an error dialog when
an error occurs.
error_string - The error string that will be displayed
on the dialog.
"""
error_dlg =
gtk.MessageDialog
(
type=
gtk.MESSAGE_ERROR
, message_format=
error_string
, buttons=
gtk.BUTTONS_OK)
error_dlg.run
(
)
error_dlg.destroy
(
)
Cette fonction offre une facilité pour avertir l'utilisateur qu'une erreur est survenue, grâce à un dialogue d'erreur. Nous l'ajouterons à la classe pyWine. Pour le détail de son fonctionnement, référez vous au tutorial sur l'outils de blogging offline WordPy.
Nous allons aussi récupérer la fonction browse_for_image() :
def
browse_for_image
(
self):
"""This function is used to browse for an image.
The path to the image will be returned if the user
selects one, however a blank string will be returned
if they cancel or do not select one."""
file_open =
gtk.FileChooserDialog
(
title=
"Select Image"
, action=
gtk.FILE_CHOOSER_ACTION_OPEN
, buttons=(
gtk.STOCK_CANCEL
, gtk.RESPONSE_CANCEL
, gtk.STOCK_OPEN
, gtk.RESPONSE_OK))
"""Create and add the Images filter"""
filter =
gtk.FileFilter
(
)
filter.set_name
(
"Images"
)
filter.add_mime_type
(
"image/png"
)
filter.add_mime_type
(
"image/jpeg"
)
filter.add_mime_type
(
"image/gif"
)
filter.add_pattern
(
"*.png"
)
filter.add_pattern
(
"*.jpg"
)
filter.add_pattern
(
"*.gif"
)
file_open.add_filter
(
filter)
"""Create and add the 'all files' filter"""
filter =
gtk.FileFilter
(
)
filter.set_name
(
"All files"
)
filter.add_pattern
(
"*"
)
file_open.add_filter
(
filter)
"""Init the return value"""
result =
""
if
file_open.run
(
) ==
gtk.RESPONSE_OK:
result =
file_open.get_filename
(
)
file_open.destroy
(
)
return
result
Toutefois, nous la modifions pour qu'elle permette les ouvertures et sauvegarde de fichiers, et qu'elle reconnaisse les fichiers pyWine (*.pwi) au lieu des images. Un paramètre supplémentaire lui sera transmis afin de la faire se comporter différement selon que l'on souhaite ouvrir ou enregistrer un fichier. Ce sera la valeur que l'on affectera à la propriété action du gtk.FileChooserDialog :
def
file_browse
(
self, dialog_action, file_name=
""
):
"""This function is used to browse for a pyWine file.
It can be either a save or open dialog depending on
what dialog_action is.
The path to the file will be returned if the user
selects one, however a blank string will be returned
if they cancel or do not select one.
dialog_action - The open or save mode for the dialog either
gtk.FILE_CHOOSER_ACTION_OPEN, gtk.FILE_CHOOSER_ACTION_SAVE
file_name - Default name when doing a save"""
if
(
dialog_action==
gtk.FILE_CHOOSER_ACTION_OPEN):
dialog_buttons =
(
gtk.STOCK_CANCEL
, gtk.RESPONSE_CANCEL
, gtk.STOCK_OPEN
, gtk.RESPONSE_OK)
else
:
dialog_buttons =
(
gtk.STOCK_CANCEL
, gtk.RESPONSE_CANCEL
, gtk.STOCK_SAVE
, gtk.RESPONSE_OK)
file_dialog =
gtk.FileChooserDialog
(
title=
"Select Project"
, action=
dialog_action
, buttons=
dialog_buttons)
"""set the filename if we are saving"""
if
(
dialog_action==
gtk.FILE_CHOOSER_ACTION_SAVE):
file_dialog.set_current_name
(
file_name)
"""Create and add the pywine filter"""
filter =
gtk.FileFilter
(
)
filter.set_name
(
"pyWine database"
)
filter.add_pattern
(
"*."
+
FILE_EXT)
file_dialog.add_filter
(
filter)
"""Create and add the 'all files' filter"""
filter =
gtk.FileFilter
(
)
filter.set_name
(
"All files"
)
filter.add_pattern
(
"*"
)
file_dialog.add_filter
(
filter)
"""Init the return value"""
result =
""
if
file_dialog.run
(
) ==
gtk.RESPONSE_OK:
result =
file_dialog.get_filename
(
)
file_dialog.destroy
(
)
return
result
FILE_EXT est défini comme suit :
FILE_EXT =
"pwi"
Dans le projet glade, nous ajoutons aussi les callbacks pour les options de menus File->Open et File->Save, de la même manière que nous l'avions fait pour Wine->Add et Wine->Edit. Je les ai appelé on_file_open et on_file_save :
#Create our dictionary and connect it
dic =
{"on_mainWindow_destroy"
: self.on_Quit
, "on_AddWine"
: self.OnAddWine
, "on_EditWine"
: self.on_EditWine
, "on_file_open"
: self.on_file_open
, "on_file_save"
: self.on_file_save}
self.wTree.signal_autoconnect
(
dic)
Pour la l'enregistrement et la lecture des objets Wine, nous utiliserons le module shelve de Python. C'est un module standard qui permet de sérialiser dans une base de données la plupart (sinon tous) des objets Python. Une autre approche aurait pu être de passer par l'utilisation de fichier XML, ou même d'utiliser le module pickle directement. Mais je pense que shelve a plus de sens pour notre application et qu'il reste plus abordable que la manipulation du XML.
La documentation nous éclaire :
Un shelf est un objet persistent organisé comme un dictionnaire. La différence avec les bases de données dbm est que les valeurs (pas les clés !) peuvent être n'importe quel objet Python - en fait, tous ce que le module pickle est capable de manipuler. Ceci inclue la plupart des instances de classes, les types données récursifs, et les objets contenant nombre d'autres sous-objets partagés. Les clés sont des chaînes de caractères (string) ordinaires.
III-C-1. Enregistrer▲
Bien, nous allons maintenant écrire la méthode on_file_save(). Tout d'abord, l'utilisateur va choisir un emplacement pour y enregistrer son fichier, il décidera aussi du nom du fichier. Ensuite, nous nous assurerons que l'extension du fichier est celle que nous gérons puis nous parcourerons l'ensemble de éléments de gtk.TreeView et enregistrerons chaque objet Wine grâce au module shelve :
def
on_file_save
(
self, widget):
"""Called when the user wants to save a wine list"""
# Get the File Save path
save_file =
self.file_browse
(
gtk.FILE_CHOOSER_ACTION_SAVE, self.project_file)
if
(
save_file !=
""
):
# We have a path, ensure the proper extension
save_file, extension =
os.path.splitext
(
save_file)
save_file =
save_file +
"."
+
FILE_EXT
""" Now we have the "real" file save loction create
the shelve file, use "n" to create a new file"""
db =
shelve.open(
save_file,"n"
)
"""Get the first item in the gtk.ListStore, and while it is not
None, move forward through the list saving each item"""
# Get the first item in the list
iter =
self.wineList.get_iter_root
(
)
while
(
iter):
# Get the wine at the current gtk.TreeIter
wine =
self.wineList.get_value
(
iter, self.cWineObject)
# Use the iters position in the list as the key name
db[self.wineList.get_string_from_iter
(
iter)] =
wine
# Get the next iter
iter =
self.wineList.iter_next
(
iter)
#close the database and write changes to disk, we are done
db.close
(
);
#set the project file
root, self.project_file =
os.path.split
(
save_file)
Après le travail fait plus haut sur les objets gtk.TreeIter, ce code ne devrait pas être difficile à comprendre. En fait, La seule difficulté est la suivante, le reste étant plutôt bien commenté :
while
(
iter):
# Get the wine at the current gtk.TreeIter
wine =
self.wineList.get_value
(
iter, self.cWineObject)
# Use the iters position in the list as the key name
db[self.wineList.get_string_from_iter
(
iter)] =
wine
# Get the next iter
iter =
self.wineList.iter_next
(
iter)
Nous effectuons une boucle parcourant chaque élément de gtk.ListStore et cela affecte l'objet Wine courant dans le fichier shelve avec la clé correspondant à la position de gtk.TreeIter.
db[self.wineList.get_string_from_iter
(
iter)] =
wine
La fonction gtk.TreeModel.get_string_from_iter() retourne une représentation du chemin pointé par iter sous forme de chaîne de caractères. Cette chaîne est une liste de nombre séparés par deux-points. Par exemple, la chaîne "4:10:0:3" peut nous être retournée. Comme nous utilisons un gtk.ListStore, les valeurs retournées s'incrémentent lorsque nous avançons dans la liste.
Ainsi, le premier élément sera "0", le second "1", le troisième "2", et ainsi de suite. Ceci sera utile pour le chargement du fichier, puisque les clés des fichiers shelve ne sont pas garanties d'être présentées dans l'ordre.
C'est à la fermeture du fichier shelve que les données sont écrites sur le disque.
Remarquez l'utilisation de l'élément self.project_file comme nom de fichier par défaut, c'est un ajout à la classe. Il désigne le nom du projet courant et nous permet d'avoir un nom par défaut dans gtk.FileChooserDialog, quand nous enregistrons. La définition se trouve dans la fonction d'initialisation :
self.project_file =
""
Ceci nous affiche le dialogue :
III-D. Charger▲
Procédons maintenant à l'implémentation de la méthode on_file_open(). Si vous avez compris l'implémentation de on_file_save(), alors ce ne sera pas bien difficile :
def
on_file_open
(
self, widget):
"""Called when the user wants to open a wine"""
# Get the file to open
open_file =
self.file_browse
(
gtk.FILE_CHOOSER_ACTION_OPEN)
if
(
open_file !=
""
):
# We have a path, open it for reading
try
:
db =
shelve.open(
open_file,"r"
)
if
(
db):
# We have opened the file, so empty out our gtk.TreeView
self.wineList.clear
(
)
""" Since the shelve file is not gaurenteed to be in order we
move through the file starting at iter 0 and moving our
way up"""
count =
0
;
while
db.has_key
(
str(
count)):
newwine =
db[str(
count)]
self.wineList.append
(
newwine.getList
(
))
count =
count +
1
db.close
(
);
#set the project file
root, self.project_file =
os.path.split
(
open_file)
else
:
self.show_error_dlg
(
"Error opening file"
)
except
:
self.show_error_dlg
(
"Error opening file"
)
Remarquez que nous utilisons un compteur (count) et la fonction has_key() quand nous chargeons les éléments. Comme expliqué précédemment, nous enregistrons chaque objet Wine avec son chemin gtk.TreeIter, qui est un nombre car nous utilisons un gtk.ListStore. Mais l'ordre de lecture n'est pas garanti, il nous faut utiliser notre propre compteur pour lire chaque élément du fichier afin de commencer à zéro et incrémenter jusqu'à ce que l'on ne trouve plus la clé compteur dans le fichier. Nous convertissons les entiers en chaînes de caractères car les clés sont des chaînes.
Pour charger un objet Wine depuis le fichier, nous demandons simplement l'élément correspondant à la clé en cours :
newwine =
db[str(
count)]
Il suffit alors de l'ajouter à la liste des vins, et nous aurons chargé un fichier .pwi. Le code try except se charge de récupérer toute erreur pouvant survenir si nous ouvrons un fichier qui ne serait pas du format d'un projet pyWine.
III-E. Conclusion▲
C'est tout pour ce tutoriel, mais si vous avez bien compris, vous pouvez embrayer sur l'implémentation de File->New, ou ajouter un bouton Delete dans la barre d'outils, ou encore modifier le titre de l'application pour y écrire le nom du projet courant.
Vous pouvez même tenter la sauvegarde au format XML. Cela pourrait même devenir une fonctionnalités sympa qui permettrait à l'utilisateur de chosisir son format de fichier.
Le code complet de ce tutoriel est accessible sur le site d'origine de l'article.