GtkTreeView: Árboles y listas en GTK+

Tabla de contenidos

Ejemplos básicos
Modelos de datos estándares
Modelos de datos a medida
GtkTreeView: Visualización
Cortar y pegar en GtkTreeView

Es habitual que se necesiten mostrar estructuras de datos relativamente complejas dentro de las interfaces gráficas de las aplicaciones. Estos datos en muchos casos están relacionados al pertenecer a un mismo conjunto y se pueden mostrar al usuario como una lista. Pero en ocasiones, los elementos de esta lista a su vez son estructuras de datos necesitando mostrarse a su vez como nuevas listas.

Para este tipo de situaciones dentro de GTK2 se ha creado el potente y flexible control (widget) GtkTreeView/Data, un control que sigue el diseño MVC (modelo/vista/controlador) y que como veremos, nos permite mostrar una estructura compleja al usuario de forma tal que para éste, su manejo sea sencillo y rápido.

La idea básica es proporcionar un control que permita representar datos en forma de árbol, donde cada nodo a su vez puede ser un nuevo árbol. Las listas son un caso especial de este caso general ya que son sólo un nivel de nodos simples sin ramificaciones.

Ejemplos básicos

Nada mejor para romper el hielo que un ejemplo simple de una lista basada en GtkTreeView. Para ello hemos cogido dos ejemplos del tutorial de GTK2 y los hemos simplificado. La idea es mostrar al usuario una lista cerrada de la compra, es decir, un conjunto de productos que el usuario tendrá que comprar. En el segundo ejemplo veremos como se puede mostrar información organizada en forma anidada, es decir, donde un elemento de datos puede a su vez tener otros elementos que dependan de él.

Ejemplo de lista de datos

Vamos a ver en el ejemplo varias partes bien diferenciadas. Dentro del main() se crea la ventana principal de la aplicación, la cual contiene una etiqueta y nuestra lista.

Figura 1. Lista de la compra

Lista de la compra

En la parte específica del uso de GtkTreeView nos encontramos con una llamada a create_model(), la cual se encarga de crear el modelo de datos de la lista, el cual será representado a través de la vista, que creamos utilizando este modelo de datos. Tras crear la vista, añadimos las columnas que queremos que el usuario vea y añadimos la vista al contenedor desde el que se visualiza en la pantalla principal.

Lo más importante de este ejemplo es ver como están perfectamente desacoplados los datos y la vista. Vemos que se asocian cuando se crea la vista con el modelo de datos, pero a la hora de visualizar, podemos elegir que parte de los datos se van a mostrar. En este caso los datos sólo tienen un campo, que es el que se muestra, pero como iremos viendo esto puede no ser así.

#include <gtk/gtk.h>
#include <string.h>
#include <stdlib.h>

static GtkWidget *window = NULL;

typedef struct
{
        gchar   *product;
} Item;

enum
{
        COLUMN_PRODUCT,
        NUM_COLUMNS
};

static GArray *articles = NULL;

static void
add_items (void)
{
        Item foo;

        g_return_if_fail (articles != NULL);

        foo.product = g_strdup ("tortilla espa\303\261ola");

        g_array_append_vals (articles, &foo, 1);

        foo.product = g_strdup ("sardinas en aceite");
        g_array_append_vals (articles, &foo, 1);

        foo.product = g_strdup ("pincho moruno");
        g_array_append_vals (articles, &foo, 1);

        foo.product = g_strdup ("arroz con leche");
        g_array_append_vals (articles, &foo, 1);

        foo.product = g_strdup ("natillas");
        g_array_append_vals (articles, &foo, 1);
}

