Página siguiente Página anterior Índice general

24. Escribiendo sus propios widgets

24.1 Visión general

Aunque la distribución de GTK viene con muchos tipos de widgets que debería cubrir todas la mayoría de las necesidades básicas, puede que haya llegado el momento en que necesite crear su propio widget. Debido a que GTK utiliza mucho la herencia de widgets, y si ya hay un widget que se acerque lo suficiente a lo que quiere, tal vez pueda hacer un nuevo widget con tan solo unas cuantas líneas de código. Pero antes de empezar a trabajar en un nuevo widget, asegúrese primero de que no hay nadie que ya haya hecho otro parecido. Así evitará la duplicación de esfuerzo y mantendrá el número de widgets GTK en su valor mínimo, lo que ayudará a que el código y la interfaz de las diferentes aplicaciones sea consistente. Por otra parte, cuando haya acabado su widget, anúncielo al mundo entreo para que todo el mundo se pueda beneficiar. Probablemente el mejor lugar para hacerlo sea la gtk-list.

Las fuentes completas de los widgets de ejemplo están disponibles en el mismo lugar en el que consiguió este tutorial, o en:

http://www.gtk.org/~otaylor/gtk/tutorial/

24.2 La anatomía de un widget

Para crear un nuevo widget, es importante conocer como funcionan los objetos de GTK. Esta sección es sólo un breve resumen. Ver la documentación a la que se hace referencia para obtener más detalles.

Los widgets GTK están implementados siguiendo una orientación a objetos. Sin embargo, están implementados en C estándar. De esta forma se mejora enormemente la portabilidad y la estabilidad con respecto a la actual generación de compiladores C++; sin embargo, con todo esto no queremos decir que el creador de widgets tenga que prestar atención a ninguno de los detalles de implementación. La información que es común a todos los widgets de una clase de widgets (p.e., a todos los widgets botón) se almacena en la estructura de clase. Sólo hay una copia de ésta en la que se almacena información sobre las señales de la clase (que actuan como funciones virtuales en C). Para permitir la herencia, el primer campo en la estructura de la clase debe ser una copia de la estructura de la clase del padre. La declaración de la estructura de la clase de GtkButton debe ser algo así:

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);
};

Cuando un botón se trata como un contenedor (por ejemplo, cuando se le cambia el tamaño), su estructura de clase puede convertirse a GtkContainerClass, y los campos relevantes se utilizarán para manejar las señales.

También hay una estructura que se crea para cada widget. Esta estructura tiene campos para almacenar la información que es diferente para cada copia del widget. Nosotros llamaremos a esta estructura la estructura objeto. Para la clase botón, es así:

struct _GtkButton
{
  GtkContainer container;

  GtkWidget *child;

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

Observe que, como en la estructura de clase, el primer campo es la estructura objeto de la clase padre, por lo que esta estructura puede convertirse en la estructura de la clase del objeto padre cuando haga falta.

24.3 Creando un widget compuesto

Introducción

Un tipo de widget que puede interesarnos es uno que sea un mero agregado de otros widgets GTK. Este tipo de widget no hace nada que no pueda hacerse sin la necesidad de crear un nuevo widget, pero proporciona una forma conveniente de empaquetar los elementos del interfaz de usuario para su reutilización. Los widgets FileSelection y ColorSelection incluidos en la distribución estándar son ejemplos de este tipo de widgets.

El widget ejemplo que hemos creado en esta sección es el widget Tictactoe, una matriz de 3x3 de botones de selección que lanza una señal cuando están deseleccionados tres botones en una misma fila, columna, o diagonal.

Escogiendo una clase padre

Normalmente la clase padre para un widget compuesto es la clase contenedor que tenga todos los elementos del widget compuesto. Por ejemplo, la clase padre del widget FileSelection es la clase Dialog. Ya que nuestros botones se ordenarán en una tabla, parece natural hacer que nuestra clase padre sea la clase GtkTable. Desafortunadamente, esto no funcionaría. La creación de un widget se divide en dos funciones - una función NOMBREWIDGET_new() que utilizará el usuario, y una función NOMBREWIDGET_init() que hará el trabajo básico de inicializar el widget que es independiente de los argumentos que se le pasen a la función _new(). Los widgets derivados sólo llaman a la función _init de su widget padre. Pero esta división del trabajo no funciona bien con las tablas, que necesitan saber en el momento de su creación el número de filas y de columnas que deben tener. A menos que queramos duplicar la mayor parte de lo hecho en gtk_table_new() en nuestro widget Tictactoe, haremos mejor si evitamos derivar de GtkTable. Por esta razón, derivaremos de GtkVBox, y meteremos nuestra tabla dentro de la caja vertical.

El fichero de cabecera

Cada clase widget tiene un fichero de cabecera que declara el objeto y las estructuras de clase para ese widget, así como las funciones públicas. Un par de características que merecen dejarse aparte. Para evitar la duplicación de definiciones, meteremos el fichero de cabecera al completo entre:

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

Y para que los programas en C++ incluyan sin problemas el fichero de cabecera, pondremos:

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

Con las funciones y las estructuras, declararemos tres macros estándar en nuestro fichero de cabecera, TICTACTOE(obj), TICTACTOE_CLASS(class), y IS_TICTACTOE(obj), que, convierten, respectivamente, un puntero en un puntero al objeto o a la estructura de la clase, y comprueba si un objeto es un widget Tictactoe.

Aquí está el fichero de cabecera al 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 función _get_type().

Ahora continuaremos con la implementación de nuestro widget. Una función del núcleo de todo widget es NOMBREWIDGET_get_type(). Cuando se llame a esta función por vez primera, le informará a GTK sobre la clase del widget, y devolverá un ID que identificará unívocamente la clase widget. En las llamadas siguientes, lo único que hará será devolver el ID.

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 estructura GtkTypeInfo tiene la definición siguiente:

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;
};

