<- PW: Introduzione - Copertina - PW: Alle radici dello GNOME - Seconda Puntata ->

PlutoWare


GTK+: Alle radici dello GNOME

Prima puntata

GTK+, il Gimp ToolKit, è nato come libreria per sviluppare Gimp (GNU Image Manipulation Program), ma col tempo si è trasformato in una libreria general purpouse, ed è stato adottato come base per un ambizioso progetto, la creazione di un desktop manager totalmente open source: Gnome.

GTK+ è scritta in C, ma è fortemente orientata agli oggetti; infatti implementa l'idea delle classi, e dell'ereditarietà, implementa una infrastruttura "segnali/funzioni di callback" utilizzando i puntatori a funzione. Esistono, inoltre, varie versioni delle GTK+, sviluppate per essere utilizzate con i principali linguaggi di programmazione: ne esiste una versione per il C++, le GTK--, una versione per il pascal, per il perl, per python, per il PHP ed è stata sviluppata anche una versione per i sistemi win32 (per approfondimenti visitate il sito GTK+).

Cominciamo ora a vedere i fondamentali di un programma GTK+, per poi proseguire il discorso nelle prossime puntate, approfondendo via via diversi argomenti, e giungendo ad essere in grado di costruire un'applicazione grafica completa basata su queste librerie, che offrono una flessibilità e una facilità d'uso con pochi eguali, unite ad un aspetto grafico davvero accattivante.

Ogni programma GTK+ deve includere l'header <gtk/gtk.h>. Un widget è "l'oggetto grafico" che ci permetterà di realizzare i nostri programmi grafici. Prima di proseguire con la presentazione e descrizione di un primo programma di prova, è necessaria, per non generare confusione, una brevissima digressione sui tipi utilizzati da GTK+.

GTK+ è basato sulle librerie GDK e Glib. Glib sostituisce alcuni dei tipi fondamentali per garantire la portabilità su piattaforme diverse. Vi si trovano tipi gchar, gshort, glong, gint, gfloat, gdouble che non sono altro che i normali char, int,... Mentre gint8, guint8, gint16, guint16, gint32, guint32, gint64, guint64 sono tipi di dato con dimensione garantita, indipendenti dall'architettura sulla quale il programma verrà compilato. Su di un processore x86 o su un alpha, gint32 sarà sempre lungo 32 bit, mentre gconstpointer sarà sempre l'equivalente di const void*.

Oltre ai tipi fondamentali sono implementate parecchie macro. Diverse funzioni della libreria standard del C sono state sostituite con altre più sicure o efficienti (g_malloc, g_free, g_print,...); inoltre sono disponibili funzioni e strutture dati per gestire liste singole e doppie, alberi, tabelle hash, ecc.

La libreria GDK, invece, ha il compito principale di interfacciare GTK+ con le librerie di più basso livello che gestiscono X Window. Uno dei vantaggi che si ottengono adottando questo metodo è che, sostituendo solo GDK (le primitive grafiche) e facendo pochi cambiamenti a Glib, è possibile portare GTK+ su altri sistemi operativi (Windows ad esempio) con uno sforzo minimo.

Di sicuro il modo migliore per imparare come funzionano queste librerie è quello di leggere del codice, quindi, come nelle migliori tradizioni inizieremo con il classico "Ciao Mondo". Come potete vedere dalle immagini in basso, questo programma crea una finestra con tre bottoni al suo interno. Consiglio di lanciare il programma da un emulatore di terminale e di osservare il comportamento della finestra e ciò che sarà stampato sullo schermo.


Ecco come appare la finestra all'esecuzione


La finestra dopo aver cliccato su nascondi mondo

Una delle caratteristiche delle funzioni di GTK+ è quella di essere molto autoesplicative; provate a leggere il codice in basso, non dovrebbe essere molto oscuro! :) Per compilarlo utilizzate il comando:

gcc -Wall -g ciaomondo.c -o ciaomondo `gtk-config --cflags --libs`

