Vous êtes à peu près ici : Accueil  »   tutoriel PyGTK  »   PyGTK : sommaire

14.11. Le TreeModel générique

Si le TreeModel standard n'est pas assez puissant pour les besoins de l'application, il est possible d'utiliser le GenericTreeModel pour construire son propre TreeModel personnalisé en python. Créer un GenericTreeModel peut être utile en cas de problème de performance avec les objets TreeStore et ListStore standards, ou si on veut une interface directe avec une source de données externe (une base de données ou un système de fichiers) pour éviter une duplication des données de et vers le TreeStore ou le ListStore.

14.11.1. Aperçu du GenericTreeModel

Avec un GenericTreeModel, on construit et gère son modèle de données et on fournit un accès externe à travers l'interface du TreeModel standard en définissant un ensemble de méthodes de classe. PyGTK implémente l'interface du TreeModel et prend en charge les méthodes du TreeModel appelées pour fournir le modèle de données réel.

Les détails de l'implémentation de votre modèle devraient rester complétement cachés aux applications externes. Ce qui signifie que la manière dont votre modèle identifie, range et retrouve les données n'est pas connue de l'application. En général, la seule information qui est sauvegardée en-dehors du GenericTreeModel sont les références de ligne qui sont enveloppées par les TreeIter externes. Ces références ne sont pas visibles par l'application.

Voici un examen détaillé de l'interface GenericTreeModel qu'il faut fournir.

14.11.2. Interface du GenericTreeModel

L'interface du GenericTreeModel comprend les méthodes suivantes qui doivent être implémentées dans le modèle personnalisé :

def on_get_flags(self)
def on_get_n_columns(self)
def on_get_column_type(self, index)
def on_get_iter(self, path)
def on_get_path(self, rowref)
def on_get_value(self, rowref, column)
def on_iter_next(self, rowref)
def on_iter_children(self, parent)
def on_iter_has_child(self, rowref)
def on_iter_n_children(self, rowref)
def on_iter_nth_child(self, parent, n)
def on_iter_parent(self, child)

Il faut remarquer que ces méthodes supportent entièrement l'interface du TreeModel, y compris :

def get_flags()
def get_n_columns()
def get_column_type(index)
def get_iter(path)
def get_iter_from_string(path_string)
def get_string_from_iter(iter)
def get_iter_root()
def get_iter_first()
def get_path(iter)
def get_value(iter, column)
def iter_next(iter)
def iter_children(parent)
def iter_has_child(iter)
def iter_n_children(iter)
def iter_nth_child(parent, n)
def iter_parent(child)
def get(iter, column, ...)
def foreach(func, user_data)

Pour illustrer l'utilisation du GenericTreeModel j'ai modifié le programme listefichiers.py et montre comment les méthodes d'interface sont réalisées. Le programme listefichiers-gtm.py affiche les fichiers d'un répertoire avec une icône indiquant si le fichier est ou non un répertoire, le nom du fichier, sa taille, son mode et sa date de dernière modification.

La méthode on_get_flags() doit retourner une valeur qui est une combinaison de  :

gtk.TREE_MODEL_ITERS_PERSIST Les TreeIter perdurent quels que soient les signaux émis par l'arbre.
gtk.TREE_MODEL_LIST_ONLY Le modèle est uniquement une liste et n'a jamais d'enfant.

Si un modèle posséde des références de lignes valides malgré les changements de lignes (réordonnancement, ajout, suppression), on utilise gtk.TREE_MODEL_ITERS_PERSIST. De la même façon, si un modèle est seulement une liste, on utilise gtk.TREE_MODEL_LIST_ONLY. Autrement, on renvoie 0 si le modèle ne possède pas de références de lignes persistantes et est une arborescence. Dans l'exemple, le modèle est une liste avec des TreeIter persistants.

    def on_get_flags(self):
        return gtk.TREE_MODEL_LIST_ONLY|gtk.TREE_MODEL_ITERS_PERSIST

La méthode on_get_n_columns() doit retourner le nombre de colonnes que le modèle exporte vers l'application. L'exemple garde une liste de types de colonnes, ainsi on peut renvoyer la longueur de la liste :

class Fichmodeleliste(gtk.GenericTreeModel):
    ...
    column_types = (gtk.gdk.Pixbuf, str, long, str, str)
    ...
    def on_get_n_columns(self):
        return len(self.types_colonnes)