Los utilidad de cada campo de esta estructura se explica por su propio nombre. Ignoraremos por ahora los campos arg_set_func y arg_get_func: son importantes, pero todavía es raro utilizarlos, su papel es permitir que las opciones de los wdigets puedan establecerse correctamente mediante lenguajes interpretados. Una vez que GTK tiene una copia de esta estructura correctamente rellenada, sabrá como crear objetos de un tipo particular de widget.

La función _class_init()

La función NOMBREWIDGET_class_init() inicializa los campos de la estructura clase del widget, y establece las señales de la clase. Para nuestro widget Tictactoe será una cosa así:


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;
}

Nuestro widget sólo tiene una señal, la señal tictactoe que se invoca cuando una fila, columna, o diagonal se rellena completamente. No todos los widgets compuestos necesitan señales, por lo que si está leyendo esto por primera vez, puede que sea mejor que pase a la sección siguiente, ya que las cosas van a complicarse un poco.

La función:

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

crea una nueva señal. Los parámetros son:

Cuando se especifican los tipos, se utilizará la enumeración 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,

  /* it'd be great if the next two could be removed eventually */
  GTK_TYPE_SIGNAL,
  GTK_TYPE_C_CALLBACK,

  GTK_TYPE_OBJECT

} GtkFundamentalType;

