En el momento en el que se encuentra que los modelo de árbol estándar
TreeModels no son suficientemente potentes para cubrir las
necesidades de una aplicación se puede usar la clase
GenericTreeModel para construir modelos
TreeModel personalizados en Python. La creación de un
GenericTreeModel puede ser de utilidad cuando existen problemas
de rendimiento con los almacenes estándar TreeStore y
ListStore o cuando se desea tener una interfaz directa con una
fuente externa de datos (por ejemplo, una base de datos o el sistema de archivos) para evitar
la copia de datos dentro y fuera de los almacenes TreeStore o
ListStore.
Con GenericTreeModel se construye y gestiona un modelo
propio de datos y se proporciona acceso externo a través de la interfaz estándar
TreeModel al definir un conjunto de métodos de clase. PyGTK
implementa la interfaz TreeModel y hace que los nuevos métodos
TreeModel sean llamados para proporcionar los datos del modelo
existente.
Los detalles de implementación del modelo personalizado deben mantenerse ocultos a
la aplicación externa. Ello significa que la forma en la que se identifica el modelo, guarda y obtiene
sus datos es desconocido a la aplicación. En general, la única información que se guarda
fuera del GenericTreeModel son las referencia de fila que se
encapsulan por los iteradores TreeIter externos. Y estas referencias no son visibles a la aplicación.
Examinemos en detalle la interfaz GenericTreeModel
que es necesario proporcionar.
La interfaz GenericTreeModel consiste en los siguientes
métodos que deben implementarse en el modelo de árbol personalizado:
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)Debe advertirse que estos métodos dan soporte a toda la interfaz de
TreeModel incluída:
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)Para ilustrar el uso de
GenericTreeModel modificaremos el programa de ejemplo filelisting.py
y veremos cómo se crean los métodos de la interfaz. El programa filelisting-gtm.py
muestra los archivos de una carpeta con un pixbuf que indica si el archivo es una carpeta o no,
el nombre de archivo, el tamaño del mismo, el modo y hora del último cambio.
El método on_get_flags() debería devolver un valor
que es una combinación de:
| Los TreeIters sobreviven a todas las señales
emitidas por el árbol. |
| El modelo es solamente una lista, y nunca tiene hijos |
Si el modelo tiene referencias de fila que son válidas entre cambios de fila
(reordenación, adición o borrado) entonces se dbe establecer
gtk.TREE_MODEL_ITERS_PERSIST. Igualmente, si el modelo es una lista
entonces de debe fijar gtk.TREE_MODEL_LIST_ONLY. De otro modo,
se debe devolver 0 si el modelo no tiene referencias de fila persistentes y es un modelo
de árbol. Para nuestro ejemplo, el modelo es una lista con iteradores
TreeIter persitentes.
def on_get_flags(self):
return gtk.TREE_MODEL_LIST_ONLY|gtk.TREE_MODEL_ITERS_PERSIST
El método on_get_n_columns() debería devolver
el número de columnas que el modelo exporta a la aplicación. Nuestro ejemplo mantiene una lista
de los tipos de las columnas, de forma que devolvemos la longitud de la lista:
class FileListModel(gtk.GenericTreeModel):
...
column_types = (gtk.gdk.Pixbuf, str, long, str, str)
...
def on_get_n_columns(self):
return len(self.column_types)
El método on_get_column_type() debería devolver el
tipo de la columna con el valor de índice index especificado. Este
método es llamado generalmente desde una TreeView cuando
se establece su modelo. Se puede, bien crear una lista o tupla que contenga la información
de tipos de datos de las columnas o bien generarla al vuelo. En nuestro ejemplo:
def on_get_column_type(self, n):
return self.column_types[n]
La interfaz GenericTreeModel convierte el tipo de Python
a un GType, de forma que el siguiente código:
flm = FileListModel() print flm.on_get_column_type(1), flm.get_column_type(1)
mostraría:
<type 'str'> <GType gchararray (64)>
Los siguientes métodos usan referencias de fila que se guardan como datos privados
en un TreeIter. La aplicación no puede ver la referencia de fila en un
TreeIter por lo que se puede usar cualquier elemento único que se
desee como referencia de fila. Por ejemplo, en un modelo que contiene filas como tuplas, se
podría usar la identificación de tupla como referencia de la fila. Otro ejemplo sería el uso de
un nombre de archivo como referencia de fila en un modelo que represente los archivos de
un directorio. En ambos casos la referencia de fila no se modifica con los cambios en el modelo,
así que los TreeIters se podrían marcar como persistentes. La
interfaz de aplicación de PyGTK GenericTreeModel extraerá
esas referencias de fila desde los TreeIters y encapsulará
las referencias de fila en TreeIters según sea necesario.
En los siguientes métodos rowref se refiere a una referencia de
fila interna.
El método on_get_iter() debería devolver una
rowref del camino de árbol indicado por path. El camino de árbol se
representará siempre mediante una tupla. Nuestro ejemplo usa la cadena del nombre de archivo
como rowref. Los nombres de archivo se guardan en una lista en el modelo, de manera que
tomamos el primer índice del camino como índice al nombre de archivo:
def on_get_iter(self, path):
return self.files[path[0]]
Es necesario ser coherente en el uso de las referencias de fila, puesto que
se obtendrán referencias de fila tras las llamadas a los métodos de
GenericTreeModel que toman argumentos con iteradores
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() y
on_iter_parent().
El método on_get_path() debería devolver un camino
de árbol dada una rowref. Por ejemplo, siguiendo con el ejemplo anterior donde el nombre de
archivo se usa como rowref, se podría definir el método
on_get_path() así:
def on_get_path(self, rowref):
return self.files.index(rowref)
Este método localiza el índice de la lista que contiene el nombre de archivo en
rowref. Es obvio viendo este ejemplo que una elección juiciosa de
la referencia de fila hará la implementación más eficiente. Se podría usar, por ejemplo, un
diccionario de Python para traducir una
rowref a un camino.
El método on_get_value() debería devolver los datos
almacenados en la fila y columna especificada por rowref y
la columna column. En nuestro ejemplo:
def on_get_value(self, rowref, column):
fname = os.path.join(self.dirname, rowref)
try:
filestat = statcache.stat(fname)
except OSError:
return None
mode = filestat.st_mode
if column is 0:
if stat.S_ISDIR(mode):
return folderpb
else:
return filepb
elif column is 1:
return rowref
elif column is 2:
return filestat.st_size
elif column is 3:
return oct(stat.S_IMODE(mode))
return time.ctime(filestat.st_mtime)
tiene que extraer la información de archivo asociada y devolver el valor adecuado en función de qué columna se especifique.
El método on_iter_next() debería devolver una rerferencia
de fila a la fila (en el mismo nivel) posterior a la especificada porrowref.
En nuestro ejemplo:
def on_iter_next(self, rowref):
try:
i = self.files.index(rowref)+1
return self.files[i]
except IndexError:
return None
El índice del nombre de archivo rowref es determinado y se
devuelve el siguiente nombre de archivo o None si no existe un archivo
después.
El método on_iter_children() debería devolver una
referencia de fila a la primera fila hija de la fila especificada por
rowref. Si rowref es
None entonces se devuelve una referencia a la primera fila de nivel superior. Si
no existe una fila hija se devuelve None. En nuestro ejemplo:
def on_iter_children(self, rowref):
if rowref:
return None
return self.files[0]
Puesto que el modelo es una lista únicamente la fila superior puede tener filas hijas
(rowref=None). Se devuelve None si
rowref contiene un nombre de archivo.
El método on_iter_has_child() debería devolver
TRUE si la fila especificada por rowref tiene filas
hijas o FALSE en otro caso. Nuestro ejemplo devuelve FALSE
puesto que ninguna fila puede tener hijas:
def on_iter_has_child(self, rowref):
return False
El método on_iter_n_children() debería devolver el
número de fijas hijas que tiene la fila especificada por rowref. Si
rowref es None entonces se devuelve el número
de las filas de nivel superior. Nuestro ejemplo devuelve 0 si rowref
no es None:
def on_iter_n_children(self, rowref):
if rowref:
return 0
return len(self.files)
El método on_iter_nth_child() debería devolver una
referencia de fila a la n-ésima fila hija de la fila especificada por
parent. Si parent es
None entonces se devuelve una referencia a la n-ésima fila de nivel
superior. Nuestro ejemplo devuelve la n-ésima fila de nivel superior si
parent es None. En otro caso devuelve
None:
def on_iter_nth_child(self, rowref, n):
if rowref:
return None
try:
return self.files[n]
except IndexError:
return None
El método on_iter_parent() debería devolver una
referencia de fila a la fila padre de la fila especificada por rowref. Si
rowref apunta a una fila de nivel superior se debe devolver
None. Nuestro ejemplo siempre devuelve None
asumiendo que rowref debe apuntar a una fila de nivel superior:
def on_iter_parent(child):
return None
Este ejemplo se ve de una vez en el programa filelisting-gtm.py. Figura 14.11, “Programa de Ejemplo de Modelo de Árbol Genérico” muestra el resultado de la ejecución del programa.
El programa filelisting-gtm.py calcula la lista de nombres de archivo
mientras se crea una instancia de FileListModel. Si se desea
comprobar la existencia de nuevos archivos de forma periódica y añadir o eliminar archivos del
modelo se podría, bien crear un nuevo modelo FileListModel de la
misma carpeta, o bien se podrían incorporar métodos para añadir y eliminar filas del modelo.
Dependiendo del tipo de modelo que se esté creando se necesitaría añadir unos métodos
similares a los de los modelos TreeStore y
ListStore:
insert()insert_before()insert_after()prepend()append()remove()clear()Naturalmente, ni todos ni cualquiera de estos métodos neceista ser implementado. Se pueden crear métodos propios que se relacionen de manera más ajustada al modelo.
Utilizando el programa de ejemplo anterior para ilustrar la adición de métodos para eliminar y añadir archivos, implementemos esos métodos:
def remove(iter) def add(filename)El método remove() elimina el archivo especificado por
iter. Además de eliminar la fila del modelo el método también debería
eliminar el archivo de la carpeta. Naturalmente, si el usuario no tiene permisos para eliminar
el archivo no se debería eliminar tampoco la fila. Por ejemplo:
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
Al método se le pasa un iterador TreeIter que ha de ser
convertido a un camino que se usa para obtener el camino del archivo usando el método
get_pathname(). Es posible que el archivo ya haya sido eliminado
por lo que debemos comprobar si existe antes de intentar su eliminación. Si se emite una
excepción OSError durante la eliminación del archivo, probablemente se deba a que es un
directorio o que la usuaria no tiene los privilegios necesarios para elimnarlo. Finalmente, el archivo
se elimina y la señal "row-deleted" se emite desde el método
rows_deleted(). La señal "file-deleted" notifica a las TreeViews que usan el modelo que éste ha cambiado, por lo que
pueden actualizar su estado interno y mostrar el modelo actualizado.
El método add() necesita crear un archivo con el nombre
dado en la carpeta actual. Si se crea el archivo su nombre se añade a la lista de archivos del
modelo. Por ejemplo:
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
Este ejemplo sencillo se asegura de que el archivo no existe y luego intenta abrir el
archivo para escritura. Si tiene éxito, el archivo se cierra y el nombre de archivo ordenado en
la lista de archivos. La ruta y el iterador
TreeIter de la fila de archivo añadida se obtienen para usarlos en el
método row_inserted() que emite la señal "row-inserted". La
señal "row-inserted" se usa para notificar a las TreeViews que
usan el modelo que necesitan actualizar su estado interno y revisar su visualización.
Los otros métodos mencionados anteriormente (por ejemplo,
append y prepend) no tienen
sentido en el ejemplo, puesto que el modelo mantiene la lista de archivos ordenada.
Otros métodos que puede merecer la pena implementar en un
TreeModel que herede de
GenericTreeModel son:
set_value()reorder()swap()move_after()move_before()La implementación de estos métodos es similar a de los métodos anteriores. Es
necesario sincronizar el modelo con el estado externo y luego notificar a las
TreeViews cuando el modelo cambie. Los siguientes métodos
se usan para notificar a las TreeViews de cambios en el modelo
emitiendo la señal apropiada:
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) # filas reordenadas
Uno de los problemas de GenericTreeModel es que
los TreeIters guardan una referencia a un objeto de Python devuelto
por el modelo de árbol personalizado. Puesto que se puede crear e inicializar un
TreeIter en código en C y que permanezca en la pila, no es posible
saber cuándo se ha destruido el TreeIter y ya no se usa la referencia
al objeto de Python. Por lo tanto, el objeto de Python referenciado en un
TreeIter incrementa por defecto su cuenta de referencias, pero no se
decrementa cuando se destruye el TreeIter. Esto asegura que
el objeto de Python no será destruido mientras está en uso por un
TreeIter, lo que podría causar una violación de segmento.
Desgraciadamente, la cuenta de referencias extra lleva a la situación en la que, como bueno, el
objeto de Python tendrá un contador de referencias excesivo, y como malo, nunca se liberará esa
memoria incluso cuando ya no se usa. Este último caso lleva a pérdidas de memoria y el primero a
pérdidas de referencias.
Para prever la situación en la que el TreeModel personalizado
mantiene una referencia al objeto de Python hasta que ya no se dispone de él (es decir, el
TreeIter ya no es válido porque el modelo ha cambiado)
y no hay necesidad de perder referencias, el GenericTreeModel tiene
la propiedad "leak-references". Por defecto "leak-references" es TRUE para
indicar que el GenericTreeModel pierde referencias. Si
"leak-references" se establece a FALSE entonces el contador de referencias
del objeto de Python no se incrementará cuando se referencia en un
TreeIter. Esto implica que el TreeModel
propio debe mantener una referencia a todos los objetos de Python utilizados en los
TreeIters hasta que el modelo sea destruido. Desgraciadamente,
incluso esto no permite la protección frente a código erróneo que intenta utilizar un
TreeIter guardado en un GenericTreeModel
diferente. Para protegerse frente a este caso la aplicación debería mantener referencias
a todos los objetos de Python referenciados desde un TreeIter desde
cualquier instancia de un GenericTreeModel. Naturalmente, esto
tiene finalmente el mismo resultado que la pérdida de referencias.
En PyGTK 2.4 y posteriores los métodos
invalidate_iters() y
iter_is_valid() están disponibles para ayudar en la gestión de
los TreeIters y sus referencias a objetos de Python:
generictreemodel.invalidate_iters()
result = generictreemodel.iter_is_valid(iter)
Estas son particularmente útiles cuando la propiedad "leak-references" está fijada como
FALSE. Los modelos de árbol derivados de
GenericTreeModel están protegidos de problemas con
TreeIters obsoletos porque se comprueba automáticamente la validez
de los iteradores con el modelo de árbol.
Si un modelo de árbol personalizado no soporta iteradores persistentes
(es decir, gtk.TREE_MODEL_ITERS_PERSIST no está activado en el resultado del
método TreeModel.get_flags() ) entonces puede llamar al método
invalidate_iters() para invalidar los
TreeIters restantes cuando cambia el modelo (por ejemplo, tras insertar
una fila). El modelo de árbol también puede deshacerse de cualquier objeto de Python que fue
referenciado por TreeIters tras llamar al método
invalidate_iters().
Las aplicaciones pueden usar el método iter_is_valid()
para determinar si un TreeIter es aún válido en el modelo
personalizado.
Los modelos ListStore y
TreeStore soportan las interfaces
TreeSortable, TreeDragSource
y TreeDragDest además de la interfaz
TreeModel. La clase
GenericTreeModel unicamente soporta la interfaz
TreeModel. Esto parece ser así dada la referencia directa al modelo
en el nivel de C por las vistas TreeView y los modelos
TreeModelSort y
TreeModelFilter models. La creación y uso de
TreeIters precisa código de unión en C que haga de enlace con
el modelo de árbol personalizado en Python que tiene los datos. Ese código de conexión lo aporta
la clase GenericTreeModel y parece que no hay una forma alternativa
de hacerlo puramente en Python puesto que las
TreeViews y los otros modelo llaman a las funciones de GtkTreeModel
en C y pasan sus referencias al modelo de árbol personalizado.
La interfaz TreeSortable también necesitaría código de
enlace en C para funcionar con el mecanismo de ordenación predeterminado de
TreeViewColumn, que se explica en la sección
Ordenación de Filas del Modelo de Árbol.
Sin embargo, un modelo personalizado puede hacer su propia ordenación y una aplicación
puede gestionar el uso de los criterios de ordenación manejando los clic sobre las cabeceras
de las TreeViewColumns y llamando a los métodos personalizados
de ordenación del modelo. El modelo completa la actualización de las vistas
TreeView emitiendo la señal "rows-reordered" utilizando el método
de TreeModel rows_reordered().
Así, probablemente no es necesario que GenericTreeModel implemente
la interfaz TreeSortable.
De la misma manera, la clase GenericTreeModel no necesita
implementar las interfaces TreeDragSource y
TreeDragDest puesto que el modelo de árbol personalizado puede
implementar sus propias interfaces de arrastrar y soltar y la aplicación puede majenar las señales
de TreeView adecuadas y llamar a los métodos propios del modelo
según sea necesario.
La clase GenericTreeModel debería usarse como último
recurso. Existen mecanismos muy poderosos en el grupo estándar de objetos
TreeView que deberían ser suficientes para la mayor parte de
aplicaciones. Sin duda que existen aplicaciones que pueden requerir el suo de
GenericTreeModel pero se debería probar antes a utilizar lo
siguiente:
Funciones de Datos de Celda | Como se ilustra en la sección Función de Datos de Celda, las funciones de datos de
celda se pueden usar para modificar e incluso generar los datos de una columna
de |
Modelo de Árbol Filtrado (TreeModelFilter) | En PyGTK 2.4, el |
Si se acaba usando un GenericTreeModel se debe tener en
cuenta que:
TreeModel completa debe ser creada y
debe funcionar tal como se documentó. Hay sutilezas que pueden conducir a errores. Por el
contrario, los TreeModels estándar están muy revisados.TreeIter puede ser complicada, especialmente en el caso de programas
que se ejecuten durante mucho tiempo y con gran variedad de visualizaciones.TreeIter a objetos de Python y a las filas del modelo en esta
interfaz.