La méthode on_get_column_type() doit renvoyer le type de la colonne pour l'index indiqué. Cette méthode est généralement appelée par un TreeView quand le modèle est établi. On peut soit créer une liste ou un tuple contenant l'information de type de colonne soit le créer à la volée. Dans l'exemple :

    def on_get_column_type(self, n):
        return self.types_colonnes[n]

L'interface GenericTreeModel convertit le type Python en un GType, donc le code suivant

  flm = Fichmodeleliste()
  print flm.on_get_column_type(1), flm.get_column_type(1)

imprimerait :

<type 'str'> <GType gchararray (64)>

Les méthodes suivantes utilisent les références de ligne qui sont conservées comme données privées dans un TreeIter. L'application ne peut lire la référence de ligne dans un TreeIter donc on peut utiliser n'importe quel item unique voulu comme référence de ligne. Par exemple, dans un modèle comportant des lignes comme des tuples, il est possible d'utiliser l'index de tuple comme la référence de ligne. Un autre exemple serait d'utiliser un nom de fichier comme référence de ligne dans un modèle représentant des fichiers dans un répertoire. Dans ces deux cas, le référence de ligne n'est pas modifiée par les changements du modèle, aussi les TreeIter peuvent être déclarés persistants. L'interface de l'application PyGTK GenericTreeModel extraira vos références de ligne à partir des TreeIter et enveloppera vos références de ligne dans les TreeIter selons les besoins.

Dans les méthodes suivantes, refligne se réfère à une référence de ligne interne.

La méthode on_get_iter() devrait renvoyer un refligne pour le chemin de l'arborescence indiqué par le chemin. Le chemin de l'arborescence doit toujours être représenté par un tuple. L'exemple utilise le nom de fichier comme refligne. Les noms de fichier sont conservés dans une liste dans le modèle, ainsi on prend le premier index du chemin comme index du nom de fichier :

    def on_get_iter(self, chemin):
        return self.fichiers[chemin[0]]

Il faut être cohérent dans l'utilisation de la référence de ligne puisque on récupérera une référence de ligne dans les appels de méthode des méthodes GenericTreeModel qui prennent les arguments TreeIter : on_get_path(), on_get_value(), on_iter_next(), on_iter_children(), on_iter_has_child(), on_iter_n_children(), on_iter_nth_child() et on_iter_parent().

La méthode on_get_path() devrait retourner un chemin de l'arborescence donnant un refligne. Par exemple, poursuivant l'exemple précédent où le nom de fichier est utilisé comme refligne, on pourrait définir la méthode on_get_path() comme suit :

    def on_get_chemin(self, refligne):
        return self.fichiers.index(refligne)

Cette méthode trouve l'index de la liste contenant le nom de fichier dans refligne. Cet exemple montre clairement qu'un choix judicieux de référence de ligne rend l'exécution plus efficiente. Par exemple, on pourrait utiliser un dictionnaire Python pour relier refligne à un chemin.

La méthode on_get_value() devrait renvoyer les données rangées à la ligne et colonne indiquées par refligne et colonne. Dans l'exemple :

    def on_get_value(self, refligne, colonne):
        fname = os.path.join(self.nomrep, refligne)
        try:
            statutfich = os.stat(fname)
        except OSError:
            return None
        mode = statutfich.st_mode
        if colonne is 0:
            if stat.S_ISDIR(mode):
                return dossierpb
            else:
                return fichierpb
        elif colonne is 1:
            return refligne
        elif colonne is 2:
            return statutfich.st_size
        elif colonne is 3:
            return oct(stat.S_IMODE(mode))
        return time.ctime(statutfich.st_mtime)

on doit extraire l'information de fichier associée et renvoyer la valeur appropriée selon la colonne indiquée.

La méthode on_iter_next() devrait retourner une référence de ligne à la ligne (de même niveau) après la ligne indiquée par refligne. Dans l'exemple :

    def on_iter_next(self, refligne):
        try:
            i = self.fichiers.index(refligne)+1
            return self.fichiers[i]
        except IndexError:
            return None

L'index du nom de fichier de refligne est établi et le nom de fichier suivant est renvoyé ou s'il n'existe pas, None est renvoyé.

La méthode on_iter_children() devrait retourner une référence de ligne vers la première ligne enfant de la ligne indiquée par refligne. Si refligne vaut None, une référence à la première ligne du niveau supérieur est retournée. S'il n'existe pas de ligne enfant, None est retourné. Dans l'exemple :

    def on_iter_children(self, refligne):
        if refligne:
            return None
        return self.fichiers[0]

Puisque le modèle est une liste, seul le niveau supérieur (refligne=None) peut posséder des lignes enfant. None est retourné si refligne contient un nom de fichier.

