sabato 30 agosto 2014

Tutorial Gtk+3 e Glade 3: come creare widget personali



INTRODUZIONE


Per prima cosa voglio ringraziare Tristan per avermi fatto entrare in questo mondo con la sua guida: http://blogs.gnome.org/tvb/2013/04/09/announcing-composite-widget-templates/.


Nonostante sia tuttora anche io poco esperto, so per esperienza che se siete alle prime armi con Gtk+3 e Glade, e vi trovate nella necessità di creare un vostro widget personale, di sicuro la guida che sto per presentarvi può esservi d'aiuto.

Essendo la programmazione tramite Gtk+ orientata agli oggetti, quello che ci basterà fare per risolvere il problema sarà crearci un oggetto con le caratteristiche desiderate. Per farlo, ovviamente, dovremo definire la classe corrispondente.
A questo punto basterà allocare un oggetto della nuova classe ed il gioco è fatto.

L'utilità della definizione di un nuovo widget sta soprattutto nel fatto di poter ripetere lo stesso oggetto nella nostra interfaccia avendolo definito una sola volta. In effetti con Glade tutto questo è un gioco da ragazzi, ma non lo è altrettanto definire il nuovo oggetto come tipo. 
Fortunatamente Gtk+3 mette a disposizione alcune funzioni che diminuiscono notevolmente il lavoro.

Nella guida utilizzarò un banalissimo esempio: nella finestra principale è presente un pulsante che permette la creazione del nuovo oggetto definito. Ogni oggetto avrà il proprio numero identificativo, e questo permetterà di verificare che in effetti si tratta di diverse istanze della stessa classe.


GLADE

L'interfaccia grafica che costruiremo è divisa in due parti: quella relativa alla finestra principale, e quella relativa al nostro widget. Questo significa che bisognerà lavorare su due file di glade separati.


mywidget.glade


Il primo passo da fare è aprire un nuovo progetto in Glade e aggiungere come padre del nostro oggetto un GtkGrid, ovvero un contenitore che ha la possibilità di essere diviso in più righe e colonne (si trova nella sezione containers).
Lo rinominiamo in "MyWidget" e spuntiamo la casella composite, grazie alla quale il nostro nuovo oggetto composto diventerà un template.






















Abbiamo creato il principale contenitore per il nostro oggetto, che può essere composto a sua volta da altri contenitori e oggetti. 

Nel mio esempio aggiungo un button (dalla sezione Control and Display) nel primo box del grid, lo rinomino “widget_button” e faccio altrettanto con l'etichetta del button scrivendo “Print message”.
Il pulsante, quando premuto, servirà per stampare un messaggio.





















Nel secondo box del grid aggiungo una textview, ovvero l'oggetto in cui verrà visualizzato il messaggio stampato, rinominandola in “widget_textview”.





















L'ultimo oggetto da aggiungere al nostro widget è un textbuffer, ovvero un oggetto che non appartiene all'interfaccia bensì al modello. È infatti qui che scriveremo realmente la stringa del messaggio, mentre la textview appena aggiunta servirà solo per visualizzarlo. Essendo parte del modello, non è necessario aggiungere il textbuffer dentro il contenitore padre; lo aggiungiamo quindi semplicemente nella pagina di Glade e lo rinominiamo in “widget_textbuffer”. A differenza degli oggetti precedenti, questo si trova nella sezione Miscellaneous.





















Come ultimo punto bisogna aggiungere le signal agli oggetti: nel mio caso aggiungo una callback per collegare la pressione del button con la funzione di stampa del nostro messaggio. In alcuni casi può non essere necessario, ma in questo dobbiamo passare un parametro alla funzione: come si capirà dal codice, per poter accedere al numero dell'oggetto allocato, bisogna che venga passato il nostro nuovo widget intero. Il campo handler indica la funzione che verrà chiamata quando verrà catturata la signal del button. Il campo user_data indica l'oggetto che si vuole passare alla funzione. Il campo swap indica che come primo parametro della signal verrà passato l'oggetto user_data e non quello che emette la chiamata alla funzione. Nel codice questo passaggio sarà più chiaro.

