Whole document tree
    

Whole document tree

GTK Tutorial: Scrivere un proprio Widget Avanti Indietro Indice

19. Scrivere un proprio Widget

19.1 Panoramica

Anche se la distribuzione GTK contiene molto tipi di widget che possono coprire molte necessità basilari, può essere necessario costruirsi un proprio widget. GTK usa molto l'ereditarietà tra i vari widget e, di solito, vi è un widget che si avvicina a quello che ti servirebbe, ed è spesso possibile creare un nuovo widget con poche linee di codice. Ma prima di iniziare il lavoro su un nuovo widget, vediamo se qualcuno non lo ha già creato. Questo eviterà un duplicazione di lavoro e farà sì che i widget non-GTK puri siano minimi, così da aiutare sia chi crea il codice che chi l'interfaccia per applicazioni GTK molto grosse. D'altra parte, quando hai finito di scrivere un widget, annuncialo a tutto il mondo così che le altre persone ne possano beneficiare. Il miglioro modo dove farlo è la gtk-list.

I sorgenti completi per i widget di esempio possono essere presi dallo stesso sito da cui avete scaricato questo tutorial, oppure da:

http://www.msc.cornell.edu/~otaylor/gtk-gimp/tutorial

19.2 L'anatomia di un widget

Per creare un nuovo widget è importante aver capito come gli ogetti di GTK lavorano. Questa sezione è solo una breve spiegazione. Guarda la documentazione di riferimento per maggiori dettagli.

I widget GTK sono implementati in un modo orientato agli oggetti, anche se usando il C standard. Questo aumenta notevolmente la portabilità e la stabilità, specialmente per le correnti generazioni di compilatori C++; comunque questo significa che chi scrive un widget deve fare attenzione ad alcuni dettagli di implementazione. L'informazione comune a tutte le istanze di una classe di widget (ad esempio: a tutti i bottoni) è memorizzata class structure. C'e' solamente una copia di questo in cui sono memorizzate le informazioni riguardanti i segnali della classe (assomiglia ad una funzione virtuale in C). Per supportare l'ereditarietà il primo campo della struttura di una classe deve essere una copia della struttura della classe genitore. La dichiarazione della struttura della classe GtkButton è:

struct _GtkButtonClass
{
  GtkContainerClass parent_class;

  void (* pressed)  (GtkButton *button);
  void (* released) (GtkButton *button);
  void (* clicked)  (GtkButton *button);
  void (* enter)    (GtkButton *button);
  void (* leave)    (GtkButton *button);
};

Quando un bottone viene trattato come un contenitore (ad esempio quando viene ridimensionato) si può fare il cast della struttura della sua classe con la GtkContainerClass, e usare i campi rilevanti per gestire i segnali.

C'è anche una struttura per ogni widget che viene creata ad ogni istanza. Questa struttura ha campi per memorizzare le informazioni che sono differenti per ogni volta che il widget viene istanziato. Chiameremo questa struttura la struttura oggetto. Per la classe Bottone, questa ha l'aspetto:

struct _GtkButton
{
  GtkContainer container;

  GtkWidget *child;

  guint in_button : 1;
  guint button_down : 1;
};

Si noti che, similmente alla struttura della classe, il primo campo è la struttura dell'oggetto della classe madre, così che, se necessario, si può fare il cast di questa struttura con quella dell'oggetto della classe madre.

19.3 Creare un Widget composto

Introduzione

Un tipo di widget a cui potreste essere interessati è un widget che è semplicemnte un aggregato di altri widget GTK. Questo tipo di widget non fa nulla che non possa essere fatto creando un nuovo widget, ma fornisce un modo conveniente per inscatolare elementi dell'interfaccia utente per poi riutilizzarli. I widget FileSelection e ColorSelection della ditribuzione standard sono esempi di questo tipo di widget.

Il widget di esempio che creeremo in questo capitolo è il Tictactoe, un vettore 3x3 di bottoni a commutazione il quale emette un segnale quando tutti e 3 i bottoni di una riga, colonna o di una diagonale sono premuti.

Scegliere la classe madre

La classe madre per un widget composto e' tipicamente la classe contenitrice che racchiude tutti gli elementi del widget composto. Per esempio, la classe madre del widget FileSelection è la classe Dialog. Visto che i nostri bottoni sono inseriti in una tabella, è naturale pensare che la nostra classe madre possa essere la GtkTable. Sfortunatamente, così non è. La creazione di un widget è diviso tra 2 funzioni : la funzione WIDGETNAME_new() che viene invocata dall'utente, e la funzione WIDGETNAME_init() che ha il compito principale di inizializzare il widget che è indipendente dai valori passati alla funzione _new(). Widget figli o discendenti possono chiamare, solamente, la funzione del loro widget genitore. Ma questa divisione del lavoro non funziona bene per la tabella, la quale, quando creata, necessita di conoscere il numero di righe e colonne che la comporrà. A meno che non vogliamo duplicare molte delle fuinzionalità della gtk_table_new() nel nostro widget Tictactoe, faremmo meglio a evitare di derivarlo dalla GtkTable. Per questa ragione lo deriviamo invece da GtkVBox, e uniamo la nostra tabella dentro il VBox.

Il File Header

Ogni classe di widget ha un file header il quale dichiara l'oggetto e la struttura della classe del widget, comprese le funzioni pubbliche. Per prevenire duplicati di definizioni, noi includiamo l'intero file header fra:

#ifndef __TICTACTOE_H__
#define __TICTACTOE_H__
.
.
.
#endif /* __TICTACTOE_H__ */

E per far felici i programmi in C++ che includono il nostro file header, in:

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
.
.
.
#ifdef __cplusplus
}
#endif /* __cplusplus */

Insieme alle funzioni e alle strutture, dichiariamo tre macro standard nel nostro file header, TICTACTOE(obj), TICTACTOE_CLASS(klass), e IS_TICTACTOE(obj), i quali rispettivamente fanno il cast di un puntatore ad un puntatore ad un ogetto od ad una struttura di classe, e guarda se un oggetto è un widget Tictactoe.

