Modelos de datos estándares

Los datos son la parte que nos permite modificar la información que se asocia al GtkTreeView. En realidad el modelo de datos es una interfaz que se debe de implementar por parte de los modelos de datos que utilicemos, aunque los desarrolladores de GTK nos proporcionan como ejemplo dos modelos de datos estándar que pasamos a analizar. En la mayoría de las aplicaciones basta con usar uno de estos modelos de datos, pero como veremos en la próxima sección, a veces estos modelos de datos son insuficientes.

Modelos de datos GtkListStore

El primero modelo, GtkListStore, ya lo hemos podido ver y es el que nos permite gestionar de forma sencilla una lista de elementos. Este modelo es muy útil en casos en los que tengamos una lista simple de elementos a mostrar, como en la lista de la compra.

A continuación exploraremos

Modelos de datos GtkTreeStore

Para cubrir el habitual caso de tener que mostrar una información organizada en más de un nivel, es decir en una estructura de árbol como la del ejemplo de los países y ciudades, disponemos también de un modelo de datos estándar: GtkTreeStore.

Generalidades

Un modelo contiene columnas y filas. Si bien cada fila en el modelo será desplegada de alguna manera en la vista, debe tenerse en cuenta que no necesariamente todas las columnas del modelo lo harán. De hecho, una columna en el modelo no guarda relación directa las columnas de la vista. Una columna del modelo se puede considerar como un campo en una estructura que tiene alguna relevancia para los datos que se quiere representar en la vista, ya sea información a ser desplegada, o atributos de ésta. Por ejemplo, se puede tener una columna con un texto a desplegar y otra con un valor que represente el color de la fuente del texto que se desplegará. Es muy importante tener claro que información se desea almacenar en el modelo, ya que no se pueden agregar o quitar columnas una vez creado el modelo.

Cada columna del modelo almacena elementos de un mismo tipo. Los tipos de datos usan el sistema de tipos de datos de GLib (GType) y los más comúnmente usados son:

  • G_TYPE_BOOLEAN
  • G_TYPE_INT - Para almacenar enteros.
  • G_TYPE_LONG, G_TYPE_ULONG, G_TYPE_FLOAT, G_TYPE_DOUBLE - Varios tipos numéricos.
  • G_TYPE_STRING - Para almacenar un string.
  • GDK_TYPE_PIXBUF - Para almacenar una imagen.

Debe tenerse claro que no es necesario entender el sistema de tipos de GLib para poder usar un GtkTreeView -- Basta con conocerlos. Una de las ventajas del uso de GType es que los usuarios avanzados podrán derivar sus propios tipos de datos, aunque por lo general, esto no es necesario.

Existen dos modos de crear un modelo de datos GtkListStore o GtkTreeStore. El primero es útil cuando la cantidad de columnas que se necesitan es un fija. Para esto utilizamos la función

GtkListStore * gtk_list_store_new (gint n_columns...); 
gint n_columns,  gint n_columns..., ... ;

si se trata de un GtkListStore o gtk_tree_store_new() en el caso de un GtkTreeStore. Los prototipos de ambas funciones son análogos. n_columns es el número de columnas que se desea almacenar en el modelo y la lista variable de parámetros representa una lista ordenada de los n_columns GTypes que se desean almacenar. Por ejemplo, para crear un modelo de lista de datos con 3 columnas, la primera para texto, la segunda para una imagen y la tercera para un número entero, tendremos que llamar a gtk_list_store_new() de la siguiente manera:

      GtkListStore * list_store = gtk_list_store_new (3, G_TYPE_STRING,
                                                         GDK_TYPE_PIXBUF,
                                                         G_TYPE_INT);
      

Por otro lado, si desconocemos la cantidad de columnas que necesitará el modelo, ie. éste debe crearse de manera dinámica, existe una función que recibe como parámetro la cantidad de columnas y un vector de punteros a GType. De este modo, podemos crear dicho vector en tiempo de ejecución y luego utilizarlo para crear el modelo. Esta función, para el caso de GtkTreeStore es:

GtkTreeStore * gtk_tree_store_newv (gint n_columnsGType *types); 
gint n_columns,  gint n_columnsGType *types, GType *types ;

