GtkTreeModel: El modelo de datos

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.

Modelos de datos estándar

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.

Para cubrir el habitual caso de tener que mostrar una información organizada en más de un nivel, es estructura de árbol, disponemos también de un modelo de datos estándar: "gtktreestore".

Modelos de datos a medida

Está claro que, a pesar del gran número de casos que podemos cubrir con los dos modelos estándar, no siempre nuestros datos serán cadenas de caracteres. Es aquí donde aparece la gran potencia de GtkTreeView ya que el modelo de datos que podemos usar es cualquier que nosotros definamos. Tan sólo deberemos de implementar las interfaces adecuadas.

Y nada mejor que un ejemplo de un desarrollo real donde se tuvo esa necesidad para mostrar como se resolvió: el gestor de proyectos MrProject. En este proyecto se ha utilizado de varias formas el widget de GtkTreeView y nos va a servir para analizar las posibles combinaciones de su uso. En el siguiente diagrama UML podemos ver como se implementa la interfaz de los modelos de datos de un GtkTreeView siguiendo diferentes aproximaciones en función de la necesidad que se tenga. Por ejemplo, en muchas ocasiones se necesitaba mostrar datos en forma de lista, estando dichos datos contenidos dentro de las propiedades de objetos. Por ello se definió la clase MgListModel que facilitó mucho la implementación de este tipo de modelos de datos.

En otra ocasión, se necesitó mostrar en forma de árbol datos de objetos, relacionados entre sí, por lo que se implementó de forma directa la interfaz de modelos de datos.

Y en otras ocasiones, se utilizó los modelos de datos predefinidos GtkTreeStore y GtkListStore. Todo ello lo vemos resumido en el siguiente diagrama UML.

Figura 2. UML de interfaces para modelos de datos

UML de interfaces para modelos de datos

Vamos a comenzar analizando MgListModel y como simplifica en gran medida la creación de los modelos de datos que se usan en MrProject, que serán bastante similares a los que se puede encontrar en otro tipo de proyectos.

El modelo de datos lo constituyen una serie de objetos que son los que se van a visualizar dentro de la interfaz gráfica. Estos objetos tienen mucha información y no queremos mostrar toda, si no únicamente parte de la misma.

Para poder implementar un nuevo modelo de datos debemos de implementar la interfaz "GtkTreeModelIface" que se puede consultar dentro del fichero de cabeceras de gtk "/usr/include/gtk-2.0/gtk/gtktreemodel.h". que es precisamente lo que hace la clase MgListModel, en la que se basarán muchos de los modelos de datos posteriores en MrProject. Vamos con el análisis de esta clase. Vamos a centrarnos en las partes del código que nos interesan. Si el lector quiere consultar los fuentes completos, no tiene mas que obtener el código fuente de MrProject e ir a los ficheros que vamos a ir indicando. En primer lugar comenzamos con "mg-list-model.h".

La interfaz MgListModel

Ha llegado el momento analizar esta interfaz, que implementa gran parte de GtkTreeModelIface, pero dejando sin implementar los métodos fundamentales dentro de las necesidades de los modelos de datos de MrProject.

struct _MgListModel
{
        GObject          parent;
	
        MgListModelPriv *priv;
};

struct _MgListModelClass
{
	GObjectClass parent_class;

	gint  (*get_n_columns)   (GtkTreeModel *tree_model);
	GType (*get_column_type) (GtkTreeModel *tree_model,
				  gint          column);
	void  (*get_value)       (GtkTreeModel *tree_model,
				  GtkTreeIter  *iter,
				  gint          column,
				  GValue       *value);
};

GType            mg_list_model_get_type      (void);
void             mg_list_model_append        (MgListModel      *model, 
					      MrpObject        *object);
void             mg_list_model_remove        (MgListModel      *model, 
					      MrpObject        *object);
void             mg_list_model_update        (MgListModel      *model,
					      MrpObject        *object);
GtkTreePath *    mg_list_model_get_path      (MgListModel      *model,
					      MrpObject        *object);
MrpObject *      mg_list_model_get_object    (MgListModel      *model,
					      GtkTreeIter      *iter);
void             mg_list_model_set_data      (MgListModel      *model,
					      GList            *data);
GList *          mg_list_model_get_data      (MgListModel      *model);
      