Qui vi è il file header completo:

/* tictactoe.h */

#ifndef __TICTACTOE_H__
#define __TICTACTOE_H__

#include <gdk/gdk.h>
#include <gtk/gtkvbox.h>

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */

#define TICTACTOE(obj)          GTK_CHECK_CAST (obj, tictactoe_get_type (), Tictactoe)
#define TICTACTOE_CLASS(klass)  GTK_CHECK_CLASS_CAST (klass, tictactoe_get_type (), TictactoeClass)
#define IS_TICTACTOE(obj)       GTK_CHECK_TYPE (obj, tictactoe_get_type ())


typedef struct _Tictactoe       Tictactoe;
typedef struct _TictactoeClass  TictactoeClass;

struct _Tictactoe
{
  GtkVBox vbox;
  
  GtkWidget *buttons[3][3];
};

struct _TictactoeClass
{
  GtkVBoxClass parent_class;

  void (* tictactoe) (Tictactoe *ttt);
};

guint          tictactoe_get_type        (void);
GtkWidget*     tictactoe_new             (void);
void           tictactoe_clear           (Tictactoe *ttt);

#ifdef __cplusplus
}
#endif /* __cplusplus */

#endif /* __TICTACTOE_H__ */

La funzione _get_type()

Continuiamo ora con l'implementazione del nostro widget. Una funzione basilare di ogni widget è la funzione WIDGETNAME_get_type(). Questa funzione, quando chiamata la prima volta, comunica a GTK la classe del widget, e ottiene un identificativo univoco per la classe del widget. Chiamate successive restituiscono semplicemente l'identificativo.

guint
tictactoe_get_type ()
{
  static guint ttt_type = 0;

  if (!ttt_type)
    {
      GtkTypeInfo ttt_info =
      {
        "Tictactoe",
        sizeof (Tictactoe),
        sizeof (TictactoeClass),
        (GtkClassInitFunc) tictactoe_class_init,
        (GtkObjectInitFunc) tictactoe_init,
        (GtkArgSetFunc) NULL,
        (GtkArgGetFunc) NULL
      };

      ttt_type = gtk_type_unique (gtk_vbox_get_type (), &ttt_info);
    }

  return ttt_type;
}

La struttura GtkTypeInfo ha la seguente definizione:

struct _GtkTypeInfo
{
  gchar *type_name;
  guint object_size;
  guint class_size;
  GtkClassInitFunc class_init_func;
  GtkObjectInitFunc object_init_func;
  GtkArgSetFunc arg_set_func;
  GtkArgGetFunc arg_get_func;
};

I campi di questa struttura sono abbastanza auto-esplicativi. Ignoreremo, per ora, i campi arg_set_func e arg_get_func: hanno un ruolo importante, ma ancora largamente non implementato, nel permettere ai linguaggi interpretati di settare convenientemente le opzioni del widget. Una volta che il GTK ha completato correttamente una copia di questa struttura, sa come creare un oggetto di un particolare widget.

La funzione _class_init()

La funzione WIDGETNAME_class_init() inizialiazza i campi della struttura della classe del widget, e setta ogni segnale della classe. Per il nostro widget Tictactoe ha il seguente aspetto:


enum {
  TICTACTOE_SIGNAL,
  LAST_SIGNAL
};

static gint tictactoe_signals[LAST_SIGNAL] = { 0 };

static void
tictactoe_class_init (TictactoeClass *class)
{
  GtkObjectClass *object_class;

  object_class = (GtkObjectClass*) class;
  
  tictactoe_signals[TICTACTOE_SIGNAL] = gtk_signal_new ("tictactoe",
                                         GTK_RUN_FIRST,
                                         object_class->type,
                                         GTK_SIGNAL_OFFSET (TictactoeClass, tictactoe),
                                         gtk_signal_default_marshaller, GTK_TYPE_NONE, 0);


  gtk_object_class_add_signals (object_class, tictactoe_signals, LAST_SIGNAL);

  class->tictactoe = NULL;
}

Il nostro widget ha semplicemente il segnale ``tictactoe'' che è invocato quando una riga, colonna o diagonale è completamente premuta. Non tutti i widget composti necessitano di segnali, quindi se stai leggendo questo per la prima volta, puoi anche saltare alla prossima sezione, dal momento che a questo punto le cose diventano un po' complicate.

La funzione:

gint   gtk_signal_new (const gchar         *name,
                       GtkSignalRunType    run_type,
                       GtkType             object_type,
                       gint                function_offset,
                       GtkSignalMarshaller marshaller,
                       GtkType             return_val,
                       guint               nparams,
                       ...);

crea un nuovo segnale. I parametri sono:

  • name: Il nome del segnale.
  • run_type: Se il segstore predefinito viene eseguito prima o dopo di quello dell'utente. Di norma questo sarà GTK_RUN_FIRST, o GTK_RUN_LAST, anche se ci sono altre possibilità.
  • object_type: l'identificativo dell'oggetto a cui questo segnale si riferisce. Esso sarà anche applicato agli oggetti discendenti.
  • function_offset: L'offset nella struttura della classe di un puntatore al gestore predefinito.
  • marshaller: una funzione che è usata per invocare il gestore del segnale. Per gestori di segnali che non hanno argomenti oltre all'oggetto che emette il segnale e i dati dell'utente, possiamo usare la funzione predefinita gtk_signal_default_marshaller
  • return_val: Il tipo del valore di ritorno.
  • nparams: Il numero di parametri del gestore di segnali (oltre ai due predefiniti menzionati sopra)
  • ...: i tipi dei parametri

Quando si specificano i tipi, si usa l'enumerazione GtkType:

typedef enum
{
  GTK_TYPE_INVALID,
  GTK_TYPE_NONE,
  GTK_TYPE_CHAR,
  GTK_TYPE_BOOL,
  GTK_TYPE_INT,
  GTK_TYPE_UINT,
  GTK_TYPE_LONG,
  GTK_TYPE_ULONG,
  GTK_TYPE_FLOAT,
  GTK_TYPE_DOUBLE,
  GTK_TYPE_STRING,
  GTK_TYPE_ENUM,
  GTK_TYPE_FLAGS,
  GTK_TYPE_BOXED,
  GTK_TYPE_FOREIGN,
  GTK_TYPE_CALLBACK,
  GTK_TYPE_ARGS,

  GTK_TYPE_POINTER,

  /* sarebbe bello poter togliere alla fine i prossimi due */
  GTK_TYPE_SIGNAL,
  GTK_TYPE_C_CALLBACK,

  GTK_TYPE_OBJECT

} GtkFundamentalType;

gtk_signal_new() restituisce un identificatore unico intero per il segnale, che memorizziamo nel vettore tictactoe_signals, che indicizzeremo usando una enumerazione. (Convenzionalmente, gli elementi dell'enumerazione sono i nomi dei segnali, in maiuscolo, ma qui ci potrebbe essere un conflitto con la macro TICTACTOE(), quindi l'abbiamo chiamato TICTACTOE_SIGNAL

Dopo aver creato un nostro segnale, abbiamo bisogno di dire a GTK di associare il nostro segnale alla classe Tictactoe. Lo facciamo invocando gtk_object_class_add_signals(). Settiamo quindi a NULL il puntatore che punta al gestore predefinito per il segnale ``tictactoe'' a NULL, indicando che non ci sono azioni predefinite.

La funzione _init()

Ogni classe di Widget necessita anche di una funzione per inizializzare la struttura dell'oggetto. Usualmente questa funzione ha il ruolo abbastanza limitato di assegnare ai campi della struttura i valori predefiniti. Per widget composti, comunque, questa funzione crea, anche, i widget componenti del widget composto.


static void
tictactoe_init (Tictactoe *ttt)
{
  GtkWidget *table;
  gint i,j;
  
  table = gtk_table_new (3, 3, TRUE);
  gtk_container_add (GTK_CONTAINER(ttt), table);
  gtk_widget_show (table);

  for (i=0;i<3; i++)
    for (j=0;j<3; j++)
      {
        ttt->buttons[i][j] = gtk_toggle_button_new ();
        gtk_table_attach_defaults (GTK_TABLE(table), ttt->buttons[i][j], 
                                   i, i+1, j, j+1);
        gtk_signal_connect (GTK_OBJECT (ttt->buttons[i][j]), "toggled",
                            GTK_SIGNAL_FUNC (tictactoe_toggle), ttt);
        gtk_widget_set_usize (ttt->buttons[i][j], 20, 20);
        gtk_widget_show (ttt->buttons[i][j]);
      }
}

E il resto...

C'è un'altra funzione che ogni widget (eccetto i Widget di base come GtkBin che non possono essere instanziati) deve avere : la funzione che l'utente invoca per creare un oggetto di quel tipo. Questa è convenzionalmente chiamata WIDGETNAME_new(). In alcuni widget, non nel caso del nostro Tictactoe, questa funzione richiede degli argomenti, e fa alcune operazioni basandosi su di essi. Le altre due funzioni sono specifiche del widget Tictactoe.

tictactoe_clear() è una funzione pubblica che resetta tutti i bottoni, nel widget, allo stato iniziale (non premuto). Notate l'uso di gtk_signal_handler_block_by_data() per impedire che il nostro gestore dei segnali venga attivato quando non ce n'è bisogno.

tictactoe_toggle() è il gestore del segnale che viene invocato quando l'utente preme il bottone. Esso guarda se vi è qualche combinazione vincente che coinvolge i bottoni premuti, e nel caso ci fosse, emette il segnale ``tictactoe''.

  
GtkWidget*
tictactoe_new ()
{
  return GTK_WIDGET ( gtk_type_new (tictactoe_get_type ()));
}

void           
tictactoe_clear (Tictactoe *ttt)
{
  int i,j;

  for (i=0;i<3;i++)
    for (j=0;j<3;j++)
      {
        gtk_signal_handler_block_by_data (GTK_OBJECT(ttt->buttons[i][j]), ttt);
        gtk_toggle_button_set_state (GTK_TOGGLE_BUTTON (ttt->buttons[i][j]),
                                     FALSE);
        gtk_signal_handler_unblock_by_data (GTK_OBJECT(ttt->buttons[i][j]), ttt);
      }
}

static void
tictactoe_toggle (GtkWidget *widget, Tictactoe *ttt)
{
  int i,k;

  static int rwins[8][3] = { { 0, 0, 0 }, { 1, 1, 1 }, { 2, 2, 2 },
                             { 0, 1, 2 }, { 0, 1, 2 }, { 0, 1, 2 },
                             { 0, 1, 2 }, { 0, 1, 2 } };
  static int cwins[8][3] = { { 0, 1, 2 }, { 0, 1, 2 }, { 0, 1, 2 },
                             { 0, 0, 0 }, { 1, 1, 1 }, { 2, 2, 2 },
                             { 0, 1, 2 }, { 2, 1, 0 } };

  int success, found;

  for (k=0; k<8; k++)
    {
      success = TRUE;
      found = FALSE;

      for (i=0;i<3;i++)
        {
          success = success && 
            GTK_TOGGLE_BUTTON(ttt->buttons[rwins[k][i]][cwins[k][i]])->active;
          found = found ||
            ttt->buttons[rwins[k][i]][cwins[k][i]] == widget;
        }
      
      if (success && found)
        {
          gtk_signal_emit (GTK_OBJECT (ttt), 
                           tictactoe_signals[TICTACTOE_SIGNAL]);
          break;
        }
    }
}

E finalmente un programma di esempio che usa il nostro widget Tictactoe:

#include <gtk/gtk.h>
#include "tictactoe.h"

/* Invocato quando una riga, colonna o diagonale e' completata. */
void
win (GtkWidget *widget, gpointer data)
{
  g_print ("Yay!\n");
  tictactoe_clear (TICTACTOE (widget));
}

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

  window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  
  gtk_window_set_title (GTK_WINDOW (window), "Aspect Frame");
  
  gtk_signal_connect (GTK_OBJECT (window), "destroy",
                      GTK_SIGNAL_FUNC (gtk_exit), NULL);
  
  gtk_container_border_width (GTK_CONTAINER (window), 10);

  /* Crea un nuovo widget Tictactoe. */
  ttt = tictactoe_new ();
  gtk_container_add (GTK_CONTAINER (window), ttt);
  gtk_widget_show (ttt);

  /* E gli aggancia il segnale "tictactoe" */
  gtk_signal_connect (GTK_OBJECT (ttt), "tictactoe",
                      GTK_SIGNAL_FUNC (win), NULL);

  gtk_widget_show (window);
  
  gtk_main ();
  
  return 0;
}

