Writing a Widget Using Cairo and GTK+2.8, Part 2
Материал из Wiki.crossplatform.ru
In this article Davyd Madeley continues his tutorial on writing a clock widget using GTK and Cairo.
Thinking back to last issue, we used Cairo to build the face of a clock as part of a GtkWidget we called EggClockFace. We covered the basics of writing a GObject and drawing in the expose hander with Cairo, but what about making the clock run?
[править] Step 3. Making it Tick
Making the clock run is as simple as starting a timer that calls a callback. However, we might also want to be able to set a different time on our clock, so we’ll store the time for the clock inside the widget. We don’t want to let people change the time directly, as then we won’t know that the time has been changed. Instead, we want them to have to use the public API we provide to change the time on the clock. In object-orientation speak, we want to make the time variable private.
We can add a structure of private data to the top of our C file. We add it to the C file rather than a header since it is private to this class (which is contained within this C file) and we don’t want to clutter our namespace with it (or let it be accessed from outside our class).
Add this to the top of your clock.c:
typedef struct _EggClockFacePrivate EggClockFacePrivate; struct _EggClockFacePrivate { struct tm time; /* the time on the clock face */ };
We’ll also want to add a macro for easy access:
#define EGG_CLOCK_FACE_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), EGG_TYPE_CLOCK_FACE, gClockFacePrivate))
Finally, register this private struct with the class at the end of egg_clock_face_class_init():
g_type_class_add_private (obj_class, sizeof (EggClockFacePrivate));
Doing this means that we can access our private variables from any function in this C file, as long as we’re passed our EggClockFace object. We get our private struct like this:
static void egg_clock_face_example_function (EggClockFace *clock) { EggClockFacePrivate *priv; priv = EGG_CLOCK_FACE_GET_PRIVATE (clock); }
You can then access the member variables as you might expect, for example:
localtime_r (&timet, &priv->time);
We can put this all together into a function we’ve called egg_clock_face_update() which will update the clock with the new time:
static gboolean egg_clock_face_update (gpointer data) { EggClockFace *clock; EggClockFacePrivate *priv; time_t timet; clock = EGG_CLOCK_FACE (data); priv = EGG_CLOCK_FACE_GET_PRIVATE (clock); /* update the time */ time (&timet); localtime_r (&timet, &priv->time); egg_clock_face_redraw_canvas (clock); return TRUE; /* keep running this event */ }
Notice that this function returns a boolean value (true). Functions passed as timeout events return a boolean value. If the value is true, the event will be run again; if the value is false it will not. There is also a function that we haven’t defined yet, egg_clock_face_redraw_canvas(). This function will redraw the canvas for us. From the manual page for GtkDrawingArea (our parent class), we are told to use gdk_window_invalidate_rect(), to reexpose the canvas (and cause it to redraw). Again from the manual, we can see that this is a wrapper around gdk_window_invalidate_region() and that in order to make our event happen now, we should also call gdk_window_process_all_updates(). Our redraw function looks like this:
static void egg_clock_face_redraw_canvas (EggClockFace *clock) { GtkWidget *widget; GdkRegion *region; widget = GTK_WIDGET (clock); if (!widget->window) return; region = gdk_drawable_get_clip_region (widget->window); /* redraw the cairo canvas completely by exposing it */ gdk_window_invalidate_region (widget->window, region, TRUE); gdk_window_process_updates (widget->window, TRUE); gdk_region_destroy (region); }
Drawing the hands requires us to think about a little geometry. For the hour hand, the hand is rotated around 30° for each hour and then a ?° more per minute.
So to draw the hour hand, we might do something like:
cairo_save (cr); cairo_set_line_width (cr, 2.5 * cairo_get_line_width (cr)); cairo_move_to (cr, x, y); cairo_line_to (cr, x + radius / 2 * sin (M_PI / 6 * hours + M_PI / 360 * minutes), y + radius / 2 * -cos (M_PI / 6 * hours + M_PI / 360 * minutes)); cairo_stroke (cr); cairo_restore (cr);
The minute hand and the seconds hand each rotate 6° per minute/second. The minute hand is easily implemented as:
cairo_move_to (cr, x, y); cairo_line_to (cr, x + radius * 0.75 * sin (M_PI / 30 * minutes), y + radius * 0.75 * -cos (M_PI / 30 * minutes)); cairo_stroke (cr);
Finally, we need to set it running, in egg_clock_face_init() we will add our timeout function:
static void egg_clock_face_init (EggClockFace *clock) { egg_clock_face_update (clock); /* update the clock once a second */ g_timeout_add (1000, egg_clock_face_update, clock); }
We’re left with clock-ex4.c (get clock.h and main.c if you need them), which you can compile with:
gcc -o clock `pkg-config --cflags --libs gtk+-2.0` clock-ex4.c main.c
and should look like this:
Extra: Making the Picture Tick The animated GIF of the clock ticking was done with a tool called byzanz. I simply recorded 60 seconds of the clock. In order to find out the window location for byzanz-record, I added this to my main.c after gtk_widget_show_all():
{ GdkRectangle rect; gdk_window_get_frame_extents (window->window, &rect); g_print ("-x %i -y %i -w %i -h %in", rect.x, rect.y, rect.width, rect.height); }
This printed settings that I could paste onto my other command line:
$ ./byzanz-record -d 60 $GEOMETRY -l clock.gif
[править] Step 4: Emitting Signals
So far we’ve written a GObject with opaque property storage and we’ve used Cairo to draw our clock face. However the GTK+ widgets we commonly interact with also offer public APIs and emit signals to notify us when certain events take place. We will add a signal to say when someone is dragging the minute hand around.
Firstly we need to decide on what our signal is going to send and add this to our clock.h. We will implement a “time-changed” signal that along with the object also gives the time in hours and minutes that the clock has now been set to. If we were connecting this signal, our callback would look something like this:
static void time_changed_cb (EggClockFace *clock, int hours, int minutes, gpointer user_data) { ... }
In clock.h we would add this to _EggClockFaceClass:
void (* time_changed) (EggClockFace *clock, int hours, int minutes);
We need to do several things in clock.c, firstly we need to define enumerated values for all of our signals:
enum { TIME_CHANGED, LAST_SIGNAL };
And we then need to define an array of unsigned ints to store their signal IDs in:
static guint egg_clock_face_signals[LAST_SIGNAL] = { 0 };
Finally we need to register our signal in egg_clock_face_class_init():
egg_clock_face_class_signals[TIME_CHANGED] = g_signal_new ( "time-changed", G_OBJECT_CLASS_TYPE (obj_class), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (EggClockFaceClass, time_changed), NULL, NULL, clock_marshal_VOID_INT_INT, G_TYPE_NONE, 2, G_TYPE_INT, G_TYPE_INT);
More information on g_signal_new can be found in the documentation. Notice the rather strange symbol there: `clock_marshal_VOIDINT_INT`. This is our marshaller. By default, GTK+ provides us with a useful set of common marshallers, all of which start with g_cclosure_marshal and are of the format `prefix_RETURNTYPE_PARAMETER_PARAMETER_etc`. A marshaller is unfortunately not provided for `VOID_INT_INT` so we will have to generate our own.
This is actually very easy to do, we simply define it in a file clock-marshallers.list, which contains a list of the marshallers we wish to generate. For example:
# these marshallers are generated with glib-genmarshal(1) VOID:INT,INT
To generate source files and headers for these marshallers, it is easiest to write a simple Makefile, for example:
all: clock clock: clock.c clock.h main.c clock-marshallers.c clock-marshallers.h gcc -g -o clock clock.c main.c clock-marshallers.c `pkg-config --libs --cflags gtk+-2.0` clock-marshallers.c: clock-marshallers.list glib-genmarshal --prefix _clock_marshal --body $< > $@ clock-marshallers.h: clock-marshallers.list glib-genmarshal --prefix _clock_marshal --header $< > $@
You can see the ‘clock’ target depends on clock.c, clock.h and main.c which we have, but it also depends on clock-marshallers.c and clock-marshallers.h which we don’t, so we define additional targets to generate them from our marhallers list. You will need to make sure that you #include clock-marshallers.h into clock.c in order to get the symbols we generated.
This kind of code autogeneration is designed to make things easier for programmers without requiring them to write repetitive marshaller code over and over again. This is especialy useful if you have lots of signals. Of course, there is nothing wrong with either writing the marshallers yourself, or simply using a generated marshallers list, but why would you? Doing so only makes it harder to add or edit signals later on.
Next we have to implement a button-press-event handler so that we can determine when someone has actually clicked on a hand. We can override the signals for button-press-event, button-release-event and motion-notify-handler at the same time as replacing the expose-event. In egg_clock_face_class_init():
widget_class->button_press_event = egg_clock_face_button_press; widget_class->button_release_event = egg_clock_face_button_release; widget_class->motion_notify_event = egg_clock_face_motion_notify;
From reading the documentation for GtkDrawingArea, our parent class, you will see that button events and motion events are masked out, so we will need to set them so that we receive events for processing. We need to do this for each EggClockFace, so in egg_clock_face_init() we’ll add:
gtk_widget_add_events (GTK_WIDGET (clock), GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK);
The line formed by the bearing of the hand is infinitely thin, so we can’t expect a user to be able to click on it. It would be nice to detect if the user clicked within 5 pixels of the line. To do this we require some more geometry.
We know that (sin ?, cos ?) is a point on the unit circle, that is it has magnitude 1. Thus a vector from the origin to (sin ?, cos ?) will be a unit vector, we will name it l. We will also take a vector p which is the vector from the origin to the point where the user clicked.
This would give vector components equal to:
px = event->x - clock->allocation.width / 2; py = clock->allocation.height / 2 - event->y; lx = sin (M_PI / 30 * minutes); ly = cos (M_PI / 30 * minutes);
Simple reasoning will tell us that there exists a point ul where l is perpendicular to (ul – p) (which is the shortest distance between the point and the line and is what we want to measure).
We can project p onto l using the dot product such that u = p.l. The dot product can be done mathematically like so:
u = lx * px + ly * py;
If u comes out to be a negative number we’ll ignore it, this means that the user clicked on the opposite side of the clock to where the hand is. Finally, the magnitude of the distance can be found. If the magnitude of the distance (squared) is less then 5 pixels (squared) we can set a private property dragging to be true (we used squared values here because we have no need to do a slow sqrt()).
if (u < 0) return FALSE; d2 = pow (px - u * lx, 2) + pow (py - u * ly, 2); if (d2 < 25) /* 5 pixels away from the line */ { priv->dragging = TRUE; }
We now need to implement handlers for the motion_notify_event and button_release_event. Both of these signal handlers share a lot of their code, so we can move it out into another function, emit_time_changed_signal(). The geometry for this function is simply the reverse of the geometry that we used to draw the hands on the clock face.
static emit_time_changed_signal (EggClockFace *clock, int x, int y) { EggClockFacePrivate *priv; double phi; int hour, minute; priv = EGG_CLOCK_FACE_GET_PRIVATE (clock); /* decode the minute hand */ /* normalise the coordinates around the origin */ x -= GTK_WIDGET (clock)->allocation.width / 2; y -= GTK_WIDGET (clock)->allocation.height / 2; /* phi is a bearing from north clockwise, use the same geometry as we * did to position the minute hand originally */ phi = atan2 (x, -y); if (phi < 0) phi += M_PI * 2; hour = priv->time.tm_hour; minute = phi * 30 / M_PI; /* update the offset */ priv->minute_offset = minute - priv->time.tm_min; egg_clock_face_redraw_canvas (clock); g_signal_emit (clock, egg_clock_face_signals[TIME_CHANGED], 0, hour, minute); }
The time-changed signal is actually sent to all listeners by g_signal_emit. You may also notice the variable minute_offset, we use this to know how far out of phase the minute hand is with the current time. This offset has to be added to any other requests for the current time. I will leave it as an exercise to the reader to implement a similar offset for the hour hand.
After all of this our two signals handlers from above are simply:
static gboolean egg_clock_face_motion_notify (GtkWidget *clock, GdkEventMotion *event) { EggClockFacePrivate *priv; int x, y; priv = EGG_CLOCK_FACE_GET_PRIVATE (clock); if (priv->dragging) { emit_time_changed_signal (EGG_CLOCK_FACE (clock), event->x, event->y); } } static gboolean egg_clock_face_button_release (GtkWidget *clock, GdkEventButton *event) { EggClockFacePrivate *priv; priv = EGG_CLOCK_FACE_GET_PRIVATE (clock); if (priv->dragging) { priv = EGG_CLOCK_FACE_GET_PRIVATE (clock); priv->dragging = FALSE; emit_time_changed_signal (EGG_CLOCK_FACE (clock), event->x, event->y); } return FALSE; }
Of course, in order to find out when our signal is emitted, we only need to connect a signal handler to the clock in main.c:
g_signal_connect (clock, "time-changed<a href=":time-changed">, G_CALLBACK (time_changed_cb), NULL); static void time_changed_cb (EggClockFace *clock, int hours, int minutes, gpointer data) { g_print (</a> - %02i:%02in", hours, minutes); }
Putting it all together, you should have clock.c, clock.h, main.c, clock-marshallers.list and the Makefile.
That’s it! You now know how to implement a GObject, draw things inside that GObject, add private data, add signals and animate the object. This forms pretty much everything you need to write your own GtkWidget.
[править] Ссылки
Источник: The GNOME Journal