<- PW: AVI - Indice Generale - Copertina - SL: Intro ->

PlutoWare


Introduzione alle liste di GTK+-2.0

L'articolo

Lo scorso giugno il desktop manager GNOME è finalmente giunto alla versione 2.0.0 (versione in verità rivolta soprattutto agli sviluppatori), rinnovando così la sfida volta ad ottenere un desktop semplice da utilizzare e bello da vedere. Come sappiamo, GNOME è basato sulle librerie GTK+, anch'esse giunte alla versione 2.0 da qualche tempo. Già nello scorso numero del Journal abbiamo effettuato una panoramica sulle novità introdotte dalle nuove GTK+; da questo inizieremo a conoscere i nuovi widget, iniziando da quelli destinati al trattamento delle liste e degli alberi. In webografia troverete gli indirizzi della documentazione delle nuove API di GTK+.




Introduzione

    Il nuovo widget di GTK+ per la gestione delle liste e degli alberi è composto in realtà da un insieme di widget. Per creare una lista o un albero con le nuove librerie, è necessario infatti utilizzare un oggetto GtkTreeModel e un oggetto GtkTreeView. Questo perché il nuovo widget è realizzato secondo il pattern Model/View/Controller.

In pratica il nostro oggetto è composto da tre parti: il modello, la vista ed il controllore. Il modello rappresenta i dati che dobbiamo trattare (la lista, l'albero); la vista rappresenta il modo in cui i dati (il modello) devono essere visualizzati; il controllore, che è in genere la nostra interfaccia grafica, ci permette di interagire con il modello e modificare, aggiungere, eliminare i dati in esso contenuti.

I widget che fanno parte del "View" sono GtkTreeView, GtkTreeViewColumn e GtkCellRenderer; i widget che generalmente sono usati per creare il modello sono GtkListStore (per le liste) e GtkTreeStore (per gli alberi), mentre se si ha la necessità di creare un particolare modello si ricorre a GtkTreeModel. Ma qual'è il vantaggio derivante da ciò, a cosa serve questa diversificazione? Una maggiore flessibilità, in primis: ad esempio è possibile avere più viste, o viste diverse, degli stessi dati. Pensiamo ad un file manager in stile Midnight Commander che visualizza lo stesso file system (i dati, il modello creato una sola volta) in due differenti finestre, ognuna delle quali mostra una differente directory, ma con impostazioni comuni, e comunque alterabili dall'utente in maniera indipendente e integrata nell'applicazione. Questo grazie alla separazione tra dati e visualizzazione.

In generale i dati saranno visualizzati in modo tabellare, disporremo quindi di righe, ognuna delle quali divise in una o più colonne (in funzione del modello che abbiamo creato) in cui disporre i dati. I dati appartenenti alla stessa colonna dovranno essere della stessa natura (quindi in una certa colonna si potranno inserire solo stringhe o interi o immagini, eccetera)

Il programma di prova

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

GtkWidget *entry1;
GtkWidget *entry2;


enum {
  COLONNA_STRINGHE,
  COLONNA_NUMERI,
  COLONNE
};


void aggiungi(GtkButton *button, gpointer data)
{
  GtkTreeView *view = GTK_TREE_VIEW(data);
  GtkTreeModel *model;
  GtkTreeIter iter;
  gchar *str;
  gchar *num;
  
  model = gtk_tree_view_get_model(GTK_TREE_VIEW(view));
  str = g_strdup(gtk_entry_get_text(GTK_ENTRY(entry1)));
  num = g_strdup(gtk_entry_get_text(GTK_ENTRY(entry2)));
  
  gtk_list_store_append(GTK_LIST_STORE(model), &iter);
  gtk_list_store_set(GTK_LIST_STORE(model), &iter, 
		     COLONNA_STRINGHE, str, 
		     COLONNA_NUMERI, atoi(num), 
		     -1);

  gtk_entry_set_text(GTK_ENTRY(entry1),"");
  gtk_entry_set_text(GTK_ENTRY(entry2),"");  
}


void modifica(GtkButton *button, gpointer data)
{
  GtkTreeView *view = GTK_TREE_VIEW(data);
  GtkTreeModel *model;
  GtkTreeIter iter;
  GtkTreeSelection *selection;
  gchar *str = NULL;
  gchar *num = NULL;

  model = gtk_tree_view_get_model(GTK_TREE_VIEW(view));
  selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));

  str = g_strdup(gtk_entry_get_text(GTK_ENTRY(entry1)));
  num = g_strdup(gtk_entry_get_text(GTK_ENTRY(entry2)));

  if (gtk_tree_selection_get_selected(selection, NULL, &iter))
  {
    gint i;
    GtkTreePath *path;
    
    path = gtk_tree_model_get_path(model, &iter);
    i = gtk_tree_path_get_indices(path)[0];
    
    gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
    gtk_tree_path_free(path);
    
    gtk_list_store_insert(GTK_LIST_STORE(model), &iter, i);
    gtk_list_store_set(GTK_LIST_STORE(model), &iter, 
		       COLONNA_STRINGHE, str, 
		       COLONNA_NUMERI, atoi(num), 
		       -1);

    gtk_entry_set_text(GTK_ENTRY(entry1),"");
    gtk_entry_set_text(GTK_ENTRY(entry2),"");      
  }  
}


void cancella(GtkButton *button, gpointer data)
{
  GtkTreeView *view = GTK_TREE_VIEW(data);
  GtkTreeModel *model;
  GtkTreeIter iter;
  GtkTreeSelection *selection;
  
  model = gtk_tree_view_get_model(GTK_TREE_VIEW(view));
  selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
  
  if (gtk_tree_selection_get_selected(selection, NULL, &iter))
  {
    gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
   
    gtk_entry_set_text(GTK_ENTRY(entry1),"");
    gtk_entry_set_text(GTK_ENTRY(entry2),"");
  }  
}