A questo punto l'interfaccia grafica e la composizione del nostro oggetto personale sono definite; salviamo il file in “mywidget.glade” e passiamo all'interfaccia principale del nostro programma.



main.glade



Apriamo un nuovo progetto in Glade.

Come nell'esempio precedente, aggiungiamo un contenitore padre, che questa volta sarà una window (sezione Toplevels). Questo oggetto infatti, così come tutti quelli della sua sezione, permette di essere visualizzato come principale, ovvero senza essere incluso in un contenitore. Rinomiamo l'oggetto in “window” e aggiungiamo all'interno un altro button, rinominandolo in “main_button”.

Infine, come nel caso del nostro widget, aggiungiamo alla signal destroy dell'oggetto “window” la funzione “on_window_destroy”. Salviamo il file in "main.glade" e passiamo al codice del nostro programma.





C, Gtk+3

Iniziamo dal nostro widget:

mywidget.h

Nel file header del nostro widget definiamo due struct, una per la classe (il nuovo tipo) ed una per l'istanza della classe (l'oggetto).

struct _MyWidget {

GtkBox Grid;
GtkWidget *widget_button;
GtkWidget *widget_label;
GtkWidget *widget_textview;
GtkTextBuffer *widget_textbuffer;
int number;
};

struct _MyWidgetClass {


GtkGridClass parent_class;
};


Nella struct _MyWidget troviamo l'oggetto padre e dei puntatori agli oggetti che abbiamo definito nella nostra interfaccia con Glade. È molto importante che gli oggetti definiti nell'interfaccia abbiano esattamente lo stesso nome di quelli definiti nella struct. Solo in questo modo si riuscirà ad allocare gli oggetti così come definiti con Glade.
La struct che definisce il nuovo tipo contiene solo la classe padre, necessaria a creare il legame padre-figlio fra i tipi.


mywidget.c

La prima cosa da fare nel file i cui troviamo l'implementazione del nostro widget è: 

G_DEFINE_TYPE(MyWidget, my_widget, GTK_TYPE_GRID);

Questa macro, messa a disposizione da Gtk+, permette di risparmiarci molto tempo e codice. Viene usata per definire il nostro nuovo tipo, legandolo all'oggetto padre e le funzioni che serviranno per inizializzare la classe e l'oggetto. Implementa inoltre di default la funzione my_widget_get_type(), necessaria per fare il cast di un oggetto al nostro nuovo tipo.



static void
my_widget_class_init (MyWidgetClass *klass) {
GtkWidgetClass *widget_class;

widget_class = GTK_WIDGET_CLASS (klass);

gtk_widget_class_set_template_from_resource (widget_class, "/my/mywidget.glade");

gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(widget_class),MyWidget, widget_button);
gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(widget_class), MyWidget, widget_textview);
gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(widget_class), MyWidget, widget_textbuffer);

gtk_widget_class_bind_template_callback (widget_class, widget_print_message);
}

Questa è la funzione che inizializza il nuovo tipo, estendendo la classe del padre: viene creato il collegamento tra il codice xml creato con Glade e la nuova classe. Successivamente vengono collegati i widget figli definiti nel template con la nuova classe, ed infine viene fatto il collegamento tra le callback definite nel template e la nuova classe.




static void
my_widget_init (MyWidget *widget)
{

static int count = 0;
widget->number = count;
count ++;
gtk_widget_init_template (GTK_WIDGET (widget));
}

Questa è la funzione che inizializza l'oggetto del nostro nuovo tipo (l'istanza della nuova classe).
La struct dell'istanza ha un campo number che si riferisce al numero di oggetto allocato. Nella funzione viene dunque settato grazie ad un contatore che poi viene incrementato. In ultimo viene effettivamente allocato il nuovo widget inizializzato secondo il template e, in questo caso, le nostre istruzioni.








my_widget_new ()
{
return g_object_new (MY_TYPE_WIDGET, NULL);
}


È la funzione che permette di ritornare un oggetto del nuovo tipo e che ci serve per allocarlo da altre funzioni.







void
widget_print_message(MyWidget *widget, GtkButton *button) {

GtkTextBuffe
r *buffer = widget->widget_textbuffer;
char buf[20];
sprintf(buf, "I'm the widget number: %d", widget->number);
gtk_text_buffer_insert_at_cursor(buffer, buf, strlen(buf)); /*inserts text into text buffer*/
gtk_text_view_set_buffer(GTK_TEXT_VIEW(widget->widget_textview), buffer);
}