19.4 Creare un widget a partire da zero

Introduzione

In questa sezione impareremo meglio come i widget si mostrano sullo schermo e interagiscono con gli eventi. Come esempio, creeremo un widget di quadrante analogico con un puntatore che l'utente può trascinare per assegnare il valore.

Mostrare un widget sullo schermo

Ci sono alcuni passi che sono necessari nella visualizzazione sullo schermo. Dopo che il widget è stato creato con una chiamata a WIDGETNAME_new(), sono necessarie alcune altre funzioni:

  • WIDGETNAME_realize() è responsabile della creazione di una finestra X per il widget se ne ha una.
  • WIDGETNAME_map() è invocata dopo che l'utente ha chiamato gtk_widget_show(). E' responsabile di vedere se il widget è attualmente disegnato sullo schermo (mappato). Per una classe contenitore, essa deve anche creare chiamate alle funzioni map()> per ogni widget figlio.
  • WIDGETNAME_draw() è invocata quando gtk_widget_draw() viene chiamata per il widget o per uno dei suoi predecessori. Esso fa sì che l'attuale chiamata alla funzione di disegno del widget disegni il widget sullo schermo. Per la classe contenitore, questa funzione deve eseguire le chiamate alla funzioni gtk_widget_draw() di ogni suo widget figlio.
  • WIDGETNAME_expose() è un gestore per l'evento di esposizione per il widget. Esso crea le chiamate necessarie alle funzioni di disegno per disegnare la porzione che si è resa visibile. Per le classi contenitore, questa funzione deve generare gli eventi di ``expose'' per tutti i widget figli che non hanno una propria finestra (se essi hanno una loro finestra, sarà X che genererà i necessari eventi di expose).

Potete notare che le ultime due funzioni sono molto simili, ognuna è responsabile per il disegno del widget sullo schermo. Infatti molti tipi di widget non sanno relamente la differenza tra le due. La funzione di predefinita draw() nella classe widget, semplicemente genera un sintetico evento di ``expose'' per l'area da ridisegnare. Comunque, alcuni tipi di widget possono risparmiare tempo distinguendo le due funzioni. Per esempio, se un widget ha piu' finestre X, allora visto che l'evento ``expose'' identifica solo la finestra esposta, esso può ridisegnare solo la finestra interessata, cosa che non è possibile per chiamate a draw().

I widget contenitori, anche se essi non farebbero differenze, non possono semplicemente usare la funzione draw() perchè per i loro widget figli la differenza potrebbere essere importante. Comunque, sarebbe uno spreco duplicare il codice di disegno nelle due funzioni. La convenzione è che questi widget abbiano una funzione chiamata WIDGETNAME_paint() che disegna il widget, che è poi chiamata dalle funzioni draw() e expose()

Nell'approccio del nostro esempio, visto che il widget, ha una sola finestra, possiamo utilizzare il modo piu' semplice ed usare la funzione predefinita draw() e implementare solamente la funzione expose().

Le origini del widget Dial

Come tutti gli animali terresti sono semplicemente varianti del primo amfibio, i widget Gtk tendono ad essere varianti di altri widget, precedentemente scritti. Così, anche se questa sezione è intitolata ``Creare un widget a partire da zero", il nostro widget inizia in realtà con il codice sorgente del widget Range. Questo è stato preso come punto d'inizio perche' sarebbe carino se il nostro widget avesse la stessa interfaccia del widget Scale il quale è semplicemente una specializzazione del widget Range. Così, sebbene il codice sorgente e' presentato sotto in forma definitiva, non si deve pensare che sia stato scritto deus ex machina in questo modo. Se poi non avete familiarità con il funzionamento del widget Scale dal punto di vista di chi scrive un'applicazione, potrebbe essere una buona idea guardare indietro prima di continuare.

Le basi

Una parte del nostro widget potrebbe essere simile al widget Tictactoe. In primo luogo, abbiamo il file header:

/* GTK - The GIMP Toolkit
 * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the Free
 * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#ifndef __GTK_DIAL_H__
#define __GTK_DIAL_H__

#include <gdk/gdk.h>
#include <gtk/gtkadjustment.h>
#include <gtk/gtkwidget.h>


#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */


#define GTK_DIAL(obj)          GTK_CHECK_CAST (obj, gtk_dial_get_type (), GtkDial)
#define GTK_DIAL_CLASS(klass)  GTK_CHECK_CLASS_CAST (klass, gtk_dial_get_type (), GtkDialClass)
#define GTK_IS_DIAL(obj)       GTK_CHECK_TYPE (obj, gtk_dial_get_type ())


typedef struct _GtkDial        GtkDial;
typedef struct _GtkDialClass   GtkDialClass;

struct _GtkDial
{
  GtkWidget widget;

  /* Politica di update (GTK_UPDATE_[CONTINUOUS/DELAYED/DISCONTINUOUS]) */
  guint policy : 2;

  /* Bottone correntemente premuto o 0 altrimenti */
  guint8 button;

  /* Dimensione della componente Dial. */
  gint radius;
  gint pointer_width;