void selection_changed(GtkTreeSelection *selection, gpointer data)
{
  GtkTreeModel *model;
  GtkTreeIter iter;
  gchar *str;
  gint num;

  if (gtk_tree_selection_get_selected(selection, &model, &iter))
    {
      gtk_tree_model_get(model, &iter, 
			 COLONNA_STRINGHE, &str, 
			 COLONNA_NUMERI, &num, 
			 -1);
      
      gtk_entry_set_text(GTK_ENTRY(entry1), str);
      g_free(str);
      
      str = g_strdup_printf("%d", num);
      gtk_entry_set_text(GTK_ENTRY(entry2), str);  
      g_free(str);
    }
}



GtkWidget*
create_window (void)
{
  GtkWidget *window;
  GtkWidget *hbox1;
  GtkWidget *hbox2;
  GtkWidget *hbox3;
  GtkWidget *vbox;
  GtkWidget *button1;
  GtkWidget *button2;
  GtkWidget *button3;
  GtkWidget *button4;
  GtkWidget *label1;
  GtkWidget *label2;
  GtkWidget *hseparator;
  GtkWidget *scrolledwindow;

  GtkListStore *model;              /* l'oggetto model   */
  
  GtkWidget *view;                  /* -\                  */ 
  GtkCellRenderer *renderer;        /* --> l'oggetto view  */
  GtkTreeSelection *selection;      /* -/                  */


  window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  gtk_window_set_title (GTK_WINDOW (window), "Gtk+2.0 Usiamo le liste");

  hbox1 = gtk_hbox_new (FALSE, 0);
  gtk_container_add (GTK_CONTAINER (window), hbox1);
  gtk_widget_show (hbox1);

  vbox = gtk_vbox_new (TRUE, 5);
  gtk_box_pack_start (GTK_BOX (hbox1), vbox, FALSE, FALSE, 0);
  gtk_container_set_border_width (GTK_CONTAINER (vbox), 5);
  gtk_widget_show (vbox);

  button1 = gtk_button_new_with_label("Aggiungi");
  gtk_box_pack_start (GTK_BOX (vbox), button1, FALSE, FALSE, 0);
  gtk_widget_show (button1);

  button2 = gtk_button_new_with_label("Modifica");
  gtk_box_pack_start (GTK_BOX (vbox), button2, FALSE, FALSE, 0);
  gtk_widget_show (button2);

  button3 = gtk_button_new_with_label("Elimina");
  gtk_box_pack_start (GTK_BOX (vbox), button3, FALSE, FALSE, 0);
  gtk_widget_show (button3);

  hbox2 = gtk_hbox_new (FALSE, 0);
  gtk_box_pack_start (GTK_BOX (vbox), hbox2, FALSE, FALSE, 0);
  gtk_widget_show (hbox2);

  label1 = gtk_label_new ("Stringa");
  gtk_box_pack_start (GTK_BOX (hbox2), label1, FALSE, FALSE, 5);
  gtk_label_set_justify (GTK_LABEL (label1), GTK_JUSTIFY_LEFT);
  gtk_widget_show (label1);

  entry1 = gtk_entry_new ();
  gtk_box_pack_start (GTK_BOX (hbox2), entry1, TRUE, TRUE, 0);
  gtk_widget_show (entry1);

  hbox3 = gtk_hbox_new (FALSE, 0);
  gtk_box_pack_start (GTK_BOX (vbox), hbox3, FALSE, FALSE, 0);
  gtk_widget_show (hbox3);

  label2 = gtk_label_new ("Numero");
  gtk_box_pack_start (GTK_BOX (hbox3), label2, FALSE, FALSE, 5);
  gtk_label_set_justify (GTK_LABEL (label2), GTK_JUSTIFY_LEFT);
  gtk_widget_show (label2);

  entry2 = gtk_entry_new ();
  gtk_box_pack_start (GTK_BOX (hbox3), entry2, TRUE, TRUE, 0);
  gtk_widget_show (entry2);

  hseparator = gtk_hseparator_new ();
  gtk_box_pack_start (GTK_BOX (vbox), hseparator, FALSE, FALSE, 0);
  gtk_widget_show (hseparator);

  button4 = gtk_button_new_with_label("Esci");
  gtk_box_pack_start (GTK_BOX (vbox), button4, FALSE, FALSE, 0);
  gtk_widget_show (button4);

  scrolledwindow = gtk_scrolled_window_new (NULL, NULL);
  gtk_box_pack_start (GTK_BOX (hbox1), scrolledwindow, TRUE, TRUE, 0);
  gtk_container_set_border_width (GTK_CONTAINER (scrolledwindow), 10);
  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolledwindow), 
				  GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
  gtk_widget_show (scrolledwindow);

  model     = gtk_list_store_new(COLONNE, G_TYPE_STRING, G_TYPE_INT);
  view      = gtk_tree_view_new_with_model (GTK_TREE_MODEL(model));
  selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
  
  gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE);
  gtk_tree_view_set_rules_hint(GTK_TREE_VIEW(view), TRUE);

  renderer = gtk_cell_renderer_text_new();
  gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(view), /* vista */
                       -1,                  /* posizione della colonna */
                       "Colonna stringhe",  /* titolo della colonna */
                       renderer,            /* cella inserita nella colonna */
                       "text",              /* attributo colonna */
                       COLONNA_STRINGHE,    /* colonna inserita  */
                       NULL);               /* fine ;-) */

  renderer = gtk_cell_renderer_text_new();
  gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(view), -1, 
					      "Colonna numeri", renderer, 
					      "text", COLONNA_NUMERI, 
					      NULL);          

  gtk_widget_show (view);
  g_object_unref(model);

  gtk_container_add (GTK_CONTAINER (scrolledwindow), view);
  gtk_container_set_border_width (GTK_CONTAINER (view), 5);
  gtk_tree_view_set_rules_hint (GTK_TREE_VIEW (view), TRUE);

  g_signal_connect(G_OBJECT (button1), 
         "clicked",
		   G_CALLBACK (aggiungi),
		   view);

  g_signal_connect(G_OBJECT (button2), 
         "clicked",
		   G_CALLBACK (modifica),
		   view);

  g_signal_connect(G_OBJECT (button3), 
         "clicked",
		   G_CALLBACK (cancella),
		   view);

  g_signal_connect(G_OBJECT(selection), 
         "changed", 
		   G_CALLBACK(selection_changed), 
		   view);

  g_signal_connect(G_OBJECT (button4), 
         "clicked",
		   G_CALLBACK (gtk_main_quit),
		   NULL);

  return window;
}



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

  gtk_init(&argc, &argv);

  window = create_window();
  gtk_widget_show(window);

  gtk_main ();

  return 0;
}	
	