Lo script di shell gtk-config si occuperà di fornire al compilatore ed al linker (semplificando, al GCC) nomi e percorsi delle librerie necessarie per una corretta generazione del file binario. Attenzione agli apici, bisogna usare gli apici inversi (`), ottenibili con la sequenza di tasti alt+apice, e non gli apici normali (').

#include <gtk/gtk.h>
		
GtkWidget *window;
GtkWidget *box; 
GtkWidget *bottone1, *bottone2, *bottone3;
GtkWidget *etichetta;
gint hide = 1;

/*
 funzioni di callback:

 le funzioni di callback verranno richiamate 
 quando si verificherà un qualche evento,
 click del mouse su un bottone,..
*/
		
gint exit_cb(GtkWidget *w, gpointer data)
{
   g_print("\nAddio mondo crudele... :(\n");
   gtk_main_quit();

   return (FALSE);
}


gint delete(GtkWidget *w, GdkEvent *event, gpointer data)
{
   return TRUE;
}

void ciao_mondo(GtkWidget *w, gpointer data)
{
   g_print("Siamo nella ciao mondo callback\n");
}


void hide_show_mondo(GtkWidget *wg, gpointer data)
{
   GtkWidget *bt = GTK_WIDGET(data);
   GtkWidget *etichetta;

   etichetta = gtk_object_get_data(GTK_OBJECT(wg), "etichetta");

   if (hide == 0) 
   {
      gtk_widget_show(bt);
      hide = 1;
      gtk_label_set(GTK_LABEL(etichetta), "Nascondi Mondo");
    }
   else 
   {
      gtk_widget_hide(bt);
      hide = 0;
      gtk_label_set(GTK_LABEL(etichetta), "Mostra Mondo");     
   }
   
   g_print("Siamo nella hide_show_mondo callback\n");
}


int main(int argc, char *argv[])
{
   /* inizializzazione della libreria di GTK+
   */
   gtk_init(&argc, &argv);

   /* creazione della finestra principale 
   */
   window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

   /* fisso le dimensioni della finestra a 250x200 pixel 
   */
   gtk_widget_set_usize(GTK_WIDGET(window), 250,200);

   /* diamo un titolo alla finestra 
   */
   gtk_window_set_title(GTK_WINDOW(window),"GTK Ciao Mondo test");

   /* colleghiamo la finestra alle funzioni di callback 
   */
   gtk_signal_connect(GTK_OBJECT(window), /* oggetto che emette il segnale          */
      "delete_event",                     /* segnale emesso                         */
      GTK_SIGNAL_FUNC(delete),            /* funzione da richiamare                 */
      NULL);                              /* dati da passare alla funzione chiamata */

   gtk_signal_connect(GTK_OBJECT(window),
      "destroy",
      GTK_SIGNAL_FUNC(exit_cb),
      NULL);

   gtk_container_set_border_width(GTK_CONTAINER(window),15);

   /* creo una "scatola" che può contenere oggetti disposti 
      in verticale, la inserisco nella finestra principale 
      e la rendo visibile 
   */
   box = gtk_vbox_new(FALSE,1);
   gtk_container_add(GTK_CONTAINER(window), box);
   gtk_widget_show(box);

   /* creiamo tre bottoni
    */
   bottone1 = gtk_button_new_with_label(" Ciao Mondo ");
   gtk_box_pack_end(GTK_BOX(box),
      bottone1,
      FALSE,
      FALSE,
      15);
		   
   gtk_signal_connect(
      GTK_OBJECT(bottone1),             /* oggetto che emette il segnale             */
      "clicked",                        /* segnale emesso                            */
      GTK_SIGNAL_FUNC(ciao_mondo),      /* funzione da richiamare                    */
      NULL);                            /* dati da passare alla funzione da chiamare */

   gtk_widget_show(bottone1);

   bottone2 = gtk_button_new();
   etichetta = gtk_label_new("Nascondi Mondo");
   gtk_container_add(GTK_CONTAINER(bottone2),etichetta);
   gtk_object_set_data(GTK_OBJECT(bottone2), "etichetta", etichetta);
   gtk_widget_show(etichetta);

   gtk_box_pack_start(GTK_BOX(box),
      bottone2,
      FALSE,
      FALSE,
      15);

   gtk_signal_connect(
      GTK_OBJECT(bottone2),
      "clicked",
      GTK_SIGNAL_FUNC(hide_show_mondo),
      (gpointer) bottone1);
     
   gtk_widget_show(bottone2);

   bottone3 = gtk_button_new_with_label(" Esci ");
   gtk_box_pack_start(GTK_BOX(box),
      bottone3,
      FALSE,
      FALSE,
      15);

   gtk_signal_connect(GTK_OBJECT(bottone3),
      "clicked",
      GTK_SIGNAL_FUNC(exit_cb),
      NULL);

   gtk_widget_show(bottone3);
   
   gtk_widget_show(window);

   gtk_main();
   
   return 0;
}
}

Esaminiamo il programma partendo dal classico metodo main.

gtk_init(&argc, &argv);

la funzione gtk_init è molto importante: si occupa di inizializzare la libreria GTK+, richiama glib_init e gdk_init (che a loro volta inizializzano GLib e GDK) ed esamina la riga di comando; i comandi riconosciuti da GTK+ verranno eliminati da argv e quello che rimane verrà passato al programma, che eventualmente lo gestirà. Un programma grafico che utilizzi le librerie GTK+ deve necessariamente richiamare questa funzione prima di ogni altra API di GTK+.

window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

crea la finestra principale del programma.

Notate come GTK+ sia orientato agli oggetti. Tutte le funzioni che creano un nuovo widget avranno la seguente forma: gtk_NomeDelWidget_new(...). Queste funzioni allocano un nuovo oggetto e lo inizializzano, quindi ritornano un puntatore a quell'oggetto. Per poter usare esplicitamente il widget è necessario effettuare un cast al tipo voluto. Una volta ottenuto un GtkWidget*, è possibile manipolarlo utilizzando i suoi metodi; tutte le funzioni per i widget di GTK+ iniziano con il nome (minuscolo) del tipo su cui operano e accettano un puntatore a quel tipo come primo argomento.

Il prototipo della funzione che inserisce un qualsiasi oggetto in un widget contenitore è il seguente:


gtk_container_add(GtkContainer *container, 
                           GtkWidget *wiget);
essa accetta un GtkContainer* come primo argomento, un GtkWidget* come secondo.
GtkContainer è un oggetto derivato da GtkWidget; eredita quindi tutte le sue proprietà e metodi. La macro GTK_CONTAINER() esegue un cast, una conversione di tipo da GtkWidget* a GtkContainer*. Eseguire il cast è obbligatorio poichè il linguaggio C, non essendo un linguaggio ad oggetti, non interpreta automaticamente la relazione di ereditarietà.

GTK+ è "guidato dagli eventi", cioè il nostro programma se ne starà per la maggior parte del suo tempo a non fare niente, ad attendere che avvenga qualcosa, e quando questo qualcosa accade, passerà il controllo ad una apposita funzione. Questo meccanismo si realizza con la tecnica dei segnali. Nel caso del nostro programma, quando si clicca su di un bottone, questo emetterà un certo segnale (o alcuni di essi); noi non dovremo far altro che intercettare quello che ci interessa e chiamare la funzione appropriata. Tutto ciò avviene utilizzando la seguente API:

 
gint gtk_signal_connect(GtkObject *object, 
                                  gchar *name, 
                                  GtkSignalFunc func, 
                                  gpointer func_data); 
Il primo argomento è il widget (l'oggetto) che emetterà il segnale; nel nostro caso è un bottone, ma potrebbe essere una barra di scorrimento, un'icona, un elemento di menu, una casella di testo, eccetera; il secondo argomento è il segnale che vogliamo intercettare; il terzo è la funzione che verrà richiamata (la funzione di callback) e l'ultimo è un puntatore agli eventuali dati da passare a tale funzione.

Ecco un piccolissimo elenco degli eventi che possono essere intercettati:

button_press_event
button_release_event
delete_event
destroy_event
key_press_event
key_release_event

per connettere una funzione ad uno di questi eventi si richiama la funzione gtk_signal_connect, e come parametro name si usa il nome dell'evento da intercettare. La prima funzione di callback alla quale è collegata la finestra principale ha un funzionamento particolare. Il segnale "delete_event" è generalmente emesso dal window manager (in Linux, e in GNOME in particolare, possiamo avere molti wm: afterstep, windowmaker, fvwm, icewm, sawfish, ...); intercettando questo segnale possiamo scegliere cosa farne. Possiamo infatti ignorarlo e uscire dall'applicazione, o possiamo utilizzarlo per qualche utile routine; ad esempio, potremmo voler scrivere dei dati in un file prima di uscire, per realizzare una sorta di persistenza.

Notate che la funzione di callback delete() ritorna TRUE; ritornando questo valore semplicemente informiamo il window manager di voler continuare la nostra applicazione; ritornando il valore FALSE, al contrario, il wm emetterà il segnale "destroy" che chiuderà il nostro programma. Detto ciò una prima applicazione che potrebbe essere realizzata è una finestra di dialogo che chieda all'utente se voglia effettivamente uscire o no, in caso di risposta affermativa si ritornerà FALSE e il programma verrà terminato, altrimenti si ritornerà TRUE e si continuerà normalmente.

box = gtk_vbox_new(FALSE,1); 
gtk_widget_show(box);
	
bottone1 = gtk_button_new_with_label(" Ciao Mondo ");
gtk_box_pack_end(GTK_BOX(box),
                 bottone1, 
                 FALSE, 
                 FALSE, 
                 15);
gtk_widget_show(bottone1);
la prima riga crea una scatola verticale, la seconda rende visibile la scatola, la terza crea un bottone con l'etichetta "Ciao Mondo", la quarta inserisce il bottone nella scatola e l'ultima rende visibile il bottone.

In GTK+ le "scatole" sono molto importanti; sono dei widget invisibili atti a contenere altri widget (un po' come le tabelle in HTML, che possono contenere altre tabelle, che a loro volta sono usate per disporre testo o grafica). Esistono due tipi di scatole, le vbox (le scatole verticali) e le hbox (le scatole orizzontali). Componendo queste due scatole, l'unico limite alla disposizione degli oggetti nelle finestre sarà imposto dalla vostra fantasia. Ad es. possiamo avere una scatola orizzontale che ne contiene altre tre, la prima delle quali sarà una scatola verticale nella quale inseriremo dei bottoni, la seconda scatola orizzontale conterrà del testo, la terza altri bottoni

esempio 1

la differenza fra una vbox e una hbox si può notare osservando la zona contornata dal rosso e quella contornata dal verde. Entrambe le scatole contengono dei bottoni, ma la prima, essendo una vbox li ha disposti in verticale (rosso), la seconda, essendo una hboxi, li ha disposti in orizzontale. In conclusione, gli oggetti contenuti (bottoni) assumono una diversa disposizione in funzione della natura del contenitore.

I prototipi per la creazione di una scatola e l'impacchettamento di oggetti nella scatola sono i seguenti:


GtkWidget *gtk_hbox_new(gboolean homogeneus, 
                                     gint spacing);

GtkWidget *gtk_box_pack_start(GtkWidget *box,
                                            GtkWidget *child,
                                            gboolean expand,  
                                            gboolean fill,    
                                            guint padding);

In gtk_hbox_new (o gtk_vbox_new) homogeneus stabilisce se gli oggetti contenuti nella scatola devono avere tutti la stessa dimensione (orizzontale se hbox, verticale se vbox); se il flag è settato a TRUE, allora tutti gli oggetti avranno la medesima dimensione di quello di dimensione massima. Ad esempio, supponiamo di avere due bottoni orizzontali, il primo con etichetta "OK" e il secondo con etichetta "precipitevolissimevolmente"; se homogeneus è settato a TRUE, allora il bottone con "OK" sarà lungo quanto quello con "precipitevolissimevolmente", mentre se homogeneus è FALSE i bottoni occuperanno lo spazio minimo che è loro necessario.

Se homogeneus è TRUE il valore expand della funzione gtk_box_pack_start sarà automaticamente TRUE.
Spacing definisce quanto gli oggetti saranno distanti fra di loro, cioè ndica quanto "spazio bianco" deve esserci tra due oggetti.

In gtk_box_pack_start (o gtk_box_pack_end) il primo argomento, box, è l'oggetto contenitore, il secondo, child, è l'oggetto che sarà contenuto. Expand verifica se gli oggetti devono essere impacchettati in modo da occupare tutto lo spazio disponibile nella scatola. Se è TRUE la scatola sarà espansa ed occuperà tutto lo spazio che le è stato assegnato, e gli oggetti che contiene occuperanno tutto questo spazio; se expand è FALSE, la scatola avrà la dimensione necessaria minima a contenere esattamente gli oggetti. Con fill si stabilisce se lo spazio disponibile nella scatola debba essere allocato agli oggetti (TRUE) o debba essere mantenuto come spazio fra gli oggetti (FALSE); infine padding determina lo spazio vuoto da lasciare ai lati dell'oggetto.

gtk_box_pack_start e gtk_box_pack_end indicano in che modo impacchettare gli oggetti. Con gtk_box_pack_start si inizierà a impacchettare da sinistra verso destra, nel caso di una hbox, e dall'alto verso il basso per una vbox. Se per esempio decidessi di impacchettare i bottoni "OK" "CANCELLA" "ANNULLA" in una vbox, esattamente con questo ordine, con gtk_box_pack_start, avrei in alto "OK" seguito da "CANCELLA" e per ultimo "ANNULLA". gtk_box_pack_end si comporta nello stesso modo, la differenza consiste nel fatto che inizia ad impacchettare da destra verso sinistra e dal basso verso l'alto.

In GTK+ è anche presente l'oggetto table, che realizza un po' quello che è una tabella HTML, o, secondo quanto visto finora, un vettore di hbox in una vbox (o di vbox in un hbox, spero abbiate capito... ;). Le API, una volta familiarizzato con le box, sono molto intuitive, per cui non scenderemo nel dettaglio.

Proseguendo con il programma si incontra gtk_button_new. Un bottone può essere creato in due modi diversi, con gtk_button_new() o gtk_button_new_with_label(). Il primo creerà una scatola vuota e sarà compito nostro inserirci qualcosa (un'etichetta, una pixmap, una pixmap + un'etichetta) utilizzando la teoria dell'impacchettamento. La seconda ci sforna un bottone bello e pronto con un'etichetta sopra. I bottoni così creati devono essere collegati, ognuno, con il proprio segnale di attivazione (ad es. clicked), alla propria funzione di callback.

L'ultima funzione da esaminare per quel che riguarda i widget presenti in questo programma è gtk_widget_show(). Ogni widget creato rimarrà invisibile finché non sarà espressamente visualizzato. È opportuno che la finestra principale venga visualizzata per ultima, per evitare di veder svolazzare per lo schermo barre di scorrimento, menu e quant'altro. Ogni oggetto visualizzato potrà essere facilmente nascosto alla vista richiamando l'opportuna api gtk_widget_hide(). Se osservate il codice relativo alla creazione del widget bottone2, noterete che il bottone è collegato alla funzione di callback hide_show_mondo(), e il dato che viene passato a questa funzione non è altro che il puntatore al primo bottone (bottone1). All'interno della callback, questo puntatore viene utilizzato per mostrare o nascondere il primo bottone. Si può comunque evitare di chiamare gtk_widget_show() per ogni widget creato: nella funzione che crea la finestra principale e che contiene tutti i widget, basta semplicemente chiamare la routine gtk_widget_show_all().

Il primo argomento della funzione di callback è un puntatore al widget che emette il segnale che la farà richiamare. Questo puntatore è usato per cambiare l'etichetta sul bottone.

Infine, occorre richiamare la funzione gtk_main(), che ci fa entrare nel ciclo principale del programma. Per uscire da questo ciclo, e terminare il programma, occorre richiamare la funzione gtk_main_quit(). Da notare che gtk_main() può essere richiamato ricorsivamente; ogni successiva chiamata di gtk_main_quit() terminerà una singola istanza, a partire dall'ultimo livello di ricorsione. La funzione gtk_main_level() può essere usata per conoscere il livello di ricorsione del ciclo main corrente, più precisamente il numero di livelli esistenti da quello corrente in basso.

In questo breve programma si è fatta conoscenza con il sistema ad oggetti implementato da GTK+. Per chi fosse interessato e avesse voglia di approfondire l'argomento vi rimando al prossimo articolo, nel quale sarà presentata l'implementazione degli oggetti in GTK+.

Siete invitati inoltre a confrontare quanto detto finora coi documenti contenenti le API Reference di GTK+, GLib e GDK; essi sono una fonte preziosa di informazioni, e una volta familiarizzato coi concetti di base dei widget, è relativamente semplice consultarli per ricavare da soli come far sì che quel widget che proprio non ne vuole sapere di fare quello che dico io... :)

Nicola Fragale

Sono iscritto in Ingegneria Informatica al Federico II di Napoli. I miei ultimi interessi informatici sono rivolti all'xml e particolarmente a GConf. Attualmente sto scrivendo una rubrica per GTK+ e GNOME, naturalmente rilasciata con licenza GPL. Nel tempo libero mi piace programmare, leggere (Stephen King è il mio autore preferito), stare con la mia ragazza e sentire i miei amici.


<- PW: Introduzione - Copertina - PW: Alle radici dello GNOME - Seconda Puntata ->