La méthode on_iter_has_child() doit renvoyer TRUE si la ligne indiquée par refligne possède des lignes enfant, FALSE sinon. Dans l'exemple, on renvoie FALSE puisque aucune ligne ne peut posséder d'enfant :

    def on_iter_has_child(self, refligne):
        return False

La méthode on_iter_n_children() renvoie le nombre de lignes enfant que posséde la ligne indiquée par refligne. Si refligne vaut None, on renvoie le nombre de lignes du niveau supérieur. Dans l'exemple, on renvoie 0 si refligne ne vaut pas None :

    def on_iter_n_children(self, refligne):
        if refligne:
            return 0
        return len(self.fichiers)

La méthode on_iter_nth_child() renvoie une référence de ligne à la énième ligne enfant de la ligne indiquée par parent. Si parent vaut None, une référence à la énième ligne du niveau supérieur est retournée. Dans l'exemple, on retourne une référence à la énième ligne du niveau supérieur si parent vaut None, None sinon :

    def on_iter_nth_child(self, refligne, n):
        if refligne:
            return None
        try:
            return self.fichiers[n]
        except IndexError:
            return None

La méthode on_iter_parent() renvoie une référence de ligne à la ligne parent de la ligne indiquée par refligne. Si refligne pointe sur une ligne du niveau supérieur, on renvoie None. Dans l'exemple, None est toujours renvoyé en supposant que refligne doit pointer sur une ligne du niveau supérieur :

    def on_iter_parent(child):
        return None

Ces exemples sont assemblés dans le programme listefichiers-gtm.py. La Figure 14.11, « Exemple de TreeModel générique » montre le résultat

Figure 14.11. Exemple de TreeModel générique

Exemple de TreeModel générique.

14.11.3. Ajouter et supprimer des lignes

Le programme listefichiers-gtm.py calcule la liste des noms de fichier lorsqu'il crée une instance de FileListModel. Si l'on souhaite vérifier réguliérement la création de nouveaux fichiers et ajouter ou retiter des fichiers du modèle, on peut soit créer un nouveau FileListModel pour le même répertoire, soit ajouter une méthode pour ajouter ou retirer des lignes dans le modèle. Selon le type de modèle créé, il peut être nécessaire d'ajouter une méthode semblable à celles des modèles de TreeStore et de ListStore :

  • insert()
  • insert_before()
  • insert_after()
  • prepend()
  • append()
  • remove()
  • clear()

Il n'est, bien sûr, pas nécessaire d'utiliser toutes ou même une seule de ces méthodes. On peut créer ses propres méthodes plus adaptées à son modèle.

En utilisant le programme exemple prédécent pour montrer l'ajout de méthodes pour supprimer ou ajouter des fichiers, voici comment on implémente les méthodes 

def remove(iter)
def add(filename)

La méthode remove() retire le fichier indiqué par le paramétre iter. Outre retirer la ligne du modèle, la méthode efface aussi le fichier du répertoire. Évidemment, si l'utilisateur n'a pas les droits de suppression du ficher, la ligne n'est pas supprimée non plus. Par exemple :

    def remove(self, iter):
        path = self.get_path(iter)
        pathname = self.get_pathname(path)
        try:
            if os.path.exists(pathname):
                os.remove(pathname)
            del self.files[path[0]]
            self.row_deleted(path)
        except OSError:
            pass
        return

La méthode transmet un TreeIter qu'il faut transformer en un chemin à utiliser pour récupérer le chemin du fichier par la méthode get_pathname(). Il est possible que le fichier ait déjà été supprimé, il faut donc tester son existence avant de le supprimer. Si une exception OSError intervient pendant la suppression du fichier, c'est probablement parce que le fichier est dans un répertoire où l'utilisateur n'a pas suffisamment de droits. Finalement, le fichier est supprimé et le signal "row-deleted" est émis par la méthode rows_deleted(). Le signal "file-deleted" indique aux TreeView utilisant le modèle que ce modèle a changé, ainsi ils peuvent mettre à jour leur état interne et afficher le modèle modifié.

La méthode add() impose de créer un fichier dans le répertoire courant avec le nom donné. Si le fichier est créé, son nom est ajouté à la liste des fichiers du modèle. Par exemple :

    def add(self, filename):
        pathname = os.path.join(self.dirname, filename)
        if os.path.exists(pathname):
            return
        try:
            fd = file(pathname, 'w')
            fd.close()
            self.dir_ctime = os.stat(self.dirname).st_ctime
            files = self.files[1:] + [filename]
            files.sort()
            self.files = ['..'] + files
            path = (self.files.index(filename),)
            iter = self.get_iter(path)
            self.row_inserted(path, iter)
        except OSError:
            pass
        return

