14.11. El Modelo de Árbol Genérico (GenericTreeModel)

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.

14.11.1. Visión general de GenericTreeMode

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.

14.11.2. La Interfaz GenericTreeModel

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:

gtk.TREE_MODEL_ITERS_PERSIST

Los TreeIters sobreviven a todas las señales emitidas por el árbol.

gtk.TREE_MODEL_LIST_ONLY

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.

Figura 14.11. Programa de Ejemplo de Modelo de Árbol Genérico

Programa de Ejemplo de Modelo de Árbol Genérico

14.11.3. Adición y Eliminación de Filas

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)
# fila cambió def row_inserted(path, iter)
# fila insertada def row_has_child_toggled(path, iter)
# cambio en la existencia de hijas en la fila def row_deleted(path)
# fila eliminada def rows_reordered(path, iter, new_order) # filas reordenadas

14.11.4. Gestión de Memoria

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.

14.11.5. Otras Interfaces

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.

14.11.6. Utilización de GenericTreeModel

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 TreeView. Se pueden crear de forma efectiva tantas columnas con datos generados como se precise. Esto proporciona un gran control sobre la presentación de los datos a partir de una fuente de datos subyacente.

Modelo de Árbol Filtrado (TreeModelFilter)

En PyGTK 2.4, el TreeModelFilter que se describe en la sección TreeModelFilter proporciona un gran nivel de control sobre la visualización de las columnas y filas de un TreeModel hijo, incluida la presentación de únicamente las filas hijas de una fila. Las columnas de datos también pueden ser generadas de forma similar al caso de las Funciones de Datos de Celda, pero aquí el modelo parece ser un TreeModel con el número y tipo de columnas especificado, mientras que la función de datos de celda deja intactas las columnas del modelo y simplemente modifica la visualización en una vista TreeView.

Si se acaba usando un GenericTreeModel se debe tener en cuenta que:

  • la interfaz 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.
  • la gestión de referencias a objetos de Python utilizados por iteradores TreeIter puede ser complicada, especialmente en el caso de programas que se ejecuten durante mucho tiempo y con gran variedad de visualizaciones.
  • es necesaria la adición de una interfaz para añadir, borrar y cambiar los contenidos de las filas. Hay cierta complicación con la traducción de los TreeIter a objetos de Python y a las filas del modelo en esta interfaz.
  • la implementación de las interfaces de ordenación y arrastrar y soltar exigen un esfuerzo considerable. La aplicación posiblemente necesite implicarse en hacer que estas interfaces sean completamente funcionales.