gtk_signal_new() devuelve un identificador entero único para la señal, que almacenamos en el vector tictactoe_signals, que indexaremos utilizando una enumeración. (Convencionalmente, los elementos de la enumeración son el nombre de la señal, en mayúsculas, pero aquí tendríamos un conflicto con la macro TICTACTOE(), por lo que lo llamaremos TICTACTOE_SIGNAL.

Después de crear nuestras señales, necesitamos llamar a GTK para asociarlas con la clase Tictactoe. Hacemos esto llamando a gtk_object_class_add_signals(). Entonces haremos que el puntero que apunta al manejador por defecto para la señal `tictactoe' sea NULL, indicando que no hay ninguna acción por defecto.

La función _init().

Cada clase widget también necesita una función para inicializar la estructura del objeto. Normalmente, esta función tiene el limitado rol de poner los distintos campos de la estructura a su valor por defecto. Sin embargo para los widgets de composición, esta función también crea los distintos widgets componentes.

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]);
      }
}

Y el resto...

Hay una función más que cada widget (excepto los widget muy básicos como GtkBin que no pueden crear objetos) tiene que tener - la función que el usuario llama para crear un objeto de ese tipo. Normalmente se llama NOMBREWIDGET_new(). En algunos widgets, que no es el caso del widget Tictactoe, esta función toma argumentos, y hace alguna inicialización en función de estos. Las otras dos funciones son específicas al widget Tictactoe.

tictactoe_clear() es una función pública que reinicia todos los botones en el widget a la posición alta. Observe la utilización de gtk_signal_handler_block_by_data() para hacer que no se ejecute nuestro manejador de señal innecesariamente por cambios en los botones.

tictactoe_toggle() es el manejador de señal que se invoca cuando el usuario pulsa un botón. Hace una comprobación para ver si hay alguna combinación ganadora, y si la hay, emite la señal ``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;
        }
    }
}

Y finalmente, un programa ejemplo que utiliza nuestro widget Tictactoe:

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

/* Invocado cuando se completa una fila, columna o diagonal */
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);

  /* Create a new Tictactoe widget */
  ttt = tictactoe_new ();
  gtk_container_add (GTK_CONTAINER (window), ttt);
  gtk_widget_show (ttt);

  /* And attach to its "tictactoe" signal */
  gtk_signal_connect (GTK_OBJECT (ttt), "tictactoe",
                      GTK_SIGNAL_FUNC (win), NULL);

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

24.4 Creando un widget desde cero.

Introducción

En esta sección, averiguaremos como se dibujan los widgets a sí mismos en pantalla y como interactuan con los eventos. Como ejemplo, crearemos un marcador analógico con un puntero que el usuario podrá arrastrar para hacer que el marcador tenga un valor dado.

Mostrando un widget en la pantalla

Hay varios pasos que están involucrados en el dibujado en pantalla. Después de que el widget se cree con una llamada a NOMBREWIDGET_new(), se necesitarán muchas más funciones:

Las últimas dos funciones son bastante similares - ambas son responsables de dibujar el widget en pantalla. De hecho en muchos widgets realmente no importa la diferencia que hay entre ambas funciones. La función draw() que hay por defecto en la clase widget simplemente genera un evento expose artificial de la zona a redibujar. Sin embargo, algunos tipos de widgets puede ahorrarse trabajo distinguiendo entre las dos funciones. Por ejemplo, si un widget tiene varias ventanas X, entonces, como los eventos expose identifican a la ventana expuesta, podrán redibujar sólo la ventana afectada, lo que no es posible con llamadas a draw().

Los widgets contenedores, aunque no utilicen la diferecia existente entre las dos funciones por sí mismos, no pueden utilizar simplemente las funciones draw() que hay por defecto ya que sus widgets hijos puede que tengan que utilizar la diferencia. Sin embargo, sería un derroche duplicar el código de dibujado entre las dos funciones. Lo normal es que cada widget tenga una función llamada NOMBREWIDGET_paint() que haga el trabajo de dibujar el widget, ésta función será a la que se llame por las funciones draw() y expose().

En nuestro ejemplo, como el widget Dial no es un widget contenedor, y sólo tiene una ventana, podemos tomar el camino más corto, utilizar la función draw() por defecto y sólo implementar la función expose().

Los orígenes del widget Dial

Así como todos los animales terrestes son variaciones del primer anfíbio que salió del barro, los widgets Gtk tienden a nacer como variaciones de algún otro widget escrito previamente. Por tanto, aunque esta sección se titule `Creando un widget de la nada', el widget Dial empieza realmente con el código fuente del widget Range. He tomado éste como punto de arranque porque sería bonito que nuestro dial tuviese la misma interfaz que los widgets Scale, que son sólo una especialización del widget Range. Por tanto, aunque el código fuente se presente más adelante en su forma final, no implica que fuese escrito de esta forma deus ex machina. Si todavía no está familiarizado, desde el punto de vista del escritor de aplicaciones, con la forma de funcionar de los widgets Scale, sería una buena idea echarles un vistazo antes de continuar.

Los comienzos

Nuestro widget tiene un aspecto algo parecido al del widget Tictactoe. Primero, tenemos un fichero de cabecera:

/* 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;

  /* política de actualización
   * (GTK_UPDATE_[CONTINUOUS/DELAYED/DISCONTINUOUS]) */
  guint policy : 2;

  /* Botón actualmente presionado o 0 si no hay ninguno */
  guint8 button;

  /* Dimensión de los componendes del dial */
  gint radius;
  gint pointer_width;

  /* ID del temporizador de actualización, o 0 si no hay ninguno */
  guint32 timer;

  /* ángulo actual */
  gfloat angle;

  /* Viejos valores almacenados del adjustment, para que así no
   * tengamos que saber cuando cambia algo */
  gfloat old_value;
  gfloat old_lower;
  gfloat old_upper;

  /* El objeto adjustment que almacena los datos para este 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__ */

Como vamos a ir con este widget un poco más lejos que con el último que creamos, ahora tenemos unos cuantos campos más en la estructura de datos, pero el resto de las cosas son muy parecidas.

Ahora, después de incluir los ficheros de cabecera, y declarar unas cuantas constantes, tenemos algunas funciones que proporcionan información sobre el widget y lo inicializan:

#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

/* Declaraciones de funciones */

[ omitido para salvar espacio ]

/* datos locales */

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);
}

Observe que ésta función init() hace menos cosas de las que hacía la función init() que utilizamos con el widget Tictactoe, ya que éste no es un widget compuesto, y la función new() hace más cosas, ya que ahora admite un argumento. Observe también que cuando almacenamos un puntero en un objeto Adjustment, incrementamos su contador interno, (y lo decrementamos cuando ya no lo utilizamos) por lo que GTK puede saber cuando se puede destruir sin que se produzcan problemas.

Aquí tenemos unas cuantas funciones para manipular las opciones 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()