Salvate il programma di prova nel file gtk_list.c e compilate con:

gcc -Wall -g gtk_list.c -o gtk_list `pkg-config gtk+-2.0 --libs --cflags`

Iniziamo immediatamente con una delle novità introdotte dalle nuove librerie, lo script pkg-config, che sostituisce e generalizza i compiti svolti sia da gtk-config sia da gnome-config. Richiamate lo script dalla riga di comando con la seguente sintassi: pkg-config --help, per avere l'elenco di tutte le possibili opzioni, e con la sintassi: pkg-config --list-all per avere l'elenco di tutte le librerie supportate dallo script. Naturalmente il suo scopo è quello di fornire al compilatore e al linker i percorsi per i file da includere e per le librerie da linkare. Il risultato della compilazione e dell'esecuzione del programma di prova è osservabile nella figura 1.



figura 1 Le nuove liste di GTK+

GtkTreeModel

Abbiamo visto che per creare una lista (o un albero) è necessario creare un modello e una vista; vediamo in dettaglio come fare.


  enum {
    COLONNA_STRINGHE,
    COLONNA_NUMERI,
    COLONNE
  };

  ...
  
  model = gtk_list_store_new(COLONNE, G_TYPE_STRING, G_TYPE_INT); 
		

Sono disponibili due tipi differenti di modelli "preconfezionati", uno per creare le liste (GtkListStore) e uno per creare gli alberi (GtkTreeStore); in questo numero del Pluto Journal vedremo come adoperare le liste. Alcune delle funzioni utilizzate per la creazione del modello e per il suo utilizzo sono le seguenti:


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

    void gtk_tree_model_get (GtkTreeModel *tree_model,
                         GtkTreeIter *iter, ...);
    void gtk_list_store_set (GtkListStore *list_store,
                         GtkTreeIter *iter, ...);
                         
    void gtk_list_store_remove (GtkListStore *list_store,
                            GtkTreeIter *iter);
    void gtk_list_store_clear (GtkListStore *list_store); 
                           
    void gtk_list_store_insert (GtkListStore *list_store,
                            GtkTreeIter *iter,
                            gint position);
    void gtk_list_store_prepend (GtkListStore *list_store,
                             GtkTreeIter *iter);
    void gtk_list_store_append (GtkListStore *list_store,
                            GtkTreeIter *iter);

    GtkTreePath* gtk_tree_path_new_from_string (const gchar *path);
    gchar* gtk_tree_path_to_string (GtkTreePath *path);
    gint*  gtk_tree_path_get_indices (GtkTreePath *path);
   
    gboolean gtk_tree_model_get_iter (GtkTreeModel *tree_model,
                                  GtkTreeIter *iter,
                                  GtkTreePath *path);
    gboolean gtk_tree_model_get_iter_from_string (GtkTreeModel *tree_model,
                                              GtkTreeIter *iter,
                                              const gchar *path_string);
                                              
    void gtk_tree_path_next (GtkTreePath *path);
    gboolean gtk_tree_path_prev (GtkTreePath *path);
    gboolean gtk_tree_path_up (GtkTreePath *path);
    void gtk_tree_path_down (GtkTreePath *path);
    void gtk_tree_path_free (GtkTreePath *path);    
  	

Osserviamo l'esempio proposto. Chiamando gtk_list_store_new (COLONNE, G_TYPE_STRING, G_TYPE_INT); abbiamo creato il modello della nostra lista, che è formata da due colonne, la prima contenente delle stringhe (G_TYPE_STRING), la seconda degli interi (G_TYPE_INT). La funzione ritorna un puntatore al nuovo oggetto "modello lista" creato. La funzione prevede come primo argomento il numero di colonne della lista e come argomenti successivi il tipo di dato che ogni colonna dovrà contenere. Avremmo potuto creare il modello richiamando la funzione nel modo seguente: gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT);, ma in genere si preferisce utilizzare un enumeratore. Questo sia per indicare il numero di colonne della lista, sia per identificare le colonne medesime, perché molte funzioni operano sia sulle singole colonne sia sul loro numero totale. Ad esempio supponiamo di scrivere un piccolo programma per la gestione delle spese della famiglia; è molto più semplice riferirsi alla colonna CONSUMI_TELEFONICI o SPESE_CINEMA che alla colonna numero tre o numero sette, semplicità che diventa ancor maggiore se il programma gestisce più di una lista.

Ora che abbiamo creato il nostro modello, vediamo come inserirci qualche dato:


  GtkTreeIter iter;

  ...

  str = g_strdup(gtk_entry_get_text(GTK_ENTRY(entry1)));
  num = g_strdup(gtk_entry_get_text(GTK_ENTRY(entry2))); 
  
  gtk_list_store_append(GTK_LIST_STORE(model), &iter); 
  gtk_list_store_set(GTK_LIST_STORE(model), &iter,  
		     COLONNA_STRINGHE, str,  
		     COLONNA_NUMERI, atoi(num),  
		     -1); 
		

