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 cómo 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 3. 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, si 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 MgGroupModel 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 obtenerdicho 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 4. 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 5. 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