  /* ID del timer di update, o 0 altrimenti */
  guint32 timer;

  /* Angolo corrente. */
  gfloat angle;

  /* Vecchi valori dell'aggiustamento così sappiamo quando 
   * qualcosa cambia */
  gfloat old_value;
  gfloat old_lower;
  gfloat old_upper;

  /* L'oggetto adjustament che memorizza i dati per questo dial */
  GtkAdjustment *adjustment;
};

struct _GtkDialClass
{
  GtkWidgetClass parent_class;
};


GtkWidget*     gtk_dial_new                    (GtkAdjustment *adjustment);
guint          gtk_dial_get_type               (void);
GtkAdjustment* gtk_dial_get_adjustment         (GtkDial      *dial);
void           gtk_dial_set_update_policy      (GtkDial      *dial,
                                                GtkUpdateType  policy);

void           gtk_dial_set_adjustment         (GtkDial      *dial,
                                                GtkAdjustment *adjustment);
#ifdef __cplusplus
}
#endif /* __cplusplus */


#endif /* __GTK_DIAL_H__ */

Essendoci più cose da fare con questo widget, rispetto al precedente, abbiamo più cambi nella struttura dati, ma le altre cose sono abbastamza simili.

Dopo aver incluso i file di header e aver dichiarato alcune costanti, dobbiamo fornire alcune funzioni circa il widget e la sua inizializzazione.

#include <math.h>
#include <stdio.h>
#include <gtk/gtkmain.h>
#include <gtk/gtksignal.h>

#include "gtkdial.h"

#define SCROLL_DELAY_LENGTH  300
#define DIAL_DEFAULT_SIZE 100

/* Dichiarazioni di funzioni successive */

[ omesse per salvare spazio ]

/* variabili locali. */

static GtkWidgetClass *parent_class = NULL;

guint
gtk_dial_get_type ()
{
  static guint dial_type = 0;

  if (!dial_type)
    {
      GtkTypeInfo dial_info =
      {
        "GtkDial",
        sizeof (GtkDial),
        sizeof (GtkDialClass),
        (GtkClassInitFunc) gtk_dial_class_init,
        (GtkObjectInitFunc) gtk_dial_init,
        (GtkArgSetFunc) NULL,
        (GtkArgGetFunc) NULL,
      };

      dial_type = gtk_type_unique (gtk_widget_get_type (), &dial_info);
    }

  return dial_type;
}

static void
gtk_dial_class_init (GtkDialClass *class)
{
  GtkObjectClass *object_class;
  GtkWidgetClass *widget_class;

  object_class = (GtkObjectClass*) class;
  widget_class = (GtkWidgetClass*) class;

  parent_class = gtk_type_class (gtk_widget_get_type ());

  object_class->destroy = gtk_dial_destroy;

  widget_class->realize = gtk_dial_realize;
  widget_class->expose_event = gtk_dial_expose;
  widget_class->size_request = gtk_dial_size_request;
  widget_class->size_allocate = gtk_dial_size_allocate;
  widget_class->button_press_event = gtk_dial_button_press;
  widget_class->button_release_event = gtk_dial_button_release;
  widget_class->motion_notify_event = gtk_dial_motion_notify;
}

static void
gtk_dial_init (GtkDial *dial)
{
  dial->button = 0;
  dial->policy = GTK_UPDATE_CONTINUOUS;
  dial->timer = 0;
  dial->radius = 0;
  dial->pointer_width = 0;
  dial->angle = 0.0;
  dial->old_value = 0.0;
  dial->old_lower = 0.0;
  dial->old_upper = 0.0;
  dial->adjustment = NULL;
}

GtkWidget*
gtk_dial_new (GtkAdjustment *adjustment)
{
  GtkDial *dial;

  dial = gtk_type_new (gtk_dial_get_type ());

  if (!adjustment)
    adjustment = (GtkAdjustment*) gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0);

  gtk_dial_set_adjustment (dial, adjustment);

  return GTK_WIDGET (dial);
}

static void
gtk_dial_destroy (GtkObject *object)
{
  GtkDial *dial;

  g_return_if_fail (object != NULL);
  g_return_if_fail (GTK_IS_DIAL (object));

  dial = GTK_DIAL (object);

  if (dial->adjustment)
    gtk_object_unref (GTK_OBJECT (dial->adjustment));

  if (GTK_OBJECT_CLASS (parent_class)->destroy)
    (* GTK_OBJECT_CLASS (parent_class)->destroy) (object);
}

Notate che questa funzione init() fa meno rispetto all'analoga del widget Tictactoe, essendo questo un widget non composto, e la funzione new() fa di più, essendoci un argomento. Inoltre, notate che quando memorizziamo un puntatore all'oggetto Adjustment, incrementiamo il conteggio dei suoi riferimenti(e corrispondentemente lo decrementato quando non lo usiamo più) così che GTK può tener traccia di quando è possibile distruggerlo senza causare guai.

Inoltre, ci sono alcune funzioni per manipolare le opzioni del widget:

GtkAdjustment*
gtk_dial_get_adjustment (GtkDial *dial)
{
  g_return_val_if_fail (dial != NULL, NULL);
  g_return_val_if_fail (GTK_IS_DIAL (dial), NULL);

  return dial->adjustment;
}

void
gtk_dial_set_update_policy (GtkDial      *dial,
                             GtkUpdateType  policy)
{
  g_return_if_fail (dial != NULL);
  g_return_if_fail (GTK_IS_DIAL (dial));

  dial->policy = policy;
}