Una vez más, se debe mencionar que existe una función análoga para GtkListStore, gtk_list_store_newv(). Un ejemplo de uso de esta función sería:


    /* crea y retorna un modelo con n_columns columnas */
    GtkTreeModel *
    create_model (gint n_columns)
    {

            GtkListStore *list_store;
            GType *types;
            gint i;

            types = g_new (GType, n_columns);

            /* creamos las columnas del modelo alternando un pixbuf y un
	       string */
            for (i = 0; i < n_columns; i++)
                    types[i] = (i%2 == 0) ? GDK_TYPE_PIXBUF : G_TYPE_STRING;

            list_store = gtk_list_store_newv (n_columns, types);
	    g_free (types);

	    return GTK_TREE_MODEL (list_store);

    }
    

Las columnas son enumeradas a partir de 0, y por lo general se acostumbra a usar enumeraciones para hacer más clara la referencia a las columnas. Usando enumeraciones, podemos agregar un elemento extra a ésta para referirnos a la cantidad de columnas que tendrá el modelo, evitando así el uso de números mágicos. Se puede notar esto en el ejemplo de la “Ejemplo de lista de datos”.

Refiriendose a las filas

A esta altura ya sabemos como crear un modelo, pero no tenemos interes alguno en contar con un modelo que no tiene datos. Pero antes de estudiar conceptos como la añadidura, eliminación, modificación o selección de filas revisaremos los elementos que hacen posibles la referencia a una fila. Como ya se mencionó previamente, una fila es a la vez un nodo dentro de un árbol, a la cual debemos ser capaces de apuntar si queremos modificarla. Es por esto que GTK brinda tres elementos, todos de distinta naturaleza, que nos permitirán referirnos a cada nodo sin complicaciones y de manera transparente. Estos elementos son GtkTreeIter, GtkTreePath y GtkTreeRowReference.

GtkTreeIter: Una referencia directa.

Un GtkTreeIter es una estructura que se utiliza para apuntar a una fila en un modelo y que es soportada tanto por GtkListStore como por GtkTreeStore. Cualquier implementación personalizada de un GtkTreeModel debe ser capaces de soportar las funciones que actuan sobre GtkTreeIter. La composición interna de ésta estructura no es en absoluto relevante para un desarrollador y jamás debe accederse directamente a ésta ni modificarse sin usar las funciones destinadas para ésto.

Enumeraremos algunas de éstas funciones para dar la idea sobre su utilidad.

  • gtk_tree_model_get_iter_first() - Obtiene un GtkTreeIter al primer elemento del nivel superior de un módelo. En un GtkTreeStore éste elemento sería la raíz y en un GtkListStore la primera fila del modelo.
  • gtk_tree_model_get_iter_next() - Obtiene el siguente GtkTreeIter al nivel del Iter dado, si este existe.
  • gtk_tree_model_get_iter_child() - Obtiene un GtkTreeIter apuntando al primer hijo del GtkTreeIter actualmente referenciado. Obviamente, esta función es util para árboles, no para listas.
  • gtk_tree_model_iter_parent() - Dado un GtkTreeIter obtiene el padre de la fila referenciada por éste.

Para detalles sobre los prototipos de éstas y otras funciones, recomendamos ver la API de Referencia de GtkTreeModel.

Los GtkTreeIter son utilizados para almacenar y obtener datos hacia y desde un modelo. Cuando se agregan filas a un modelo se obtiene un GtkTreeIter como resultado. La validez de esta estructura de datos es temporal - tan pronto como el modelo cambie o emita una señal un GtkTreeIter referenciando a una fila en él puede transformarse en inválido, por lo que no debe pensarse jamás en un iter más allá que para acción directa e inmediata sobre un modelo.

Si se observa con cuidado la API de GtkTreeModel se podrá ver que las únicas funciones que actuan única y exclusivamente sobre GtkTreeIter de manera directa son gtk_tree_iter_copy() y gtk_tree_iter_free() y además no se recomienda usarlas en aplicaciones. Esto no hace más que mostrarnos que la utilidad de los iters es solo temporal y no son utiles para, por ejemplo, ser utilizados como datos de usuario en callbacks. Considerando que ni siquiera existe una función constructora, ¿debemos nosotros, desarrolladores de aplicaciones, reservar dinamicamente la memoria para almacenar un GtkTreeIter? La respuesta es no. GtkTreeIter debe ser utilizado solo en el ámbito de la función donde se obtuvo, y es por eso que basta con declararlos de manera estática y usarlos como tal:

      void
      get_data_from_model (GtkTreeModel *model)
      {
              GtkTreeIter iterator;
              gint age;

              if (gtk_tree_model_get_iter_first (model, &iterator)
                  != FALSE) {
                      gtk_tree_model_get (model, &iterator,
                                          COLUMN_AGE, &age,
                                          -1);
              }
              ...
      }
      