Vemos que la clase implementa parte de la interfaz GtkTreeModelIface aunque deja tres métodos sin implementar, que serán los únicos que tendrán que implementar las clases que utilicen como interfaz de MgListModel: get_n_columns, get_column_type y get_value.

La interfaz GtkTreeModelIface es mucho más amplia, pero MgListModel se encarga de realizar una implementación por defecto de todos los demás métodos. Para verlo vamos a analizar el fichero "mg-list-model.c" y en concreto, el inicializador de la clase y la construcción propiamente dicha de la clase.

GtkType
mg_list_model_get_type (void)
{
        static GType type = 0;
        
        if (!type) {
                static const GTypeInfo info =
                        {
                                sizeof (MgListModelClass),
                                NULL,		/* base_init */
                                NULL,		/* base_finalize */
                                (GClassInitFunc) mlm_class_init,
                                NULL,		/* class_finalize */
                                NULL,		/* class_data */
                                sizeof (MgListModel),
                                0,
                                (GInstanceInitFunc) mlm_init,
                        };
                
                static const GInterfaceInfo tree_model_info =
                        {
                                (GInterfaceInitFunc) mlm_tree_model_init,
                                NULL,
                                NULL
                        };

                type = g_type_register_static (G_TYPE_OBJECT,
					       "MgListModel", 
					       &info, 0);
      
                g_type_add_interface_static (type,
                                             GTK_TYPE_TREE_MODEL,
                                             &tree_model_info);
        }
        
        return type;
}
      

La parte que nos interesa del constructor de la clase es donde indicamos que nuestra clase implementa la interfaz "GTK_TYPE_TREE_MODEL", algo que vamos a ver en el inicializador de la clase, "mlm_tree_model_init" y, se nos fijamos en el constructor de clase "mlm_class_init", vamos a ver los tres métodos que se dejan sin implementar de la interfaz de GtkTreeModelIface.

static void
mlm_class_init (MgListModelClass *klass)
{
        GObjectClass *object_class;

        parent_class = g_type_class_peek_parent (klass);
        object_class = (GObjectClass*) klass;

        object_class->finalize = mlm_finalize;

	klass->get_n_columns   = NULL;
	klass->get_column_type = NULL;
	klass->get_value       = NULL;
}
      

Estos tres métodos son los que dejamos obligatorios para que sean implementados por cada uno de los modelos de datos que se basen en esta interfaz.

Veamos ahora como nuestra clase implementa los demás métodos de la interfaz.

mlm_tree_model_init (GtkTreeModelIface *iface)
{
        iface->get_iter        = mlm_get_iter;
        iface->get_path        = mlm_get_path;
        iface->iter_next       = mlm_iter_next;
        iface->iter_children   = mlm_iter_children;
        iface->iter_has_child  = mlm_iter_has_child;
        iface->iter_n_children = mlm_iter_n_children;
        iface->iter_nth_child  = mlm_iter_nth_child;
        iface->iter_parent     = mlm_iter_parent;

	/* Llama al método class->function por lo que las subclases pueden poner sus propios métodos */
	iface->get_n_columns   = mlm_get_n_columns;
	iface->get_column_type = mlm_get_column_type;
	iface->get_value       = mlm_get_value;
}
      

Comencemos precisamente con estos tres últimos métodos, por ejemplo con el que nos permite obtener el número de columnas de un modelo de datos.

static gint
mlm_get_n_columns (GtkTreeModel *tree_model)
{
	MgListModelClass *klass;
	
	klass = MG_LIST_MODEL_GET_CLASS (tree_model);
	
	if (klass->get_n_columns) {
		return klass->get_n_columns (tree_model);
	}

	g_warning ("Tienes que implementar get_n_columns!");

	return -1;
}
      

Vemos como en este método, se obtiene la clase real del objeto que está siendo utilizado, por ejemplo MgGroupModel, y se intenta invocar al método "get_n_columns" de esta clase. En el caso de que la clase que dice implementar MgListModel no lo haga, se saca un mensaje de aviso y se devuelve "-1".

Veamos ahora un ejemplo de uno de los métodos que si que implementa esta interfaz, clase abstracta en terminología de C++, y que evita que los modelos de datos basados en esta interfaz tenga que realizar esta labor. Para ser capaces de realizar esta implementación, nos basamos en que el modelo en realidad es una lista de objetos. Este es el único requisito. Si quisieramos una estructura de datos en árbol, no nos valdrá esta interfaz para ayudarnos en nuestra labor.

