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
.
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.
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
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
)
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é.
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.
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
|
TreeModelFilter | En PyGTK 2.4, le |
Si un GenericTreeModel
doit être utilisé, il faut
veiller à :
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é.
TreeIter
peut se révéler difficile, particulièrement
pour des programmes longs avec beaucoup d'affichages variés.