GtkTreePath: Una referencia absoluta.

GtkTreeRowReference: Una referencia persistente.

Modelos de datos ordenados

Hasta ahora hemos visto la flexibilidad de GtkTreeView a la hora de visualizar datos. Hemos visto que pueden estar organizados los datos en cualquier tipo de estructura de datos, que podemos crearnos nuestros modelos de datos y con ellos, visualizar los datos de forma sencilla.

Ahora nos toca analizar como facilitar el acceso a dichos datos en el caso de que el volumen a mostrar sea muy grande. Imaginemos que mostramos una lista de nombres, y que dicha lista tenga cientos de entradas. Y ahora pensemos que el usuario quiere localizar un nombre en dicha lista. Si los nombres no aparecen ordenados de alguna forma, el usuario puede tardar mucho tiempo si quiere localizar en dicha lista un nombre. O por ejemplo, pensemos que hay otro campo que sea la edad. Si el usuario quiere localizar por edad a ciertos nombres, lo que realmente va a necesitar es la posibilidad de ordenar todos los campos por edad.

Resumiendo, que necesitamos la funcionalidad que le permita al usuario seleccionar una columna (campo) de los datos y ordenar por ella la visualización de los datos. Y como no podía ser de otra forma, GtkTreeView soporta esta funcionalidad. Para ello tan sólo tenemos que asegurarnos de proporcionar la función o funciones de ordenación para nuestros datos.

Vamos a ver que en los casos más sencillo, de ordenar cadenas u ordenar enteros, la propia GTK se encarga de hacer el trabajo por nosotros. En caso de que queramos ordenar de formas especiales, entoces sí será necesario proporcionar una función de ordenación.

Para convertir un modelo en un modelo ordenado basta con pasar el modelo de datos a la función gtk_tree_model_sort_new_with_model(), que es parte de gtk-2.0/gtk/gtktreemodelsort.h. Para acceder luego al contenido del modelo ordenado, hay que tener cuidado de llamar a las funciones de esta clase a la hora de acceder a los contenidos del modelo de datos.

Por último, ¿cómo indica un usuario que quiere ordenar por una columna? A la hora de crear la columna y visualizarla, como ya veremos en la sección siguiente, llamamos sobre la columna ya creada a la función gtk_tree_view_column_set_sort_column_id().

Vamos a mostrar todo lo hasta ahora expuesto con el ejemplo de la lista de la compra, pero modificado para que soporte ordenación. En nuestro caso, el modelo de datos GtkListStore, basta con indicar que la columna se puede ordenar para pasar a tener la posibilidad de pulsar sobre la cabezera de la columna y que se ordenen los datos. El propio modelo GtkListStore ya incorpora la funcionalidad de ordenación de datos.

Por lo tanto, los únicos cambios a realizar son:

    static void
    add_columns (GtkTreeView *treeview)
    {
            GtkCellRenderer   *renderer;
            GtkTreeModel      *model = gtk_tree_view_get_model (treeview);
            GtkTreeViewColumn *col;

            /* columna de producto */
            renderer = gtk_cell_renderer_text_new ();
            col = gtk_tree_view_column_new_with_attributes ("Producto",
                                                            renderer,
                                                            "text", COLUMN_PRODUCT,
                                                            NULL);
            gtk_tree_view_column_set_sort_column_id (col, COLUMN_PRODUCT);
            gtk_tree_view_append_column (treeview, col);
    }
      

Si ahora compilamos y ejecutamos el programa de la lista de la compra:

Que duda cabe que si nuestro modelo de datos es más complejo, por ejemplo en árbol, deberemos de profundizar mucho más en la metodología de ordenación de datos. Si utilizamos el modelo de datos GtkTreeStore de nuevo tendremos ordenación gratis, al igual que en los casos con modelos de datos sencillos.

Pero si nuestros modelos de datos deben de ordenarse por ejemplo por objetos, tendremos que crear las funciones que especifiquen como se ordenan los objetos.