Cet exemple simple s'assure que le fichier n'existe pas déjà, puis tente d'ouvrir le fichier en écriture. Si cela réussit, le fichier est refermé et son nom inséré dans la liste de fichiers. Le chemin et le TreeIter de la ligne du fichier ajouté sont récupérés pour être utilisés dans la methode row_inserted() qui émet le signal "row-inserted". Ce signal "row-inserted" sert à indiquer aux TreeView utilisant le modèle qu'ils doivent mettre à jour leur état et rafraîchir leur affichage.

Les autres méthodes mentionnées précèdemment (par exemple append et prepend) n'ont pas de signification pour cet exemple puisque le modèle sa liste de fichiers triée.

D'autres méthodes peuvent être utiles à utiliser dans un TreeModel découlant du GenericTreeModel :

  • set_value()
  • reorder()
  • swap()
  • move_after()
  • move_before()

L'implémentation de ces méthodes est similaire à celle des méthodes précédentes. Il faut synchroniser le modèle et l'état externe, et ensuite indiquer aux TreeView que le modèle a changé. Les méthodes suivantes sont utilisées pour indiquer aux TreeView les changement du modèle en envoyant le signal approprié :

def row_changed(path, iter)
def row_inserted(path, iter)
def row_has_child_toggled(path, iter)
def row_deleted(path)
def rows_reordered(path, iter, new_order)

14.11.4. Gestion de la mémoire

L'un des problèmes avec le GenericTreeModel est que le TreeIter contient une référence à un objet Python venent du modèle personnalisé. Puisque le TreeIter peut être crée et initialisé dans un module en C et être présent dans la pile, il n'est pas possible de connaître le moment où le TreeIter est détruit et la référence à l'objet Python n'est plus d'utilité. Donc l'objet Python référencé dans un TreeIter voit par défaut son cimpteur incrémenté, mais il n'est pas décrémenté lorque le TreeIter est détruit. Ceci garantit que l'objet Python ne peut être détruit quand il est utilisé par un TreeIter et produire éventuellement une erreur de segmentation. Malheureusement les comptes de référence supplémentaires font que, au mieux l'objet Python aura un compte de référence excessif et, au pire, il ne sera jamais libéré même lorsque il n'est pas utilisé. Le dernier cas cause des fuites de mémoire et le premier, des fuites de références.

Pour parer à la situation où le TreeModel personnalisé maintient une référence à l'objet Python jusqu'à ce qu'il ne soit plus disponible (le TreeIter est invalide car le modèle a changé) et il n'y a pas besoin de relacher les références, le GenericTreeModel possède une propriété "leak-references". Par défaut, "leak-references" vaut TRUE pour indiquer que le GenericTreeModel relachera les références. Si "leak-references" vaut FALSE, le compteur de références de l'objet Python ne sera pas incrémenté quand il sera référencé dans un TreeIter. Ce qui signifie que le le TreeModel personnalisé doit conserver une référence à tous les objets Python utilisés dans un TreeIter jusqu'à la destruction du modèle. Malheureusement, même ceci ne protège pas d'un mauvais code qui tente d'utiliser un TreeIter sauvegardé dans un GenericTreeModel différent. Pour se protéger contre ce cas de figure, l'application doit conserver une référence à tous les objets Python référencés dans un TreeIter pour toutes les instances du GenericTreeModel. Naturellement, ceci a le le même effet qu'une fuite de références.

Avec PyGTK 2.4 et ultérieurs, les méthodes invalidate_iters() et iter_is_valid() sont disponibles comme aide à la gestion des TreeIter et des références des objets Python :

  generictreemodel.invalidate_iters()

  result = generictreemodel.iter_is_valid(iter)

Ceci est particulièrement utile lorsque la propriété "leak-references" vaut FALSE. Les modèles d'arbre dérivés du GenericTreeModel sont protégés des problèmes de TreeIter périmés car la validité des iters est automatiquement vérifiée avec le modèle arbre.

Si un modèle d'arbre personnalisé ne gère pas les iters persistants (par exemple, gtk.TREE_MODEL_ITERS_PERSIST n'est pas établi dans le retour de la méthode TreeModel.get_flags(), il est possible d'appeler la methode invalidate_iters() pour annuler tous les TreeIter en cours quand le modèle est modifié (après insertion d'une nouvelle ligne par ex.). Le modèle d'arbre peut aussi utiliser n'importe quel objet Python qui a été référencé par un TreeIter après l'appel à la méthode invalidate_iters().