Nel nostro esempio recuperiamo i dati da inserire nella lista da due widget entry, quindi utilizziamo la funzione gtk_list_store_append() per aggiungere una nuova riga al modello.

La riga può essere inserita in testa alla lista utilizzando la funzione gtk_list_store_prepend();, o in coda usando gtk_list_store_append(); o, infine, in una generica posizione richiamando gtk_list_store_insert() (figura 2).

Le funzioni per appendere in testa e in coda alla lista necessitano del puntatore al modello e dell'indirizzo di una struttura GtkTreeIter, mentre per la funzione di inserimento in un generico punto occorre passare anche la posizione in cui si vuole inserire il dato.

Ma cos'è un GtkTreeIter? Per realizzare liste, alberi e quant'altro non è sufficiente disporre di un widget flessibile che ci consenta di creare diversi modelli; è necessario anche un meccanismo che ci permetta di accedere ai dati contenuti nel modello stesso. Pensiamo ad un foglio di calcolo ed alla sua struttura a griglia, e supponiamo di voler accedere alla cella che si trova all'intersezione della colonna XYZ e della riga 436. Come fare? La risposta di GTK+ è GtkTreeIter (GtkTreePath): è possibile accedere (riferirsi) ai dati del modello contenuti in una certa riga o in una certa colonna utilizzando la struttura GtkTreeIter (o GtkTreePath).

Una volta aggiunta la riga alla lista, occorre inserire i dati veri e propri. Questa operazione sarà effettuata richiamando la funzione gtk_list_store_set(). La funzione vuole il puntatore al modello come primo argomento, l'iteratore come secondo e poi un elenco variabile di coppie (numero_di_colonna, valore_da_inserire_in_colonna) terminato da -1. Osserviamo il codice proposto; abbiamo le coppie COLONNA_STRINGHE, str e COLONNA_NUMERI, atoi(num), quindi nella colonna COLONNA_STRINGHE inseriamo il valore della variabile str, e così via. Possiamo osservare, inoltre, come le enumerazioni effettuate ci facilitino la vita; pensate ad una lista con qualche decina di colonne, alla difficoltà di ricordare il numero di una certa colonna, alla possibilità di commettere errori invertendo numeri di colonna, ecc.



figura 2 Elementi inseriti nella lista

Abbiamo inserito dei dati nella lista, ora vogliamo recuperarli:


  ...
  gchar *str;
  gint num;

  ...
  
  gtk_tree_model_get(model, &iter, 
		COLONNA_STRINGHE, &str, 
		COLONNA_NUMERI, &num, 
		-1);
      
  gtk_entry_set_text(GTK_ENTRY(entry1), str);
  g_free(str);
      
  str = g_strdup_printf("%d", num);
  gtk_entry_set_text(GTK_ENTRY(entry2), str);  
  g_free(str);		
  
  ...
		

Richiamiamo la funzione gtk_tree_model_get(), passandole il puntatore al modello, il puntatore all'iteratore (cioè il puntatore alla riga che ci interessa), e l'elenco delle coppie (numero_di_colonna_da_recuperare, variabile_in_cui_salvare_il_valore_recuperato) terminato da -1. Osserviamo che non siamo tenuti a recuperare il contenuto di tutte le colonne; se avessimo avuto bisogno del contenuto della colonna COLONNA_NUMERI, avremmo richiamato la funzione nel modo seguente: gtk_tree_model_get(model, &iter, COLONNA_NUMERI, &num, -1);. Quindi con il puntatore all'iteratore selezioniamo una riga del modello, con il numero di colonna selezioniamo una delle colonne (o la colonna, se unica) della riga puntata, il valore contenuto in questa cella verrà salvato nella variabile passata.

Per eliminare una riga dalla lista, è sufficiente chiamare gtk_list_store_remove() passandogli il puntatore alla lista e l'iteratore relativo alla riga da eliminare

Per finire vogliamo pulire la lista, eliminando tutte le righe in essa contenute. Otteniamo questo risultato utilizzando la funzione gtk_list_store_clear() e passandole il puntatore alla lista.

GtkTreeIter e GtkTreePath

GtkTreeIter e GtkTreePath sono strettamente correlati: GtkTreeIter punta alla locazione in cui i dati del modello sono memorizzati; permette quindi di accedere ai dati contenuti nel modello. Il corretto valore dell'Iter ci sarà fornito da "un'interrogazione" al modello. GtkTreePath indica un potenziale nodo; è un "indirizzo" di un possibile nodo del modello. Se "l'indirizzo" è valido, cioè se a quell'indirizzo corrisponde nel modello un dato, allora potremo accedervi. Utilizzando GtkTreePath possiamo visitare un generico nodo del nostro modello, se quel nodo esiste.

E' possibile convertire la struttura GtkTreePath sia in una lista d'interi, utilizzando la funzione gtk_tree_path_get_indices(); che prende un puntatore alla path e ritorna un puntatore alla lista d'interi, sia in una stringa usando la funzione gtk_tree_path_to_string();, che prende un puntatore alla path e ritorna una stringa. Utilizzeremo proprio la possibilità di trasformare una path in una stringa per meglio comprendere il suo funzionamento.

Supponiamo che il nostro modello rappresenti un albero, la stringa rappresentativa della path sarà formata da una lista di numeri separati da ":" (due punti), ad esempio "0:4:7"; ogni numero si riferisce ad un offset del livello, in questo caso "0" si riferisce al nodo radice dell'albero, mentre "4:7" si riferisce all'ottavo elemento del quinto nodo della radice. Quindi, se volessimo accedere al secondo elemento del terzo nodo del nodo radice, la nostra stringa dovrebbe essere "0:2:1".