void
gtk_dial_set_adjustment (GtkDial      *dial,
                          GtkAdjustment *adjustment)
{
  g_return_if_fail (dial != NULL);
  g_return_if_fail (GTK_IS_DIAL (dial));

  if (dial->adjustment)
    {
      gtk_signal_disconnect_by_data (GTK_OBJECT (dial->adjustment), (gpointer) dial);
      gtk_object_unref (GTK_OBJECT (dial->adjustment));
    }

  dial->adjustment = adjustment;
  gtk_object_ref (GTK_OBJECT (dial->adjustment));

  gtk_signal_connect (GTK_OBJECT (adjustment), "changed",
                      (GtkSignalFunc) gtk_dial_adjustment_changed,
                      (gpointer) dial);
  gtk_signal_connect (GTK_OBJECT (adjustment), "value_changed",
                      (GtkSignalFunc) gtk_dial_adjustment_value_changed,
                      (gpointer) dial);

  dial->old_value = adjustment->value;
  dial->old_lower = adjustment->lower;
  dial->old_upper = adjustment->upper;

  gtk_dial_update (dial);
}

gtk_dial_realize()

Abbiamo ora raggiunto alcuni nuovi tipi di funzione. In primo luogo, abbiamo una funzione che crea la finestra di X. Noterete che viene passata alla funzione gdk_window_new() una maschera che specifica quali campi della struttura GdkWindowAttr non sono vuoti (ai rimanenti campi può essere dato il valore predefinito). Anche il modo con cui la maschera degli eventi del widget creata non è complicato. Chiameremo gtk_widget_get_events() per sapere la maschera degli eventi che l'utente ha specificato per questo widget (con gtk_widget_set_events()) e aggiungeremo gli eventi che ci possono interessare.

Dopo aver creato la finestra, settiamo lo stile e lo sfondo, e creiamo un puntatore al widget nel campo dei dati utente (user data) del GdkWindow. Quest'ultimo passo permette a GTK di mandare gli eventi della finestra al widget corretto.

static void
gtk_dial_realize (GtkWidget *widget)
{
  GtkDial *dial;
  GdkWindowAttr attributes;
  gint attributes_mask;

  g_return_if_fail (widget != NULL);
  g_return_if_fail (GTK_IS_DIAL (widget));

  GTK_WIDGET_SET_FLAGS (widget, GTK_REALIZED);
  dial = GTK_DIAL (widget);

  attributes.x = widget->allocation.x;
  attributes.y = widget->allocation.y;
  attributes.width = widget->allocation.width;
  attributes.height = widget->allocation.height;
  attributes.wclass = GDK_INPUT_OUTPUT;
  attributes.window_type = GDK_WINDOW_CHILD;
  attributes.event_mask = gtk_widget_get_events (widget) | 
    GDK_EXPOSURE_MASK | GDK_BUTTON_PRESS_MASK | 
    GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK |
    GDK_POINTER_MOTION_HINT_MASK;
  attributes.visual = gtk_widget_get_visual (widget);
  attributes.colormap = gtk_widget_get_colormap (widget);

  attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL | GDK_WA_COLORMAP;
  widget->window = gdk_window_new (widget->parent->window, &attributes, attributes_mask);

  widget->style = gtk_style_attach (widget->style, widget->window);

  gdk_window_set_user_data (widget->window, widget);

  gtk_style_set_background (widget->style, widget->window, GTK_STATE_ACTIVE);
}

Negoziazione della dimensione

Prima di visualizzare per la prima volta la finestra, e se il layout della finestra cambia, GTK chiede ad ogni widget, incluso nella finestra, la propria dimensione. Questa richiesta è fatta dalla funzione gtk_dial_size_request(). Non essendo il nostro widget un contenitore, e non avendo dei veri limiti per la propria dimensione, restituiamo semplicemnte un valore ragionevole.

static void 
gtk_dial_size_request (GtkWidget      *widget,
                       GtkRequisition *requisition)
{
  requisition->width = DIAL_DEFAULT_SIZE;
  requisition->height = DIAL_DEFAULT_SIZE;
}

Dopo che tutti i widget hanno restituito una dimensione ideale, viene calcolata la disposizione della finestra e ad ogni widget figlio è notificata la propria dimensione attuale . Usualmente, questo sarà almeno quanto richiesto, ma occasionalmente può essere più piccolo. La notifica della dimensione viene fatta dalla funzione gtk_dial_size_allocate(). Notate che questa funzione è utilizzata anche quando la finestra X del widget è spostata o modificata come dimensione.

static void
gtk_dial_size_allocate (GtkWidget     *widget,
                        GtkAllocation *allocation)
{
  GtkDial *dial;

  g_return_if_fail (widget != NULL);
  g_return_if_fail (GTK_IS_DIAL (widget));
  g_return_if_fail (allocation != NULL);

  widget->allocation = *allocation;
  if (GTK_WIDGET_REALIZED (widget))
    {
      dial = GTK_DIAL (widget);

      gdk_window_move_resize (widget->window,
                              allocation->x, allocation->y,
                              allocation->width, allocation->height);

      dial->radius = MAX(allocation->width,allocation->height) * 0.45;
      dial->pointer_width = dial->radius / 5;
    }
}
.

gtk_dial_expose()

Come menzionato sopra, tutto il lavoro di questo widget viene fatto nella gestione dell'evento ``expose''. Non c'è molto da notare su questo eccetto l'uso della funzione gtk_draw_polygon per disegnare il puntatore con un'ombreggiatura a tre dimensioni in accordo con il colore memorizzato nello stile del wiget.