Les applications peuvent utiliser la méthode iter_is_valid() pour déterminer si un TreeIter est encore valide pour le modèle d'arbre personnalisé.

14.11.5. Autres interfaces

Les modèles ListStore et TreeStore comprennent outre l'interface TreeModel, les interfaces TreeSortable, TreeDragSource et TreeDragDest. Le GenericTreeModel ne comprend que l'interface TreeModel. Je pense que c'est à cause de la référence directe au modèle dans le langage C par les modèles TreeView, TreeModelSort et TreeModelFilter. Créer et utiliser un TreeIter exige un code collant au C pour l'interface avec le modèle d'arbre personnalisé Python qui contient les données. Ce code collant est fourni par le GenericTreeModel et il semble qu'il n'y a pas d'alternative purement Python de réaliser ceci car le TreeView et les autres modèles appellent les fonctions du GtkTreeModel en C en passant leur référence au modèle d'arbre personnalisé.

L'interface TreeSortable nécessite aussi un code collant au C pour agir sur le mécanisme de tri par défaut du TreeViewColumn ainsi qu'il est expliqué dans la Section 14.2.9, « Ordonner les lignes d'un TreeModel ». Cependant un modèle personnalisé doit réaliser ses propres tris et une application doit gérer l'utilisation des critéres de tri en prenant en compte les clics sur les en-têtes des TreeViewColumn et en appelant les méthodes de tri du modèle d'arbre personnalisé. Le modèle effectue la mise à jour du TreeView en émettant le signal "rows-reordered" grace à la méthode rows_reordered() du TreeModel. Ainsi le GenericTreeModel ne nécessite probablement pas d'implémenter l'interface TreeSortable.

Pareillement, le GenericTreeModel n'a pas besoin d' implémenter les interfaces TreeDragSource et TreeDragDest puisque le modèle d'arbre personnalisé peut effectuer ses propres interfaces de glisser-déposer et l'application peut gérer les signaux TreeView appropriés et faire appel aux méthodes du modèle d'arbre personnalisé tant que nécessaire.

14.11.6. Utilisation du GenericTreeModel

Je crois que le GenericTreeModel ne devrait être utilisé qu'en dernier ressort. Les objets standard du TreeView comprennent des mécanismes puissants qui devraient être suffisants pour la plupart des applications. Sans doute, il existe des applications qui peuvent avoir besoin du GenericTreeModel mais il faudrait d'abord essayer d'utiliser ce qui suit :

Cell Data Functions

Comme il est montré dans la Section 14.4.5, « Fonction d'affichage des données cellulaires », les fonctions de données des cellules peuvent être utilisées pour modifier et même produire les données pour un affichage d'une colonne du TreeView. On peut créer autant d'affichages de colonnes avec des données générées que l'on souhaite. Cela donne beaucoup de contrôle de la présentation de données d'une source de données sous-jacente.

TreeModelFilter

En PyGTK 2.4, le TreeModelFilter, comme indiqué dans la Section 14.10.2, « Le TreeModelFilter », permet un fort contrôle sur l'affichage des colonnes et lignes d'un TreeModel enfant, y compris afficher seulement les lignes filles d'une ligne. Les colonnes de données peuvent être produite d'une façon semblable à l'utilisation des fonctions de données en cellule ; ici le modèle est un TreeModel avec un nombre et un type de colonnes indiqués alors qu'une fonction de données de cellule laisse les colonnes modèles inchangées et modifie juste l'affichage dans un TreeView.

Si un GenericTreeModel doit être utilisé, il faut veiller à :

  • l'interface complète du TreeModel doit être créée et être en mesure de fonctionner comme indiqué. I y a des finesses qui peuvent induire des erreurs. Par opposition, le TreeModel standard a été complètement testé.
  • gérer les références des objets Python utilisés par un TreeIter peut se révéler difficile, particulièrement pour des programmes longs avec beaucoup d'affichages variés.
  • il faut créer un interface pour ajouter, supprimer ou modifier le contenu des lignes. Le lien d'un TreeIter aux objets de Python et aux rangées modèles dans cette interface n'est pas trés élégant.
  • le développement d'interfaces de glisser-déposer et de tri demande un effort important, l'application doit trés probablement participer à ce que ces interfaces soient entièrement fonctionnelles.