La possibilità di trasformare stringhe in path, utilizzando la funzione gtk_tree_path_new_from_string();, che prende una stringa nella forma "x:y:z:k...:w" e ritorna un puntatore a GtkTreePath, ci permette di navigare con molta semplicità attraverso la nostra lista o albero.

Come già detto, GtkTreeIter si riferisce ad un preciso nodo del modello, ed è il modello stesso che assegna un valore corretto all'iteratore. I GtkTreeIter sono il modo principale per accedere ai dati contenuti nel modello. GtkTreeModel definisce inoltre una serie di funzioni che permettono di navigare attraverso i dati utilizzando GtkTreeIter e/o GtkTreePath

E' possibile recuperare l'iteratore associato ad un determinato elemento della lista in vari modi; qui ne esamineremo un paio.

Si può utilizzare la funzione gtk_tree_model_get_iter_from_string(), che vuole il puntatore al modello, l'indirizzo di memoria della variabile iter e la stringa che rappresenta la path dell'elemento che ci interessa. Ritorna TRUE se l'iter è stato assegnato correttamente. Ad esempio con questo codice if (gtk_tree_model_get_iter_from_string(model, &iter, "0:3:4:5")) possiamo far puntare l'iter all'elemento indicato dalla stringa, e contemporaneamente verificare che l'elemento esista (nel caso in cui l'elemento non esista o non fosse raggiungibile, la funzione ritornerebbe FALSE, inoltre il valore contenuto in iter non sarebbe valido).

Un modo alternativo per accedere ad un elemento della lista è il seguente: partiamo dalla stringa rappresentativa della path, richiamando gtk_tree_path_new_from_string(), che ci fornirà un puntatore a GtkTreePath. Passeremo poi questo puntatore alla funzione gtk_tree_model_get_iter();. Quest'ultima funzione vuole il puntatore al modello, il puntatore della path di cui vogliamo l'iter e l'indirizzo di memoria della variabile iter. La funzione riempirà la variabile iter con i valori appropriati a far puntare l'iteratore all'elemento che ci interessa. Anche questa funzione ritorna un valore booleano, che sarà TRUE se alla variabile iter è stato assegnato un valore corretto, FALSE in caso contrario.

Attenzione, la memoria al puntatore a GtkTreePath, allocata richiamando gtk_tree_path_new_from_string() deve essere liberata utilizzando gtk_tree_path_free().

Gli iteratori possono essere considerati validi finché il modello non viene modificato o finché non emette un segnale. Non bisogna liberare la memoria occupata dagli iteratori, che sono di proprietà del modello. Si noti tra l'altro che possiamo sempre allocare gli iteratori sullo stack; in parole povere, dichiararli come oggetti (non puntatori) e passare alle funzioni il riferimento (con l'operatore &). Questo ha il vantaggio rispetto all'allocazione sull'heap di non richiedere di liberare la memoria. Anche la documentazione di GTK consiglia l'allocazione sullo stack, per l'uso comune.

Ricapitolando, la memoria che allochiamo utilizzando la funzione gtk_tree_path_new_from_string() va liberata chiamando gtk_tree_path_free(), mentre non bisogna liberare la memoria occupata dagli iteratori.

Per navigare attraverso la lista o l'albero si ricorre alle seguenti funzioni (è necessario passare a tutte un puntatore a GtkTreePath):

GtkTreeView


  ...

  view      = gtk_tree_view_new_with_model (GTK_TREE_MODEL(model)); 
  selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view)); 
  
  gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE); 
  gtk_tree_view_set_rules_hint(GTK_TREE_VIEW(view), TRUE); 

  renderer = gtk_cell_renderer_text_new(); 
  gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(view), /* vista */ 
                         -1,                 /* posizione della colonna */ 
                         "Colonna stringhe", /* titolo della colonna */ 
                         renderer,           /* cella inserita nella colonna */ 
                         "text",             /* attributo colonna */ 
                         COLONNA_STRINGHE,   /* colonna inserita  */ 
                         NULL);              /* fine ;-) */ 
  
  ... 
                             
  gtk_tree_view_set_rules_hint (GTK_TREE_VIEW (view), TRUE); 

  ...

  g_signal_connect(G_OBJECT(selection), "changed",  
		   G_CALLBACK(selection_changed), view); 		
		


   GtkWidget* gtk_tree_view_new_with_model(GtkTreeModel *model);
   GtkTreeModel* gtk_tree_view_get_model(GtkTreeView *tree_view);
   GtkTreeSelection* gtk_tree_view_get_selection(GtkTreeView *tree_view);

   gint gtk_tree_view_insert_column_with_attributes(GtkTreeView *tree_view,
                                                gint position,
                                                const gchar *title,
                                                GtkCellRenderer *cell,
                                                ...);

   GtkTreeViewColumn* gtk_tree_view_get_column(GtkTreeView *tree_view, gint n);

   GtkTreeViewColumn* 
        gtk_tree_view_column_new_with_attributes(const gchar *title,
                                             GtkCellRenderer *cell, ...);
   gint gtk_tree_view_append_column(GtkTreeView *tree_view,
                               GtkTreeViewColumn *column);
   gint gtk_tree_view_insert_column(GtkTreeView *tree_view,
                               GtkTreeViewColumn *column,
                               gint position);

   gint gtk_tree_view_remove_column(GtkTreeView *tree_view,
                               GtkTreeViewColumn *column); 
   GtkTreeViewColumn* gtk_tree_view_get_column(GtkTreeView *tree_view, gint n);
    
   gboolean gtk_tree_view_get_headers_visible(GtkTreeView *tree_view);
   void gtk_tree_view_set_headers_visible(GtkTreeView *tree_view,
                                      gboolean headers_visible);

   void gtk_tree_view_column_set_title(GtkTreeViewColumn *tree_column,
                                  const gchar *title);
   G_CONST_RETURN gchar* 
        gtk_tree_view_column_get_title (GtkTreeViewColumn *tree_column);

   void gtk_tree_view_columns_autosize(GtkTreeView *tree_view);
   void gtk_tree_view_column_set_resizable(GtkTreeViewColumn *tree_column,
                                       gboolean resizable);
   gboolean gtk_tree_view_column_get_resizable(GtkTreeViewColumn *tree_column);

   void gtk_tree_view_column_set_clickable(GtkTreeViewColumn *tree_column,
                                       gboolean clickable);
   gboolean gtk_tree_view_column_get_clickable(GtkTreeViewColumn *tree_column);

   void gtk_tree_view_column_set_visible(GtkTreeViewColumn *tree_column,
                                     gboolean visible);
   gboolean gtk_tree_view_column_get_visible(GtkTreeViewColumn *tree_column);
    