static gint
gtk_dial_expose (GtkWidget      *widget,
                 GdkEventExpose *event)
{
  GtkDial *dial;
  GdkPoint points[3];
  gdouble s,c;
  gdouble theta;
  gint xc, yc;
  gint tick_length;
  gint i;

  g_return_val_if_fail (widget != NULL, FALSE);
  g_return_val_if_fail (GTK_IS_DIAL (widget), FALSE);
  g_return_val_if_fail (event != NULL, FALSE);

  if (event->count > 0)
    return FALSE;
  
  dial = GTK_DIAL (widget);

  gdk_window_clear_area (widget->window,
                         0, 0,
                         widget->allocation.width,
                         widget->allocation.height);

  xc = widget->allocation.width/2;
  yc = widget->allocation.height/2;

  /* Draw ticks */

  for (i=0; i<25; i++)
    {
      theta = (i*M_PI/18. - M_PI/6.);
      s = sin(theta);
      c = cos(theta);

      tick_length = (i%6 == 0) ? dial->pointer_width : dial->pointer_width/2;
      
      gdk_draw_line (widget->window,
                     widget->style->fg_gc[widget->state],
                     xc + c*(dial->radius - tick_length),
                     yc - s*(dial->radius - tick_length),
                     xc + c*dial->radius,
                     yc - s*dial->radius);
    }

  /* Draw pointer */

  s = sin(dial->angle);
  c = cos(dial->angle);


  points[0].x = xc + s*dial->pointer_width/2;
  points[0].y = yc + c*dial->pointer_width/2;
  points[1].x = xc + c*dial->radius;
  points[1].y = yc - s*dial->radius;
  points[2].x = xc - s*dial->pointer_width/2;
  points[2].y = yc - c*dial->pointer_width/2;

  gtk_draw_polygon (widget->style,
                    widget->window,
                    GTK_STATE_NORMAL,
                    GTK_SHADOW_OUT,
                    points, 3,
                    TRUE);
  
  return FALSE;
}

Gestione degli eventi

Il resto del codice del widget manipola vari tipi di eventi, e non è differente da quello che può essere trovato in molte applicazione GTK. Due tipi di eventi possono verificarsi: l'utente può clickare sul widget con il mouse e trascinare per muovere il puntatore, o il valore dell'oggetto Adjustmente può cambiare a causa di alcune circostanze esterne.

Quando l'utente clicka sul widget, noi vediamo se la pressione era veramente vicina al puntatore, e se così, memorizziamo il bottone premuto dall'utente con il campo button della struttura del widget, e prendiamo tutti gli eventi del mouse con una chiamata alla funzione gtk_grab_add(). Successivi movimenti del mouse causano il ricalcolo dei valori di controllo (fatto dalla funzione gtk_dial_update_mouse). Dipendentemente dalla politica che abbiamo stabilito, gli eventi ``value_changed'' possono essere generati istantaneamente (GTK_UPDATE_CONTINUOUS), dopo un certo tempo aggiunto con la funzione gtk_timeout_add() (GTK_UPDATE_DELAYED), o solamente quando il bottone del mouse e' rilasciato (GTK_UPDATE_DISCONTINUOUS).

static gint
gtk_dial_button_press (GtkWidget      *widget,
                       GdkEventButton *event)
{
  GtkDial *dial;
  gint dx, dy;
  double s, c;
  double d_parallel;
  double d_perpendicular;

  g_return_val_if_fail (widget != NULL, FALSE);
  g_return_val_if_fail (GTK_IS_DIAL (widget), FALSE);
  g_return_val_if_fail (event != NULL, FALSE);

  dial = GTK_DIAL (widget);

  /* Determina se il bottone premuto era dentro la regione del puntatore:
     lo facciamo calcolando la distanza parallela e 
     perpendicolare dal punto dove il bottone del mouse e' stato premuto
     alla linea passante per il puntatore. */
  
  dx = event->x - widget->allocation.width / 2;
  dy = widget->allocation.height / 2 - event->y;
  
  s = sin(dial->angle);
  c = cos(dial->angle);
  
  d_parallel = s*dy + c*dx;
  d_perpendicular = fabs(s*dx - c*dy);
  
  if (!dial->button &&
      (d_perpendicular < dial->pointer_width/2) &&
      (d_parallel > - dial->pointer_width))
    {
      gtk_grab_add (widget);

      dial->button = event->button;

      gtk_dial_update_mouse (dial, event->x, event->y);
    }

  return FALSE;
}

static gint
gtk_dial_button_release (GtkWidget      *widget,
                          GdkEventButton *event)
{
  GtkDial *dial;

  g_return_val_if_fail (widget != NULL, FALSE);
  g_return_val_if_fail (GTK_IS_DIAL (widget), FALSE);
  g_return_val_if_fail (event != NULL, FALSE);

  dial = GTK_DIAL (widget);

  if (dial->button == event->button)
    {
      gtk_grab_remove (widget);

      dial->button = 0;

      if (dial->policy == GTK_UPDATE_DELAYED)
        gtk_timeout_remove (dial->timer);
      
      if ((dial->policy != GTK_UPDATE_CONTINUOUS) &&
          (dial->old_value != dial->adjustment->value))
        gtk_signal_emit_by_name (GTK_OBJECT (dial->adjustment), "value_changed");
    }

  return FALSE;
}

static gint
gtk_dial_motion_notify (GtkWidget      *widget,
                         GdkEventMotion *event)
{
  GtkDial *dial;
  GdkModifierType mods;
  gint x, y, mask;

  g_return_val_if_fail (widget != NULL, FALSE);
  g_return_val_if_fail (GTK_IS_DIAL (widget), FALSE);
  g_return_val_if_fail (event != NULL, FALSE);

  dial = GTK_DIAL (widget);

  if (dial->button != 0)
    {
      x = event->x;
      y = event->y;

      if (event->is_hint || (event->window != widget->window))
        gdk_window_get_pointer (widget->window, &x, &y, &mods);

      switch (dial->button)
        {
        case 1:
          mask = GDK_BUTTON1_MASK;
          break;
        case 2:
          mask = GDK_BUTTON2_MASK;
          break;
        case 3:
          mask = GDK_BUTTON3_MASK;
          break;
        default:
          mask = 0;
          break;
        }

      if (mods & mask)
        gtk_dial_update_mouse (dial, x,y);
    }

  return FALSE;
}

static gint
gtk_dial_timer (GtkDial *dial)
{
  g_return_val_if_fail (dial != NULL, FALSE);
  g_return_val_if_fail (GTK_IS_DIAL (dial), FALSE);

  if (dial->policy == GTK_UPDATE_DELAYED)
    gtk_signal_emit_by_name (GTK_OBJECT (dial->adjustment), "value_changed");

  return FALSE;
}

