Custom drawing is to draw shapes dynamically. This section shows an example of custom drawing. You can draw rectangles by dragging the mouse.
Down the button.
Move the mouse
Up the button.
The programs are at src/custom_drawing
directory.
Download the repository and see
the directory. There are four files.
This file describes a ui file to compile. The compiler glib-compile-resources uses it.
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/com/github/ToshioCP/rect">
<file>rect.ui</file>
</gresource>
</gresources>
The prefix is /com/github/ToshioCP/rect
and the file is
rect.ui
. Therefore, GtkBuilder reads the resource from
/com/github/ToshioCP/rect/rect.ui
.
The following is the ui file that defines the widgets. There are two
widgets which are GtkApplicationWindow and GtkDrawingArea. The ids are
win
and da
respectively.
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object class="GtkApplicationWindow" id="win">
<property name="default-width">800</property>
<property name="default-height">600</property>
<property name="resizable">FALSE</property>
<property name="title">Custom drawing</property>
<child>
<object class="GtkDrawingArea" id="da">
<property name="hexpand">TRUE</property>
<property name="vexpand">TRUE</property>
</object>
</child>
</object>
</interface>
This program uses GtkApplication. The application ID is
com.github.ToshioCP.rect
.
#define APPLICATION_ID "com.github.ToshioCP.rect"
See GNOME Developer Documentation for further information.
The function main
is called at the beginning of the
application.
int
main (int argc, char **argv) {
GtkApplication *app;
int stat;
app = gtk_application_new (APPLICATION_ID, G_APPLICATION_HANDLES_OPEN);
g_signal_connect (app, "startup", G_CALLBACK (app_startup), NULL);
g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
g_signal_connect (app, "shutdown", G_CALLBACK (app_shutdown), NULL);
stat =g_application_run (G_APPLICATION (app), argc, argv);
g_object_unref (app);
return stat;
}
It connects three signals and handlers.
static void
app_startup (GApplication *application) {
GtkApplication *app = GTK_APPLICATION (application);
GtkBuilder *build;
GtkWindow *win;
GtkDrawingArea *da;
GtkGesture *drag;
build = gtk_builder_new_from_resource ("/com/github/ToshioCP/rect/rect.ui");
win = GTK_WINDOW (gtk_builder_get_object (build, "win"));
da = GTK_DRAWING_AREA (gtk_builder_get_object (build, "da"));
gtk_window_set_application (win, app);
g_object_unref (build);
gtk_drawing_area_set_draw_func (da, draw_cb, NULL, NULL);
g_signal_connect_after (da, "resize", G_CALLBACK (resize_cb), NULL);
drag = gtk_gesture_drag_new ();
gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (drag), GDK_BUTTON_PRIMARY);
gtk_widget_add_controller (GTK_WIDGET (da), GTK_EVENT_CONTROLLER (drag));
g_signal_connect (drag, "drag-begin", G_CALLBACK (drag_begin), NULL);
g_signal_connect (drag, "drag-update", G_CALLBACK (drag_update), da);
g_signal_connect (drag, "drag-end", G_CALLBACK (drag_end), da);
}
The startup handler does three things.
static void
app_activate (GApplication *application) {
GtkApplication *app = GTK_APPLICATION (application);
GtkWindow *win;
win = gtk_application_get_active_window (app);
gtk_window_present (win);
}
The activate handler just shows the window.
The program has two cairo surfaces and they are pointed by the global variables.
static cairo_surface_t *surface = NULL;
static cairo_surface_t *surface_save = NULL;
The drawing process is as follows.
surface
.surface
to the cairo surface of the
GtkDrawingArea.gtk_widget_queue_draw (da)
to draw it if
necessary.They are created in the “resize” signal handler.
static void
resize_cb (GtkWidget *widget, int width, int height, gpointer user_data) {
cairo_t *cr;
if (surface)
cairo_surface_destroy (surface);
surface = cairo_image_surface_create (CAIRO_FORMAT_RGB24, width, height);
if (surface_save)
cairo_surface_destroy (surface_save);
surface_save = cairo_image_surface_create (CAIRO_FORMAT_RGB24, width, height);
/* Paint the surface white. It is the background color. */
cr = cairo_create (surface);
cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
cairo_paint (cr);
cairo_destroy (cr);
}
This callback is called when the GtkDrawingArea is shown. It is the only call because the window is not resizable.
It creates image surfaces for surface
and
surface_save
. The surface
surface is painted
white, which is the background color.
The drawing function copies surface
to the
GtkDrawingArea surface.
static void
draw_cb (GtkDrawingArea *da, cairo_t *cr, int width, int height, gpointer user_data) {
if (surface) {
cairo_set_source_surface (cr, surface, 0.0, 0.0);
cairo_paint (cr);
}
}
This function is called by the system when it needs to redraw the drawing area.
Two surfaces surface
and surface_save
are
destroyed before the application quits.
static void
app_shutdown (GApplication *application) {
if (surface)
cairo_surface_destroy (surface);
if (surface_save)
cairo_surface_destroy (surface_save);
}
Gesture class is used to recognize human gestures such as click, drag, pan, swipe and so on. It is a subclass of GtkEventController. GtkGesture class is abstract and there are several implementations.
The program rect.c
uses GtkGestureDrag. It is the
implementation for drags. The parent-child relationship is as
follows.
GObject -- GtkEventController -- GtkGesture -- GtkGestureSingle -- GtkGestureDrag
GtkGestureSingle is a subclass of GtkGesture and optimized for singe-touch and mouse gestures.
A GtkGestureDrag instance is created and initialized in the startup
signal handler in rect.c
. See line 18 to 23 in the
following.
static void
app_startup (GApplication *application) {
GtkApplication *app = GTK_APPLICATION (application);
GtkBuilder *build;
GtkWindow *win;
GtkDrawingArea *da;
GtkGesture *drag;
build = gtk_builder_new_from_resource ("/com/github/ToshioCP/rect/rect.ui");
win = GTK_WINDOW (gtk_builder_get_object (build, "win"));
da = GTK_DRAWING_AREA (gtk_builder_get_object (build, "da"));
gtk_window_set_application (win, app);
g_object_unref (build);
gtk_drawing_area_set_draw_func (da, draw_cb, NULL, NULL);
g_signal_connect_after (da, "resize", G_CALLBACK (resize_cb), NULL);
drag = gtk_gesture_drag_new ();
gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (drag), GDK_BUTTON_PRIMARY);
gtk_widget_add_controller (GTK_WIDGET (da), GTK_EVENT_CONTROLLER (drag));
g_signal_connect (drag, "drag-begin", G_CALLBACK (drag_begin), NULL);
g_signal_connect (drag, "drag-update", G_CALLBACK (drag_update), da);
g_signal_connect (drag, "drag-end", G_CALLBACK (drag_end), da);
}
gtk_gesture_drag_new
creates a new
GtkGestureDrag instance.gtk_gesture_single_set_button
sets the
button number to listen to. The constant GDK_BUTTON_PRIMARY
is the left button of a mouse.gtk_widget_add_controller
adds an event
controller, gestures are descendants of the event controller, to a
widget.The process during the drag is as follows.
We need two global variables for the start point.
static double start_x;
static double start_y;
The following is the handler for the “drag-begin” signal.
static void
copy_surface (cairo_surface_t *src, cairo_surface_t *dst) {
if (!src || !dst)
return;
cairo_t *cr = cairo_create (dst);
cairo_set_source_surface (cr, src, 0.0, 0.0);
cairo_paint (cr);
cairo_destroy (cr);
}
static void
drag_begin (GtkGestureDrag *gesture, double x, double y, gpointer user_data) {
// save the surface and record (x, y)
copy_surface (surface, surface_save);
start_x = x;
start_y = y;
}
surface
to surface_save
, which is
an image just before the dragging.start_x
and
start_y
.static void
drag_update (GtkGestureDrag *gesture, double offset_x, double offset_y, gpointer user_data) {
GtkWidget *da = GTK_WIDGET (user_data);
cairo_t *cr;
copy_surface (surface_save, surface);
cr = cairo_create (surface);
cairo_rectangle (cr, start_x, start_y, offset_x, offset_y);
cairo_set_line_width (cr, 1.0);
cairo_stroke (cr);
cairo_destroy (cr);
gtk_widget_queue_draw (da);
}
surface
from surface_save
.gtk_widget_queue_draw
to add the GtkDrawingArea
to the queue to redraw.static void
drag_end (GtkGestureDrag *gesture, double offset_x, double offset_y, gpointer user_data) {
GtkWidget *da = GTK_WIDGET (user_data);
cairo_t *cr;
copy_surface (surface_save, surface);
cr = cairo_create (surface);
cairo_rectangle (cr, start_x, start_y, offset_x, offset_y);
cairo_set_line_width (cr, 6.0);
cairo_stroke (cr);
cairo_destroy (cr);
gtk_widget_queue_draw (da);
}
surface
from surface_save
.gtk_widget_queue_draw
to add the GtkDrawingArea
to the queue to redraw.Download the repository. Change
your current directory to src/custom_drawing
. Run meson and
ninja to build the program. Type _build/rect
to run the
program. Try to draw rectangles.
$ cd src/custom_drawing
$ meson setup _build
$ ninja -C _build
$ _build/rect