Abbiamo creato il modello, ora occorre visualizzare i dati in esso contenuti. Per far ciò si ricorre ad un oggetto GtkTreeView, che è creato richiamando la funzione gtk_tree_view_new_with_model() alla quale bisogna passare il puntatore al modello della nostra lista; ci ritornerà il puntatore all'oggetto GtkTreeView.

Dal puntatore al GtkTreeView è possibile ricavare il puntatore al modello e un puntatore a GtkTreeSelection. Chiamando la funzione gtk_tree_view_get_model(), con in ingresso il puntatore a GtkTreeView, avremo un puntatore a GtkTreeModel, che rappresenta il modello utilizzato da quella particolare vista, mentre chiamando la funzione gtk_tree_view_get_selection() con in ingresso il puntatore a GtkTreeView, otterremo il puntatore a GtkTreeSelection.

Creato l'oggetto GtkTreeView, è necessario inserire nella lista le varie colonne, operazione che è possibile compiere in vari modi. Iniziamo dal più semplice, la funzione gtk_tree_view_insert_column_with_attributes();, alla quale passeremo il puntatore a GtkTreeView, la posizione in cui inserire la colonna (se si passa -1 la colonna è inserita in ultima posizione), il titolo che apparirà sulla colonna, un puntatore ad un oggetto GtkCellRenderer, che rappresenta la cella della colonna, ed infine un elenco d'attributi da assegnare alla cella, terminato da NULL. La funzione ritorna un intero che indica il numero totale di colonne presenti. Gli attributi che è possibile assegnare alla cella sono delle stringhe che descrivono le sue caratteristiche; nel programma allegato si fa uso di un solo attributo, la stringa "text", con la quale indichiamo che le celle della lista conterranno del testo.

Un modo alternativo per creare una colonna è quello di ricorrere alla funzione gtk_tree_view_column_new_with_attributes(), che accetta in ingresso il titolo della colonna, il puntatore alla cella che si sta inserendo e l'elenco, terminato da NULL, degli attributi della cella. Quello che ci sarà ritornato è un puntatore al nuovo oggetto colonna GtkTreeViewColumn. Sarà, ora compito nostro inserire la colonna in GtkTreeView, utilizzando una tra le seguenti funzioni:

Per eliminare una colonna è sufficiente richiamare gtk_tree_view_remove_column, passando come primo argomento il puntatore all'oggetto vista e come secondo il puntatore alla colonna da eliminare. L'intero restituito indica il numero di colonne dopo l'eliminazione.

Se abbiamo la necessità di compiere una certa operazione su di una colonna e non disponiamo del suo puntatore, come possiamo fare? Possiamo ricorrere alla funzione gtk_tree_view_get_column(), che avuti in ingresso un puntatore a GtkTreeView e un intero (la posizione della colonna) ci restituisce il puntatore alla colonna. Attenzione: le colonne sono numerate a partire da zero, la prima colonna sarà dunque in posizione zero.

Abbiamo creato la nostra lista, ed inserito anche qualche dato; adesso cerchiamo di modificarne l'aspetto esteriore.

Se vogliamo sapere se le intestazioni delle colonne dalla lista sono visibili per nasconderle (o sono nascoste per mostrarle), utilizzeremo la funzione gtk_tree_view_get_headers_visible(), alla quale passeremo il puntatore al GtkTreeView di cui vogliamo sapere se sta visualizzando gli header, e ci ritornerà TRUE nel caso in cui gli header siano visibili. A questo punto possiamo modificare lo stato degli header richiamando la funzione gtk_tree_view_set_headers_visible(), alla quale passeremo il puntatore a GtkTreeView ed un valore booleano, vero o falso, a seconda se vogliamo visualizzare o meno le intestazioni.

Sappiamo come mostrare o nascondere gli header delle colonne, ma come fare a modificare la stringa che contengono? Ricorreremo alle funzioni gtk_tree_view_column_set_title() e gtk_tree_view_column_get_title(); la prima necessita del puntatore alla specifica colonna (GtkTreeViewColumn) di cui vogliamo modificare il titolo e del nuovo titolo, mentre alla seconda passeremo sempre il puntatore alla colonna e ci restituirà il titolo della stessa.

Utilizzando la funzione gtk_tree_view_columns_autosize() facciamo in modo che le colonne si ridimensionino automaticamente. Supponiamo di avere una colonna in cui l'elemento più lungo sia di 10 caratteri, e di inserire la stringa "precipitevolissimevolmente"; se abbiamo stabilito che le colonne debbano essere autoridimensionanti, allora la colonna in questione s'allargherà per visualizzare completamente la nuova stringa.

Possiamo decidere che debba essere l'utente ad occuparsi del ridimensionamento delle colonne, posizionando il puntatore del mouse sul bordo della testata della colonna e trascinandolo.