/* Creamos el modelo de datos asociado a la vista */
static GtkTreeModel *
create_model (void)
{
        gint i = 0;
        GtkListStore *model;
        GtkTreeIter iter;

        /* crear matriz */
        articles = g_array_sized_new (FALSE, FALSE, sizeof (Item), 1);

        add_items ();

        /* crear lista de almacen de datos list */
        model = gtk_list_store_new (NUM_COLUMNS, G_TYPE_STRING);

        /* añadir elementos */
        for (i = 0; i < articles->len; i++) {
                gtk_list_store_append (model, &iter);

                gtk_list_store_set (model, &iter,
                                    COLUMN_PRODUCT,
                                    g_array_index (articles, Item, i).product,
                                    -1);
        }

        return GTK_TREE_MODEL (model);
}

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

        /* columna de producto */
        renderer = gtk_cell_renderer_text_new ();
        gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (treeview),
                                                     -1, "Producto", renderer,
                                                     "text", COLUMN_PRODUCT,
                                                     NULL);
}

GtkWidget *
do_cells (void)
{
        if (!window) {
                GtkWidget *vbox;
                GtkWidget *hbox;
                GtkWidget *sw;
                GtkWidget *treeview;
                GtkTreeModel *model;

                /* creamos ventana principal y demás elementos */
                window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
                gtk_window_set_title (GTK_WINDOW (window), "Lista de la compra");
                gtk_container_set_border_width (GTK_CONTAINER (window), 5);
                g_signal_connect (G_OBJECT (window), "destroy",
                                  G_CALLBACK (gtk_widget_destroyed), &window);

                vbox = gtk_vbox_new (FALSE, 5);
                gtk_container_add (GTK_CONTAINER (window), vbox);

                gtk_box_pack_start (GTK_BOX (vbox),
                                    gtk_label_new ("Lista de la compra"),
                                    FALSE, FALSE, 0);

                sw = gtk_scrolled_window_new (NULL, NULL);
                gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (sw),
                                                     GTK_SHADOW_ETCHED_IN);
                gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
                                                GTK_POLICY_AUTOMATIC,
                                                GTK_POLICY_AUTOMATIC);
                gtk_box_pack_start (GTK_BOX (vbox), sw, TRUE, TRUE, 0);

                /* crear el modelo */
                model = create_model ();

                /* crear tree view */
                treeview = gtk_tree_view_new_with_model (model);
                g_object_unref (G_OBJECT (model));

                add_columns (GTK_TREE_VIEW (treeview));

                gtk_container_add (GTK_CONTAINER (sw), treeview);

                gtk_window_set_default_size (GTK_WINDOW (window), 320, 200);
        }

        if (!GTK_WIDGET_VISIBLE (window))
                {
                        gtk_widget_show_all (window);
                } else {
                        gtk_widget_destroy (window);
                        window = NULL;
                }

        return window;
}

int
main (int argc, char *argv[]) {

        gtk_init (&argc, &argv);

        window = GTK_WIDGET (do_cells ());
        g_signal_connect (G_OBJECT (window), "destroy",
                    G_CALLBACK (gtk_main_quit),
                    NULL);
        gtk_widget_show_all (window);

        gtk_main ();
}

      

Ya habiendo visto cada parte resumidamente, iremos desglosando el ejemplo en varias partes de las cuales explicaremos paso a paso que se va a haciendo, por ello que mejor que empezar que con nuestro querido main() :-)

    int
    main (int argc, char *argv[]) {

            gtk_init (&argc, &argv);

            window = GTK_WIDGET (do_cells ());
            gtk_widget_show_all (window);

            gtk_main ();
    }
      

Aquí inicializamos la librería GTK y luego llamamos al método do_cells() que es el que se encargará de crear las ventana donde se dispondrá nuestro GtkTreeView. Posteriormente se visualizará nuestra ventana (mediante la llamada a gtk_widget_show_all()) y se ejecutará el bucle gtk_main().

Ya visto que se llama a la función do_cells() podemos ver que ahí lo único que se hace es crear la ventana y los consecutivos contenedores, además de crear nuestro modelo de datos, nuestro GtkTreeModel a partir de la función create_model() que veremos a continuación. Una vez hecho esto lo cogerá nuestra vista de árbol y se embeberá dentro del sw que está dentro del hbox. Esta parte no es necesaria ver de forma exhaustiva ya que está perfectamente explicada en la parte relativa a GTK por lo que seguro que lo entendéis :)

