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/
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.
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.
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.
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__ */
_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.
_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:
name
: El nombre de la señal.run_type
: Si el manejador por defecto se ejecuta antes o
despues del manejador de usuario. Normalmente debe ser
GTK_RUN_FIRST
, o GTK_RUN_LAST
, aunque hay otras
posibilidades.object_type
: El ID del objeto al que se le aplica esta
señal. (También se aplicará a los descendientes de los objetos)function_offset
: El desplazamiento en la estructura de la
clase de un puntero al manejador por defecto.marshaller
: Una función que se utiliza para invocar al
manejador de señal. Para los manejadores de señal que no tengan más
argumentos que el objeto que emitió la señal podemos utilizar la
función marshaller por defecto gtk_signal_default_marshaller
.return_val
: El tipo del valor devuelto.nparams
: El número de parámetros del manejador de señal
(distintos de los dos por defecto que hemos mencionado arriba)....
: Los tipos de los parámetros.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.
_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]);
}
}
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;
}
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.
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:
NOMBREWIDGET_realize()
es la responsable de crear una
ventana X para el widget, si tiene alguna.NOMBREWIDGET_map()
se invoca después de las llamadas del
usuario
gtk_widget_show()
. Es la responsable de asegurarse de que el
widget está dibujado (mapeado) en la pantalla. Para una
clase contenedor, también deberá ocuparse de llamar a las funciones
map()
de cada widget hijo.NOMBREWIDGET_draw()
se invoca cuando se llama a
gtk_widget_draw()
desde el widget de uno de sus
antepasados. Hace las llamadas necesarias a las funciones de dibujo
para dibujar el widget en la pantalla. Para los widgets
contenedores, esta función debe llamar a las gtk_widget_draw
de
sus widgets hijos.NOMBREWIDGET_expose()
es un manejador de los eventos
expose
del widget. Hace las llamadas necesarias a las
funciones de dibujo para dibujar la parte expuesta en la
pantalla. Para los widgets contenedores, esta función debe
generar los eventos expose
de sus widgets hijos que no
tengan su propia ventana. (Si tuviesen su propia ventana, X generaría
los eventos expose
necesarios)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()
.
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.
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);
}
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;
}
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;
}
}
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:
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!