IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Créer des interfaces graphique avec PyGTK et Glade


précédentsommaire

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 :

  1. 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.
  2. Ajoutons un Toolbar Button dans l'espace vide ainsi créé.
  3. 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.
  4. 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.
Image non disponible

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 :

 
Sélectionnez
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 :

 
Sélectionnez
#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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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() :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
#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 :

 
Sélectionnez
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 :

 
Sélectionnez
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é :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
self.project_file = ""

Ceci nous affiche le dialogue :

Image non disponible

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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.


précédentsommaire

Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.