La parte de create_model() es la que se encarga de crear el modelo de datos de nuestro árbol en el widget model, posteriormente se creará el modelo de vistas del árbol además de la columna que le añadiremos, llamada con la función add_columns().

Ya visto que llama a create_model(), analizaremos dicha función. Esta se encarga de recorrer el arreglo donde hemos metido todos los elementos de la lista de compra y los agrega al final del widget GtkListStore, que para efectos prácticos puede ser entendido como una simple lista a la cual se le agregan nuevos nodos. Para agregar un nuevo elemento a un GtkListStore utilizamos la función gtk_list_store_append(), la cual almacena en iter una referencia al elemento creado, el cual se encuentra momentáneamente vacío. Para almacenar los datos en éste se utiliza gtk_list_store_set(). Una vez completo nuestro modelo lo retornamos para que nuestra función do_cells() pueda crear el widget GtkTreeView referenciando inmediatamente nuestro modelo con este mediante:

    treeview = gtk_tree_view_new_with_model (model);
      

A partir de este momento, cuando necesitemos trabajar con los datos lo haremos únicamente con el GtkListStore, y dichos cambios se reflejarán de forma automática dentro de la vista. Por ejemplo, para añadir un nuevo producto al treeview solo debemos añadirlo al GtkListStore y la vista lo cargará de forma inmediata.

Sólo queda ver el añadido de una celda productos con la función add_columns() sobre nuestra vista de árbol GtkTreeView.

En la función add_columns() utilizaremos un tipo GtkCellRenderer con el texto "producto" el cual insertaremos en el árbol de la siguiente manera:

    renderer = gtk_cell_renderer_text_new ();
    gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (treeview),
                                                 -1, "Producto", renderer,
                                                 "text", COLUMN_PRODUCT,
                                                 NULL);
    

Como podéis ver es bastante sencillo, la única parte no comentada es la de la introducción de datos dentro del array, ya correspondiente a la parte de glib, no por ello complicada, únicamente vamos añadiendo valores sobre el array de la siguiente forma:

        foo.product = g_strdup ("sardinas en aceite");
        g_array_append_vals (articles, &foo, 1);
      

Luego estos datos serán los que utilicemos en la lista para almacenar los datos y conseguir nuestro modelo de árbol GtkTreeModel.

Como podéis ver no es complicado, únicamente hay que diferenciar entre el modelo de datos que es el que utilizamos para toda la manipulación del contenido del árbol y luego el GtkTreeView que es el que utilizaremos para la parte correspondiente a la muestra por pantalla de ese modelo de datos ya creado.

Para concluir este primer ejemplo, resumir que hemos utilizado un modelo de datos GtkListStore que hemos podido utilizar de forma sencilla y asociarlo a la vista pero, ¿y si nuestros datos tienen una estructura más compleja? ¿Y si no son simplemente cadenas de caractéres si no que son completos objetos con mucha información? ¿Es capaz GtkTreeView de visualizar estos objetos según nuestras necesidades?

La respuesta es que sí. Tenemos el modelo de datos GtkListStore para los habituales casos de tratamiento de cadenas de caracteres en una lista y GtkTreeStore para las cadenas de caractéres que se organizan en árbol. Es muy útil tenerlos ya predefinidos ya que los utilizaremos muy a menudo. Pero nada nos impide crearnos nuestros propios modelos de datos, tal y como vamos a mostrar en el siguiente apartado.

Ejemplo de árbol de datos

A continuación veremos un ejemplo que nos permitirá explicar el uso básico del modelo GtkTreeStore. Este modelo es una versión genérica de lo que un programador podría querer hacer con un GtkTreeModel que, al igual que GtkListStore, simplifica mucho las cosas. Las simplifica, básicamente, porque siendo la estructura de árbol tan común y utilizada dentro de las aplicaciones, no existe ninguna razón para dejar los detalles del diseño del módelo a cualquiera que los necesite. Un ejemplo típico podría ser una estructura de directorios, un árbol de dependencias, o una lista de países y ciudades de éstos, que en esencia es el ejemplo que veremos a continuación.