Questa è l'implementazione della callback che abbiamo dichiarato nell'interfaccia con Glade. Come si può notare, rispetto ad una normale callback, i parametri formali sono invertiti. Ed è proprio per questo motivo che abbiamo dovuto spuntare la casella swap all'atto della modifica delle signal in Glade. Riempiamo il textbuffer del nostro widget con il messaggio e lo visualizziamo sulla textview.




main.c

gint
main (gint argc, gchar *argv[]) {
GtkWidget *window;

GError *error = NULL;
GtkBuilder *gtkBuilder = NULL; /*main builder*/
gtk_init (&argc, &argv);

gtkBuilder = gtk_builder_new();
if (!gtk_builder_add_from_file (gtkBuilder, "main.glade", &error)) {
g_warning ("%s", error->message);
g_error_free (error);
 }

gtk_builder_connect_signals(gtkBuilder, 0);
window = GTK_WIDGET(gtk_builder_get_object(gtkBuilder, "window"));

gtk_widget_show_all (window);
g_object_unref(G_OBJECT(gtkBuilder));
gtk_main ();

return 0;
}

La funzione main è molto semplice: si utilizza l'oggetto Builder per accedere all'interfaccia così come descritta nel file xml creato con Glade. La funzione gtk_builder_add_from_file() in effetti fa proprio questo. A questo punto si “catturano” le signal definite e gli oggetti (nel nostro caso la finestra principale, chiamata “window”) direttamente dal builder. Alla fine si visualizza tutto ciò che è contenuto nella finestra tramite gtk_widget_show_all (window) e si dereferenzia il Builder.




void
on_window_destroy (GtkWidget *object, GThread *thread) { /*callback to close the main window*/
printf("Byebye\n");
gtk_main_quit ();
}

Questa è la callback che chiude il programma quando viene distrutto l'oggetto “window”.




void
create_widget(GtkButton *button, gpointer *user_data) {

GtkWidget *widget_window;
GtkWidget *widget;
char title[20];
int widget_number;
widget = my_widget_new();
widget_number = return_widget_number(MY_WIDGET(widget));
sprintf(title, "MyWidget %d", widget_number);
widget_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); 

gtk_window_set_title(GTK_WINDOW(widget_window), title);
gtk_container_add (GTK_CONTAINER (widget_window), GTK_WIDGET(widget));
gtk_widget_show_all (widget_window);
}

Ed ecco infine la callback che permette di creare il nostro widget personale ogni volta che viene premuto il pulsante nella finestra principale.
Come si può notare, per visualizzare il nostro oggetto, il cui padre è un GtkGrid, abbiamo bisogno di un contenitore toplevel. In questo banalissimo caso utilizzo una finestra, ma si potrebbe visualizzare il nostro widget dentro un notebook che è a sua volta dentro un contenitore toplevel, esattamente come facciamo ora con una semplice finestra. Utilizzando poi il numero della pagina, viene settato il titolo della finestra che visualizza il nostro oggetto.
Infine aggiungendo il nostro oggetto (creato con la funzione my_widget_new()) al suo contenitore, visualizziamo il tutto come fatto nella funzione main().





CONCLUSIONI

Questa semplice guida non porta di certo a risultati esaltanti, ma serve per affrontare per la prima volta l'idea di creare un widget personale. Gtk+3 è uno strumento potente e completo e quindi chiaramente offre ulteriori possibilità nella creazione di widget personali, come quella di utilizzare variabili private, interfacce, e tutto quello di cui dispone un linguaggio O.O. Mi sono voluto soffermare sugli aspetti fondamentali, soprattutto perchè li ritengo più utili che buttarsi in un progetto complesso e difficilmente comprensibile.
Ma con un pò di fantasia e di volontà, grazie a questi strumenti, si possono fare davvero dei bei lavori.

Per maggiori dettagli vi rimando alla documentazione ufficiale:


Qualsiasi problema troviate nella guida, vi prego di segnalarmelo.

Nessun commento:

Posta un commento