void
mg_list_model_update (MgListModel *model, MrpObject *object)
{
	MgListModelPriv *priv;
        GtkTreePath     *path;
	GtkTreeIter      iter;
        gint             i;

	g_return_if_fail (MG_IS_LIST_MODEL (model));
	g_return_if_fail (MRP_IS_OBJECT (object));

	priv = model->priv;

	i = g_list_index (priv->data_list, object);

	path = gtk_tree_path_new ();
	gtk_tree_path_append_index (path, i);

	gtk_tree_model_get_iter (GTK_TREE_MODEL (model), &iter, path);

	gtk_tree_model_row_changed (GTK_TREE_MODEL (model), path, &iter);

	gtk_tree_path_free (path);
}
        

Vemos que para implementar el método que nos permite actualizar un elemento del modelo de datos, lo localizamos en la lista de objetos (g_list_index) y utilizando los métodos que nos proporciona el propio GtkTreeModel, provocamos el cambio en el modelo de datos. Este cambio se propagará a todas las interfaces gráficas que utilicen este modelo de datos y se actualizarán para reflejar los cambios.

MgGroupModel: El modelo de datos completo

Hasta el momento no hemos creado mas que una clase que nos facilita implementar modelos de datos específicos basados en la idea de que son una lista de objetos de los que queremos mostrar ciertas características. Por ello nos basta con que la parte que cambia de un modelo de datos a otro es el número de columnas a mostrar, el tipo de cada columna y el valor que almacena. En el caso de "mg_group_model" dichos métodos se implementan en:

mgm_class_init (MgGroupModelClass *klass)
{
        GObjectClass     *object_class;
	MgListModelClass *lm_class;
	
        parent_class = g_type_class_peek_parent (klass);
        object_class = G_OBJECT_CLASS (klass);
	lm_class     = MG_LIST_MODEL_CLASS (klass);
	
        object_class->finalize = mgm_finalize;

	lm_class->get_n_columns   = mgm_get_n_columns;
	lm_class->get_column_type = mgm_get_column_type;
	lm_class->get_value       = mgm_get_value;
}
        

La implementación de "mgm_get_n_columns" y "mgm_get_column_type" es la siguiente:

static gint
mgm_get_n_columns (GtkTreeModel *tree_model)
{
        return NUMBER_OF_GROUP_COLS;
}

static GType
mgm_get_column_type (GtkTreeModel *tree_model,
		      gint          column)
{
        switch (column) {
        case GROUP_COL_NAME:
        case GROUP_COL_MANAGER_NAME:
        case GROUP_COL_MANAGER_PHONE:
        case GROUP_COL_MANAGER_EMAIL:
                return G_TYPE_STRING;
		
        case GROUP_COL_GROUP_DEFAULT:
                return G_TYPE_BOOLEAN;

	default:
		return G_TYPE_INVALID;
        }
}
        

que se basan en que tenemos una unión que contiene todas las columnas que son visibles de un grupo. El añadir nuevas columnas sería muy sencillo ya que bastaría con añadirlas a la unión de columnas y actualizar los métodos que realizan el tratamiento de dichas columnas. De forma automática las interfaces gráficas se verían también actualizadas. De igual forma se podría eliminar una columna, o permitir desde algún diálogo especificar que columnas se quieren ver en la interfaz y cuales no. Veamos esa unión con las columnas:

enum {
        GROUP_COL_NAME,
        GROUP_COL_GROUP_DEFAULT,
        GROUP_COL_MANAGER_NAME,
        GROUP_COL_MANAGER_PHONE,
        GROUP_COL_MANAGER_EMAIL,
        NUMBER_OF_GROUP_COLS
};
        

El método quizá más complejo es el que se encarga de obtener los valores de cada una de las columnas. Estos valores se obtienen del objeto que se está mostrando actualmente, y en nuestro caso, veremos que en su mayoría son propiedades del objeto.