Implementaremos una pequeña ventana con un GtkTreeView en su interior que muestre en una sola columna una lista de países expandible que nos muestre algunas ciudades de estos. Una pantalla de éste ejemplo es:

Figura 2. Países y algunas de sus ciudades

Países y algunas de sus ciudades

Como puede apreciarse, existe una jerarquía clara entre los elementos del GtkTreeView. Tenemos tres elementos (los países) en un mismo nivel. Si obviamos por un instante las ciudades, tendremos simplemente una lista. Ahora bien, considerando que cada ciudad está asociada a algún país, es lógico ver a éstas como hijos de los países. Cada ciudad o país (en general, cualquier fila en un GtkTreeStore) no es más que un nodo dentro de un árbol, que contiene los mismos datos que el resto de éstos (en el ejemplo, sólo el nombre de la ciudad o país), pero que se relaciona con los otros mediante una relación jerárquica que se especifica al momento de llenar el modelo.

Sin más preámbulo, demos un vistazo al código completo.


#include <gtk/gtk.h>

enum {
        COL_DATA,
        COL_TOTAL
};

GtkTreeModel *
create_model (void)
{
        GtkTreeStore *model;
        GtkTreeIter top;
        GtkTreeIter child;

        model = gtk_tree_store_new (COL_TOTAL, G_TYPE_STRING);

        gtk_tree_store_append (model, &top, NULL);
        gtk_tree_store_set (model, &top,
                            COL_DATA, "Argentina",
                            -1);

        gtk_tree_store_append (model, &child, &top);
        gtk_tree_store_set (model, &child,
                            COL_DATA, "Buenos Aires",
                            -1);

        gtk_tree_store_append (model, &child, &top);
        gtk_tree_store_set (model, &child,
                            COL_DATA, "Mendoza",
                            -1);

        gtk_tree_store_append (model, &child, &top);
        gtk_tree_store_set (model, &child,
                            COL_DATA,
                            "C\303\263rdoba",
                            -1);

        gtk_tree_store_append (model, &top, NULL);
        gtk_tree_store_set (model, &top,
                            COL_DATA, "Chile",
                            -1);

        gtk_tree_store_append (model, &child, &top);
        gtk_tree_store_set (model, &child,
                            COL_DATA,
                            "Valpara\303\255so",
                            -1);

        gtk_tree_store_append (model, &child, &top);
        gtk_tree_store_set (model, &child,
                            COL_DATA, "Santiago",
                            -1);

        gtk_tree_store_append (model, &child, &top);
        gtk_tree_store_set (model, &child,
                            COL_DATA,
                            "Concepci\303\263n",
                            -1);

        gtk_tree_store_append (model, &top, NULL);
        gtk_tree_store_set (model, &top,
                            COL_DATA,
                            "Espa\303\261a",
                            -1);

        gtk_tree_store_append (model, &child, &top);
        gtk_tree_store_set (model, &child,
                            COL_DATA, "Barcelona",
                            -1);

        gtk_tree_store_append (model, &child, &top);
        gtk_tree_store_set (model, &child,
                            COL_DATA, "Madrid",
                            -1);

        gtk_tree_store_append (model, &child, &top);
        gtk_tree_store_set (model, &child,
                            COL_DATA,
                            "Zaragoza",
                            -1);

        return GTK_TREE_MODEL (model);

}

GtkWidget *
create_view_and_model (void)
{
        GtkWidget *view;
        GtkTreeModel *model;
        GtkTreeViewColumn *col;
        GtkCellRenderer *renderer;

        view = gtk_tree_view_new ();

        col = gtk_tree_view_column_new ();
        gtk_tree_view_column_set_title (col, "Paises/Ciudades");

        gtk_tree_view_append_column (GTK_TREE_VIEW (view), col);

        renderer = gtk_cell_renderer_text_new ();
        gtk_tree_view_column_pack_start (col, renderer, TRUE);
        gtk_tree_view_column_add_attribute  (col, renderer, "text",
                                            COL_DATA);

        model = create_model ();

        gtk_tree_view_set_model (GTK_TREE_VIEW (view), model);

        g_object_unref (model);

        return view;
}