Per sapere se una specifica colonna è ridimensionabile o meno, utilizzeremo la funzione gtk_tree_view_column_get_resizable(), alla quale passeremo un puntatore a GtkTreeViewColum e che ci ritornerà un valore booleano. Utilizzeremo invece la funzione gtk_tree_view_column_set_resizable(), alla quale passeremo il puntatore alla colonna e un valore booleano, per indicare se quella colonna è ridimensionabile (TRUE) o no (FALSE).

Vogliamo che gli header delle colonne della lista siano cliccabili? Bene, chiamiamo la funzione gtk_tree_view_column_set_clickable() con un puntatore alla colonna e un valore booleano (TRUE per rendere l'header cliccabile, FALSE altrimenti), come parametri. Potremmo in seguito intercettare il click sull'header e richiamare, ad esempio, una callback per riordinare la lista.

Poniamo che durante il funzionamento del nostro programma si verifichi una certa condizione, e vogliamo che una colonna non sia più mostrata finché non se ne verifichi un'altra. Utilizzeremo allora la funzione gtk_tree_view_column_set_visible() per mostrare (TRUE) o nascondere (FALSE) la colonna individuata dal puntatore a GtkTreeViewColumn passato come primo parametro. Per conoscere lo stato di una colonna si chiamerà la funzione gtk_tree_view_column_get_visible, che ritornerà TRUE o FALSE a seconda che la colonna passata come parametro sia o no visibile.

GtkTreeSelection


  ...

  g_signal_connect(G_OBJECT(selection), 
         "changed", 
         G_CALLBACK(selection_changed), 
         view);  
           
  ...

  void modifica(GtkButton *button, gpointer data)
  {
    ...
 
    /* questa istruzione è ridondante */
    model = gtk_tree_view_get_model(GTK_TREE_VIEW(view));
    selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));

    ... 
     
    /* posso recuperare il modello da qui, 
       sostituendo a NULL il ptr. al modello
    */
    if (gtk_tree_selection_get_selected(selection, NULL, &iter))      
    {
      gint i;
      GtkTreePath *path;
   
      path = gtk_tree_model_get_path(model, &iter);
      i = gtk_tree_path_get_indices(path)[0];
    
      gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
      gtk_tree_path_free(path);
    
      gtk_list_store_insert(GTK_LIST_STORE(model), &iter, i);
       
  ...

  void selection_changed(GtkTreeSelection *selection, gpointer data)
  { 
    ...
  
    if (gtk_tree_selection_get_selected(selection, &model, &iter))
      {
        gtk_tree_model_get(model, &iter, 
            COLONNA_STRINGHE, &str, 
            COLONNA_NUMERI, &num, 
            -1);
      
        gtk_entry_set_text(GTK_ENTRY(entry1), str);
        g_free(str);
      
        str = g_strdup_printf("%d", num);
        gtk_entry_set_text(GTK_ENTRY(entry2), str);  
        g_free(str);
      }

    ...		
		

L'oggetto GtkTreeSelection è creato automaticamente durante la creazione dell'oggetto GtkTreeView, e non può esistere se non in funzione di GtkTreeView. Non esiste, quindi, una funzione gtk_tree_selection_new(); il solo modo per ricavare un puntatore all'oggetto GtkTreeSelection è quello di recuperarlo da un puntatore a GtkTreeView.

Lo scopo dell'oggetto GtkTreeSelection è quello di determinare lo stato della lista/albero e/o determinare se e quale nodo (elemento/riga) della lista/albero è stato selezionato/deselezionato.

Nell'esempio allegato, abbiamo riempito la lista con una serie di valori, ma nel caso avessimo commesso un errore, come potremmo modificare il dato errato? Possiamo selezionare il dato che ci interessa (molto banalmente lo clicchiamo) e intercettare il segnale "changed" che è emesso dall'oggetto GtkTreeSelection ogni qualvolta che un suo nodo è selezionato; quindi richiamiamo un'opportuna funzione di callback.


    GtkTreeView* gtk_tree_selection_get_tree_view (GtkTreeSelection *selection);
    
    gboolean gtk_tree_selection_get_selected (GtkTreeSelection *selection,
                                          GtkTreeModel **model,
                                          GtkTreeIter *iter);
    void gtk_tree_selection_set_mode (GtkTreeSelection *selection,
                                  GtkSelectionMode type);

    void gtk_tree_selection_select_path (GtkTreeSelection *selection,
                                     GtkTreePath *path);
    void gtk_tree_selection_select_iter (GtkTreeSelection *selection,
                                     GtkTreeIter *iter);
                                              
    void gtk_tree_selection_select_all (GtkTreeSelection *selection);
    void gtk_tree_selection_unselect_all (GtkTreeSelection *selection);
    void gtk_tree_selection_select_range (GtkTreeSelection *selection,
                                      GtkTreePath *start_path,
                                      GtkTreePath *end_path);
  		

Vediamo come utilizzare l'oggetto GtkTreeSelection per compiere delle operazioni sugli elementi selezionati. Nota: nel programma d'esempio ho utilizzato qualche istruzione in eccesso, quindi quello che sto per dire non è il modo più breve per effettuare operazioni su GtkTreeSelection; in questo modo sarà però più chiara la flessibilità offerta da queste funzioni.

Abbiamo visto che un GtkTreeSelection non può esistere se non in funzione di un GtkTreeView, quindi la prima operazione che si deve fare è quella di recuperare il puntatore a GtkTreeView. Una volta in possesso del puntatore alla vista, si può, eventualmente, recuperare il puntatore al modello, ed utilizzando quest'ultimo stabilire quale elemento/elementi sono stati selezionati (utilizzando GtkTreeIter o GtkTreePath) e si effettuano le operazioni necessarie al nostro programma.

Se si è già in possesso del puntatore a GtkTreeSelection (ad esempio passato all'interno di una funzione di callback), si può recuperare il puntatore a GtkTreeView, chiamando la funzione gtk_tree_selection_get_tree_view(), alla quale si passa come parametro il puntatore a GtkTreeSelection e si ottiene il puntatore a GtkTreeView.

Se si è in una funzione callback richiamata dall'intercettazione del segnale "changed" emesso dall'oggetto GtkTreeSelection, si può ricorrere alla funzione gtk_tree_selection_get_selected(), alla quale bisogna passare il puntatore al GtkTreeSelection, e gli indirizzi di memoria del modello e dell'iteratore. La funzione assegnerà alle variabili del modello e dell'iteratore dei valori opportuni. All'uscita dalla funzione, l'iter punterà all'elemento selezionato, mentre se si passa NULL come indirizzo dell'iter, allora la funzione testerà se nella lista/albero è presente qualche elemento selezionato.

Richiamando la funzione gtk_tree_selection_set_mode() possiamo indicare che tipo di selezione vogliamo effettuare. La funzione accetta come primo argomento un puntatore a GtkTreeSelection, e come secondo una variabile di tipo GtkSelectionMode, alla quale si può assegnare uno tra i seguenti valori:

Possiamo selezionare un determinato elemento utilizzando una tra le funzioni gtk_tree_selection_select_path() e gtk_tree_selection_select_iter(). Ad entrambe dobbiamo passare come primo parametro il puntatore a GtkTreeSelection, mentre il secondo parametro da passare è l'"indirizzo" dell'elemento che vogliamo che sia selezionato. Per la prima funzione passeremo la path dell'elemento, alla seconda forniremo l'iter dell'elemento.

Utilizzando gtk_tree_selection_select_path() possiamo, ad esempio, costruire una funzione che ci permetta di navigare attraverso una lista o un albero. Costruiamo la path partendo da una stringa gtk_tree_path_new_from_string(...,"0"), selezioniamo il nodo "puntato" dalla path con gtk_tree_selection_select_path(), e poi navighiamo attraverso la lista/albero con gtk_tree_path_next(), gtk_tree_path_prev (), gtk_tree_path_up() eccetera.

Gli elementi contenuti nella lista/albero possono essere selezionati o deselezionati tutti insieme semplicemente chiamando le funzioni gtk_tree_selection_select_all() o gtk_tree_selection_unselect_all(). Se invece vogliamo selezionare i nodi contenuti in un certo intervallo, useremo la funzione gtk_tree_selection_select_range. Alle prime due funzioni dobbiamo passare solo il puntatore a GtkTreeSelection, mentre all'ultima dobbiamo passare anche le path al primo ed all'ultimo elemento da selezionare.

GtkCellRenderer


  ...
 
  renderer = gtk_cell_renderer_text_new();
  gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(view), /* vista */
                       -1,                  /* posizione della colonna */
                       "Colonna stringhe",  /* titolo della colonna */
                       renderer,            /* cella inserita nella colonna */
                       "text",              /* attributo colonna */
                       COLONNA_STRINGHE,    /* colonna inserita  */
                       NULL);               /* fine ;-) */		
   
  ...
		

Le celle contenute nelle colonne si creano utilizzando le funzioni della classe GtkCellRenderer. Con l'introduzione dell'oggetto GtkCellRenderer è diventato semplicissimo inserire nelle liste oggetti quali immagini, check button, radio button; per le celle contenenti stringhe è ora possibile editare il contenuto semplicemente facendo click sulla cella stessa.

In questo numero del Pluto Journal esamineremo solo le celle di tipo testo, nei prossimi numeri vedremo come inserire qualche immagine, check button, eccetera (mi raccomando non mancate ;)).

La cella "testuale" è creata dalla funzione gtk_cell_renderer_text_new(). La funzione non accetta nessun parametro, e ritorna il puntatore al nuovo oggetto. E' in pratica la sola funzione che ci serve. Una volta che è stata creata dobbiamo inserire la cella in una colonna, e le dobbiamo assegnare alcuni attributi; per compiere queste operazioni si ricorre ad una funzione che abbiamo incontrato in precedenza, gtk_tree_view_insert_column_with_attributes. Trattandosi di una cella che conterrà una stringa, il nostro attributo sarà "text".

Gli attributi disponibili sono molti, vi rimando alla documentazione per l'elenco completo.

Bene, siamo giunti alla conclusione di questo articolo, che spero possa esservi utile. Per finire vi invito a provare a modificare il programma d'esempio, relativamente all'uso di path, iter e selector. Inserite nella finestra del programma due etichette, la prima con la frase "elemento selezionato" (o qualcosa di simile), e la seconda da impostare in funzione dell'elemento selezionato e contenente la path dell'elemento stesso.

Alla prossima! :)

Riferimenti webografici

Potete scaricare la documentazione relativa alle nuove API di GTK+-2.0 da qui:

http://developer.gnome.org/doc/API/2.0/glib/index.html
http://developer.gnome.org/doc/API/2.0/gobject/index.html
http://developer.gnome.org/doc/API/2.0/gdk/index.html
http://developer.gnome.org/doc/API/2.0/gtk/index.html
http://developer.gnome.org/doc/API/2.0/gdk-pixbuf/index.html
http://developer.gnome.org/doc/API/2.0/pango/index.html
http://developer.gnome.org/doc/API/2.0/atk/book1.html



L'autore

Nicola Fragale. E' iscritto ad Ingegneria Informatica al Federico II di Napoli. I suoi ultimi interessi informatici sono rivolti all'XML e particolarmente a GConf. Attualmente sta scrivendo una rubrica per GTK+ e GNOME, naturalmente rilasciata sotto licenza GPL.



<- PW: AVI - Indice Generale - Copertina - SL: Intro ->