static void
mgm_get_value (GtkTreeModel *tree_model,
		GtkTreeIter  *iter,
		gint          column,
		GValue       *value)
{
        gchar            *str = NULL;
	MrpGroup         *group, *default_group;
	MgGroupModelPriv *priv;
	gboolean          is_default;

        g_return_if_fail (MG_IS_GROUP_MODEL (tree_model));
        g_return_if_fail (iter != NULL);

	priv = MG_GROUP_MODEL (tree_model)->priv;
        group = MRP_GROUP (mg_list_model_get_object (
				   MG_LIST_MODEL (tree_model), iter));
        

Aquí es donde hemos obtenido de la lista de objetos (mg_list_model_get_object) el objeto, en nuestro caso un MrpGroup, del que queremos mostrar la información. Vemos que hacemos uso de uno de los métodos públicos de la clase MgListModel para obtener dicho objeto de la lista. Ahora, en función de la columna que nos estén pidiendo de dicho MrpGroup, devolvemos un valor u otro.

        
        switch (column) {
        case GROUP_COL_NAME:
                mrp_object_get (group, "name", &str, NULL);
		g_value_init (value, G_TYPE_STRING);
		g_value_set_string (value, str);
		g_free (str);
                break;

	case GROUP_COL_GROUP_DEFAULT:
		g_object_get (priv->project,
			      "default-group", &default_group,
			      NULL);

		is_default = (group == default_group);
		
                g_value_init (value, G_TYPE_BOOLEAN);
                g_value_set_boolean (value, is_default);
                break;

        case GROUP_COL_MANAGER_NAME:
                mrp_object_get (group, "manager_name", &str, NULL);
                g_value_init (value, G_TYPE_STRING);
		g_value_set_string (value, str);
		g_free (str);
                break;

	case GROUP_COL_MANAGER_PHONE:
                mrp_object_get (group, "manager_phone", &str, NULL);
                g_value_init (value, G_TYPE_STRING);
		g_value_set_string (value, str);
		g_free (str);
		
                break;

	case GROUP_COL_MANAGER_EMAIL:
                mrp_object_get (group, "manager_email", &str, NULL);
                g_value_init (value, G_TYPE_STRING);
		g_value_set_string (value, str);
		g_free (str);
		
                break;

	default:
                g_assert_not_reached ();
        }
}
        

y estos valores que hemos obtenido aquí serán los que se muestren dentro de la interfaz gráfica. Y ha llegado el momento de mostrar como aparece todo esto dentro de la interfaz gráfica de MrProject.

Figura 3. Grupos en MrProject

Grupos en MrProject

Implementación completa de modelo de datos

Lo que hemos analizado hasta ahora es aplicable a los casos en los que tengamos una lista de datos a visualizar pero no siempre es así, lo que por ejemplo dentro de MrProject ha obligado a que algunos modelos de datos, como el de la vista Gantt, sean implementados por completo desde cero.

Figura 4. Vista Gantt con tareas anidadas

Vista Gantt con tareas anidadas

Para resolver este modelo de datos, se ha tenido que crear un modelo dentro de mg-gantt-model.c/h que implementa por completo todos los métodos de la interfaz GtkTreeModelIface. Veámoslo:

GType
mg_gantt_model_get_type (void)
{
	static GType type = 0;
...
		static const GInterfaceInfo tree_model_info = {
			(GInterfaceInitFunc) gantt_model_tree_model_init,
			NULL,
			NULL
		};
...
}
        

que nos indica que el método que utilizamos para inicializar la interfaz GInterfaceInfo que implementa esta clase es "gantt_model_tree_model_init".

static void
gantt_model_tree_model_init (GtkTreeModelIface *iface)
{
	iface->get_n_columns = gantt_model_get_n_columns;
	iface->get_column_type = gantt_model_get_column_type;
	iface->get_iter = gantt_model_get_iter;
	iface->get_path = gantt_model_get_path;
	iface->get_value = gantt_model_get_value;
	iface->iter_next = gantt_model_iter_next;
	iface->iter_children = gantt_model_iter_children;
	iface->iter_has_child = gantt_model_iter_has_child;
	iface->iter_n_children = gantt_model_iter_n_children;
	iface->iter_nth_child = gantt_model_iter_nth_child;
	iface->iter_parent = gantt_model_iter_parent;
}
        

Vemos que aquí implementamos por completo todas las funciones de la interfaz GtkTreeModelIface y que, entre otras funciones, permiten que los datos se organicen en estructuras de árbol.

[FIXME] Destacar las partes más importantes de la implementación en árbol

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 fromas 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 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.