gboolean
app_quit (GtkWidget *widget, GdkEvent *event,
          gpointer data)
{
        gtk_main_quit ();
        return TRUE;
}

int
main (int argc, char **argv)
{
        GtkWidget *window;
        GtkWidget *view;

        gtk_init (&argc, &argv);

        window = gtk_window_new (GTK_WINDOW_TOPLEVEL);

        g_signal_connect (window, "delete_event",
                          G_CALLBACK (app_quit), NULL);

        view = create_view_and_model ();

        gtk_container_add (GTK_CONTAINER (window), view);

        gtk_widget_show_all (window);

        gtk_main ();

        return 0;
}

    

Como se puede apreciar en el código, no hemos seguido el mismo estilo del ejemplo anterior con respecto a la creación del modelo y la vista. En el ejemplo anterior, creamos el modelo, luego lo asociamos a una vista mientras creamos ésta mediante gtk_tree_view_new_with_model() y por último creamos sus columnas. En este ejemplo creamos primero que nada la vista y sus columnas, luego creamos y llenamos el modelo para finalmente relacionar vista y modelo mediante gtk_tree_view_set_model(). Hemos hecho esto para hacer énfasis en la independencia entre el modelo y la vista. Del mismo modo, debemos mencionar que podemos relacionar un sólo modelo con varias vistas distintas.

En nuestra función main() hemos creado dos widgets, la ventana y la vista de árbol, para luego embeber una dentro de la otra. La creación concreta de la vista se realiza en la función create_view_and_model() mediante gtk_tree_view_new(). Luego procedemos a crear la columna que contendrá el nombre de la ciudad o país y la integramos a la vista. La creación específica del modelo la hacemos en la función create_model() que analizaremos más adelante. Ahora solo queda integrar el modelo a la vista. Hacemos esto mediante gtk_treeview_set_model(). La llamada a g_object_unref() es necesaria para que cuando destruyamos la vista se haga lo mismo con el modelo. En caso contrario, el modelo tendrá todavía una referencia al destruir la vista y seguirá en el entorno.

Como es lógico, nos enfocaremos en la función create_model() que es la de mayor relevancia para el ejemplo. En ésta función comenzamos creando el modelo, con:

model = gtk_tree_store_new (COL_TOTAL, G_TYPE_STRING);

Donde el primer parámetro es el total de columnas en el modelo (en nuestro ejemplo, sólo una) y a continuación especificamos el tipo de dato que contendrá cada una de dichas columnas.

Para agregar un nuevo elemento al modelo GtkTreeStore utilizamos la función gtk_tree_store_append(). Esta función necesita tres parametros, el modelo al cual queremos agregar un elemento, un GtkTreeIter para almacenar una referencia a este elemento y otro que indica el elemento al cual anidaremos el nuevo. Este segundo GtkTreeIter debe haber sido creado previamente, y si se desease añadir un elemento en el nivel principal (como es el caso de los países en el ejemplo) basta con pasar NULL como padre. He ahí la diferencia radical entre las siguientes líneas de código:

    gtk_tree_store_append (model, &top, NULL);
    ...
    gtk_tree_store_append (model, &child, &top);
    

La primera llamada a gtk_tree_store_append() crea un nuevo elemento de nivel cero y la siguiente crea un elemento que será hijo de ésta.

Llenar un elemento en los modelos GtkTreeModel y GtkListStore es una tarea prácticamente idéntica. Se utiliza para esto la función gtk_tree_model_set() que análogamente a gtk_list_store_set() necesita como parámetros el modelo, el iterador, y una lista variable de los valores a definir. En nuestro ejemplo, podemos ver una de las llamadas a dicha función en la siguientes líneas:

    gtk_tree_store_set (model, &child,
                        COL_DATA, "Mendoza",
                        -1);