Ahora vienen algunas funciones nuevas. Primero, tenemos una función que hace el trabajo de crear la ventana X. A la función se le pasará una máscara gdk_window_new() que especifica que campos de la estructura GdkWindowAttr tienen datos (los campos restantes tendrán los valores por defecto). También es bueno fijarse en la forma en que se crea la máscara de eventos. Llamamos a gtk_widget_get_events() para recuperar la máscara de eventos que el usuario ha especificado para su widget (con gtk_widget_set_events()), y añadir nosotros mismos los eventos en los que estemos interesados.

Después de crear la ventana, decidiremos su estilo y su fondo, y pondremos un puntero al widget en el campo de datos del usuario de la GdkWindow. Este último paso le permite a GTK despachar los eventos que hayan para esta ventana hacia el widget correcto.

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);
}

Negociación del tamaño

Antes de que se muestre por primera vez la ventana conteniendo un widget, y cuando quiera que la capa de la ventana cambie, GTK le preguntara a cada widget hijo por su tamaño deseado. Esta petición se controla mediante la función gtk_dial_size_request(). Como nuestro widget no es un widget contenedor, y no tiene ninguna limitación en su tamaño, nos contentaremos con devolver un valor por defecto.

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

Después de que todos los widgets hayan pedido su tamaño ideal, se calculará la ventana y cada widget hijo será informado de su tamaño actual. Normalmente, éste será al menos tan grande como el pedido, pero si por ejemplo, el usuario ha redimensionado la ventana, entonces puede que el tamaño que se le de al widget sea menor que el que pidió. La notificación del tamaño se maneja mediante la función gtk_dial_size_allocate(). Fíjese que esta función calcula los tamaños de los diferentes elementos que componen la ventana para su uso futuro, así como todo el trabajo sucio que poner los widgets de la ventana X en la nueva posición y con el nuevo tamaño.

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()

Como se mencionó arriba, todo el dibujado de este widget se hace en el manejador de los eventos expose. No hay mucho destacable aquí, excepto la utilización de la función gtk_draw_polygon para dibujar el puntero con un degradado tridimensional de acuerdo con los colores almacenados en el estilo del widget.

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;

  /* Dibujar las rayitas */

  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);
    }

  /* Dibujar el puntero */

  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;
}

Manejo de eventos

El resto del código del widget controla varios tipos de eventos, y no es muy diferente del que podemos encontrar en muchas aplicaciones GTK. Pueden ocurrir dos tipos de eventos - el usuario puede pulsar en el widget con el ratón y arrastrar para mover el puntero, o el valor del objeto Adjustement puede cambiar debido a alguna circunstancia externa.

Cuando el usuario pulsa en el widget, haremos una comprobación para ver si la pulsación se hizo lo suficientemente cerca del puntero, y si así fue, almacenamos el botón que pulsó el usuario en en el campo button de la estructura del widget, y grabamos todos los eventos del ratón con una llamada a gtk_grab_add(). El movimiento del ratón hará que se recalcule el valor del control (mediante la función gtk_dial_update_mouse). Dependiendo de la política que sigamos, o bien se generarán instantáneamente los eventos value_changed (GTK_UPDATE_CONTINUOUS), o bien después de una espera del temporizador establecido mediante gtk_timeout_add() (GTK_UPDATE_DELAYED), o bien sólo cuando se levante el botón (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);

  /* Determinar si la pulsación del botón fue dentro de la región del
     puntero - esto lo hacemos calculando la distancia x e y del punto
     donde se pulsó el botón ratón de la línea que se ha pasado mediante el
     puntero */
  
  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);
            }
        }
    }
}

Cambios en el Adjustment por motivos externos significa que se le comunicarán a nuestro widget mediante las señales changed y value_changed. Los manejadores de estas funciones llaman a gtk_dial_update() para comprobar los argumentos, calcular el nuevo ángulo del puntero, y redibujar el widget (llamando a 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;
    }
}

Posibles mejoras

El widget Dial tal y como lo hemos descrito tiene unas 670 líneas de código. Aunque pueda parecer un poco exagerado, todavía no hemos escrito demasiado código, ya que la mayoría de las líneas son de ficheros de cabecera y de adornos. Todavía se le pueden hacer algunas mejoras a este widget:

24.5 Aprendiendo más

Sólo se han descrito una pequeña parte de los muchos detalles involucrados en la creación de widgets, la mejor fuente de ejemplos es el código mismo de GTK. Hágase algunas preguntas hacerca del widget que desea crear: ¿es un widget contenedor? ¿Debe tener su propia ventana? ¿Es una modificación de un widget existente? En ese momento busque un widget similar, y comience a hacer los cambios. ¡Buena suerte!


Página siguiente Página anterior Índice general