static void
gtk_dial_update_mouse (GtkDial *dial, gint x, gint y)
{
  gint xc, yc;
  gfloat old_value;

  g_return_if_fail (dial != NULL);
  g_return_if_fail (GTK_IS_DIAL (dial));

  xc = GTK_WIDGET(dial)->allocation.width / 2;
  yc = GTK_WIDGET(dial)->allocation.height / 2;

  old_value = dial->adjustment->value;
  dial->angle = atan2(yc-y, x-xc);

  if (dial->angle < -M_PI/2.)
    dial->angle += 2*M_PI;

  if (dial->angle < -M_PI/6)
    dial->angle = -M_PI/6;

  if (dial->angle > 7.*M_PI/6.)
    dial->angle = 7.*M_PI/6.;

  dial->adjustment->value = dial->adjustment->lower + (7.*M_PI/6 - dial->angle) *
    (dial->adjustment->upper - dial->adjustment->lower) / (4.*M_PI/3.);

  if (dial->adjustment->value != old_value)
    {
      if (dial->policy == GTK_UPDATE_CONTINUOUS)
        {
          gtk_signal_emit_by_name (GTK_OBJECT (dial->adjustment), "value_changed");
        }
      else
        {
          gtk_widget_draw (GTK_WIDGET(dial), NULL);

          if (dial->policy == GTK_UPDATE_DELAYED)
            {
              if (dial->timer)
                gtk_timeout_remove (dial->timer);

              dial->timer = gtk_timeout_add (SCROLL_DELAY_LENGTH,
                                             (GtkFunction) gtk_dial_timer,
                                             (gpointer) dial);
            }
        }
    }
}

Cambiamenti esterni all'Adjustment sono comunicati al nostro widget dai segnali ``changed'' e ``value_changed''. Il gestore per queste funzioni chiama gtk_dial_update() per validare gli argomenti, calcolare il nuovo angolo del puntatore e ridisegnare il widget (chiamando gtk_widget_draw()).

static void
gtk_dial_update (GtkDial *dial)
{
  gfloat new_value;
  
  g_return_if_fail (dial != NULL);
  g_return_if_fail (GTK_IS_DIAL (dial));

  new_value = dial->adjustment->value;
  
  if (new_value < dial->adjustment->lower)
    new_value = dial->adjustment->lower;

  if (new_value > dial->adjustment->upper)
    new_value = dial->adjustment->upper;

  if (new_value != dial->adjustment->value)
    {
      dial->adjustment->value = new_value;
      gtk_signal_emit_by_name (GTK_OBJECT (dial->adjustment), "value_changed");
    }

  dial->angle = 7.*M_PI/6. - (new_value - dial->adjustment->lower) * 4.*M_PI/3. /
    (dial->adjustment->upper - dial->adjustment->lower);

  gtk_widget_draw (GTK_WIDGET(dial), NULL);
}

static void
gtk_dial_adjustment_changed (GtkAdjustment *adjustment,
                              gpointer       data)
{
  GtkDial *dial;

  g_return_if_fail (adjustment != NULL);
  g_return_if_fail (data != NULL);

  dial = GTK_DIAL (data);

  if ((dial->old_value != adjustment->value) ||
      (dial->old_lower != adjustment->lower) ||
      (dial->old_upper != adjustment->upper))
    {
      gtk_dial_update (dial);

      dial->old_value = adjustment->value;
      dial->old_lower = adjustment->lower;
      dial->old_upper = adjustment->upper;
    }
}

static void
gtk_dial_adjustment_value_changed (GtkAdjustment *adjustment,
                                    gpointer       data)
{
  GtkDial *dial;

  g_return_if_fail (adjustment != NULL);
  g_return_if_fail (data != NULL);

  dial = GTK_DIAL (data);

  if (dial->old_value != adjustment->value)
    {
      gtk_dial_update (dial);

      dial->old_value = adjustment->value;
    }
}

Possibili Miglioramenti

Il widget Dial, da come l'abbiamo costruito, è lungo circa 670 linee di codice C. Anche se questo potrebbe sembrare un po' troppo, abbiamo realmente fatto un bel po' con quel tanto di codice, specialmente considerando che molta della lunghezza è costituita da file header e commmenti. Comunque ci sono alcuni miglioramenti che potrebbero essere fatti a questo widget:

  • Se tu provate questo widget, troverete che ci sono alcuni lampeggiamenti quando il puntatore viene trascinato in giro. Questo perchè l'intero widget è cancellato ogni volta che il puntatore viene mosso, prima di essere ridisegnato. Spesso, il modo migliore per gestire questo tipo di problema è il disegnare il tutto su una pixmap non visibile, poi copiare il risultato finale sullo schermo in una passata sola (il widget ProgressBar viene disegnato in questo modo).
  • L'utente potrebbe essere abilitato ad usare le frecce su e giu per incrementare e diminuire il valore.
  • Potrebbe essere carino se il widget avesse i bottoni per incrementare e decrementare il valore di step. Anche se potrebbe essere possibile usare dei widget Bottone incorporati per questo, possiamo anche far sì che il bottone sia auto-ripentente quando premuto, come le frecce in una barra di scorrimento. Molto del codice per implementare questo tipo di comportamento può essere trovato nel widget GtkRange.
  • il widget Dial potrebbe essere fatto/creato dentro un widget contenitore con un singolo widget figlio posizionato all'inizio tra i 2 bottoni menzionati prima. L'utente potrebbe poi aggiungere o una etichetta o un widget ``entry'' per mostrare il valore corrente del dial.

19.5 Impararne di più

Fin qui abbiamo esposto solo una piccola parte di tutto quello che serve per creare un widget. Se volete davvero scrivere un vostro widget, la miglior risorsa di esempi è lo stesso codice sorgente GTK. Chiedete a voi stessi alcune cose su come deve essere il widget che volete scrivere: è un widget contenitore? dovrà avere una propria finestra? è una modifica di un widget precedente? Trovate poi un widget simile e iniziate a fargli delle modifiche. Buone Fortuna.


Avanti Indietro Indice