GTK+ 2.0 Tutorial | ||
---|---|---|
<<< Previous | Writing Your Own Widgets | Next >>> |
In this section, we'll learn more about how widgets display themselves on the screen and interact with events. As an example of this, we'll create an analog dial widget with a pointer that the user can drag to set the value.
There are several steps that are involved in displaying on the screen. After the widget is created with a call to WIDGETNAME_new(), several more functions are needed:
WIDGETNAME_realize() is responsible for creating an X window for the widget if it has one.
WIDGETNAME_map() is invoked after the user calls gtk_widget_show(). It is responsible for making sure the widget is actually drawn on the screen (mapped). For a container class, it must also make calls to map()> functions of any child widgets.
WIDGETNAME_draw() is invoked when gtk_widget_draw() is called for the widget or one of its ancestors. It makes the actual calls to the drawing functions to draw the widget on the screen. For container widgets, this function must make calls to gtk_widget_draw() for its child widgets.
WIDGETNAME_expose() is a handler for expose events for the widget. It makes the necessary calls to the drawing functions to draw the exposed portion on the screen. For container widgets, this function must generate expose events for its child widgets which don't have their own windows. (If they have their own windows, then X will generate the necessary expose events.)
You might notice that the last two functions are quite similar - each is responsible for drawing the widget on the screen. In fact many types of widgets don't really care about the difference between the two. The default draw() function in the widget class simply generates a synthetic expose event for the redrawn area. However, some types of widgets can save work by distinguishing between the two functions. For instance, if a widget has multiple X windows, then since expose events identify the exposed window, it can redraw only the affected window, which is not possible for calls to draw().
Container widgets, even if they don't care about the difference for themselves, can't simply use the default draw() function because their child widgets might care about the difference. However, it would be wasteful to duplicate the drawing code between the two functions. The convention is that such widgets have a function called WIDGETNAME_paint() that does the actual work of drawing the widget, that is then called by the draw() and expose() functions.
In our example approach, since the dial widget is not a container widget, and only has a single window, we can take the simplest approach and use the default draw() function and only implement an expose() function.
Just as all land animals are just variants on the first amphibian that crawled up out of the mud, GTK widgets tend to start off as variants of some other, previously written widget. Thus, although this section is entitled "Creating a Widget from Scratch", the Dial widget really began with the source code for the Range widget. This was picked as a starting point because it would be nice if our Dial had the same interface as the Scale widgets which are just specialized descendants of the Range widget. So, though the source code is presented below in finished form, it should not be implied that it was written, ab initio in this fashion. Also, if you aren't yet familiar with how scale widgets work from the application writer's point of view, it would be a good idea to look them over before continuing.
Quite a bit of our widget should look pretty familiar from the Tictactoe widget. First, we have a header file:
/* 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; /* update policy (GTK_UPDATE_[CONTINUOUS/DELAYED/DISCONTINUOUS]) */ guint policy : 2; /* Button currently pressed or 0 if none */ guint8 button; /* Dimensions of dial components */ gint radius; gint pointer_width; /* ID of update timer, or 0 if none */ guint32 timer; /* Current angle */ gfloat angle; /* Old values from adjustment stored so we know when something changes */ gfloat old_value; gfloat old_lower; gfloat old_upper; /* The adjustment object that stores the data for this dial */ GtkAdjustment *adjustment; }; struct _GtkDialClass { GtkWidgetClass parent_class; }; GtkWidget* gtk_dial_new (GtkAdjustment *adjustment); GtkType 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__ */ |
Since there is quite a bit more going on in this widget than the last one, we have more fields in the data structure, but otherwise things are pretty similar.
Next, after including header files and declaring a few constants, we have some functions to provide information about the widget and initialize it:
#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 /* Forward declarations */ [ omitted to save space ] /* Local data */ static GtkWidgetClass *parent_class = NULL; GtkType gtk_dial_get_type () { static GtkType dial_type = 0; if (!dial_type) { static const GtkTypeInfo dial_info = { "GtkDial", sizeof (GtkDial), sizeof (GtkDialClass), (GtkClassInitFunc) gtk_dial_class_init, (GtkObjectInitFunc) gtk_dial_init, /* reserved_1 */ NULL, /* reserved_1 */ NULL, (GtkClassInitFunc) NULL }; dial_type = gtk_type_unique (GTK_TYPE_WIDGET, &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); } |
Note that this init() function does less than for the Tictactoe widget, since this is not a composite widget, and the new() function does more, since it now has an argument. Also, note that when we store a pointer to the Adjustment object, we increment its reference count, (and correspondingly decrement it when we no longer use it) so that GTK can keep track of when it can be safely destroyed.
Also, there are a few function to manipulate the widget's options:
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); } |
Now we come to some new types of functions. First, we have a function that does the work of creating the X window. Notice that a mask is passed to the function gdk_window_new() which specifies which fields of the GdkWindowAttr structure actually have data in them (the remaining fields will be given default values). Also worth noting is the way the event mask of the widget is created. We call gtk_widget_get_events() to retrieve the event mask that the user has specified for this widget (with gtk_widget_set_events()), and add the events that we are interested in ourselves.
After creating the window, we set its style and background, and put a pointer to the widget in the user data field of the GdkWindow. This last step allows GTK to dispatch events for this window to the correct widget.
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); } |
Before the first time that the window containing a widget is displayed, and whenever the layout of the window changes, GTK asks each child widget for its desired size. This request is handled by the function gtk_dial_size_request(). Since our widget isn't a container widget, and has no real constraints on its size, we just return a reasonable default value.
static void gtk_dial_size_request (GtkWidget *widget, GtkRequisition *requisition) { requisition->width = DIAL_DEFAULT_SIZE; requisition->height = DIAL_DEFAULT_SIZE; } |
After all the widgets have requested an ideal size, the layout of the window is computed and each child widget is notified of its actual size. Usually, this will be at least as large as the requested size, but if for instance the user has resized the window, it may occasionally be smaller than the requested size. The size notification is handled by the function gtk_dial_size_allocate(). Notice that as well as computing the sizes of some component pieces for future use, this routine also does the grunt work of moving the widget's X window into the new position and size.
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; } } |
As mentioned above, all the drawing of this widget is done in the handler for expose events. There's not much to remark on here except the use of the function gtk_draw_polygon to draw the pointer with three dimensional shading according to the colors stored in the widget's style.
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; } |
The rest of the widget's code handles various types of events, and isn't too different from what would be found in many GTK applications. Two types of events can occur - either the user can click on the widget with the mouse and drag to move the pointer, or the value of the Adjustment object can change due to some external circumstance.
When the user clicks on the widget, we check to see if the click was appropriately near the pointer, and if so, store the button that the user clicked with in the button field of the widget structure, and grab all mouse events with a call to gtk_grab_add(). Subsequent motion of the mouse causes the value of the control to be recomputed (by the function gtk_dial_update_mouse). Depending on the policy that has been set, "value_changed" events are either generated instantly (GTK_UPDATE_CONTINUOUS), after a delay in a timer added with gtk_timeout_add() (GTK_UPDATE_DELAYED), or only when the button is released (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); /* Determine if button press was within pointer region - we do this by computing the parallel and perpendicular distance of the point where the mouse was pressed from the line passing through the pointer */ 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); } } } } |
Changes to the Adjustment by external means are communicated to our widget by the "changed" and "value_changed" signals. The handlers for these functions call gtk_dial_update() to validate the arguments, compute the new pointer angle, and redraw the widget (by calling 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; } } |
The Dial widget as we've described it so far runs about 670 lines of code. Although that might sound like a fair bit, we've really accomplished quite a bit with that much code, especially since much of that length is headers and boilerplate. However, there are quite a few more enhancements that could be made to this widget:
If you try this widget out, you'll find that there is some flashing as the pointer is dragged around. This is because the entire widget is erased every time the pointer is moved before being redrawn. Often, the best way to handle this problem is to draw to an offscreen pixmap, then copy the final results onto the screen in one step. (The ProgressBar widget draws itself in this fashion.)
The user should be able to use the up and down arrow keys to increase and decrease the value.
It would be nice if the widget had buttons to increase and decrease the value in small or large steps. Although it would be possible to use embedded Button widgets for this, we would also like the buttons to auto-repeat when held down, as the arrows on a scrollbar do. Most of the code to implement this type of behavior can be found in the Range widget.
The Dial widget could be made into a container widget with a single child widget positioned at the bottom between the buttons mentioned above. The user could then add their choice of a label or entry widget to display the current value of the dial.
<<< Previous | Home | Next >>> |
Creating a Composite widget | Up | Learning More |