Now, we will make a new application which has GtkDrawingArea and
TfeTextView in it. Its name is “color”. If you write a name of a color
in TfeTextView and click on the run
button, then the color
of GtkDrawingArea changes to the color given by you.
The following colors are available. (without new line charactor)
In addition the following two options are also available.
This application can only do very simple things. However, it tells us that if we add powerful parser to it, we will be able to make it more efficient. I want to show it to you in the later section by making a turtle graphics language like Logo program language.
In this section, we focus on how to bind the two objects.
First, We need to make the ui file of the widgets. Title bar, four buttons in the tool bar, textview and drawing area. The ui file is as follows.
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object class="GtkApplicationWindow" id="win">
<property name="title">color changer</property>
<property name="default-width">600</property>
<property name="default-height">400</property>
<child>
<object class="GtkBox" id="boxv">
<property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<child>
<object class="GtkBox" id="boxh1">
<property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
<child>
<object class="GtkLabel" id="dmy1">
<property name="width-chars">10</property>
</object>
</child>
<child>
<object class="GtkButton" id="btnr">
<property name="label">Run</property>
<signal name="clicked" handler="run_cb"></signal>
</object>
</child>
<child>
<object class="GtkButton" id="btno">
<property name="label">Open</property>
<signal name="clicked" handler="open_cb"></signal>
</object>
</child>
<child>
<object class="GtkLabel" id="dmy2">
<property name="hexpand">TRUE</property>
</object>
</child>
<child>
<object class="GtkButton" id="btns">
<property name="label">Save</property>
<signal name="clicked" handler="save_cb"></signal>
</object>
</child>
<child>
<object class="GtkButton" id="btnc">
<property name="label">Close</property>
<signal name="clicked" handler="close_cb"></signal>
</object>
</child>
<child>
<object class="GtkLabel" id="dmy3">
<property name="width-chars">10</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="boxh2">
<property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
<property name="homogeneous">TRUE</property>
<child>
<object class="GtkScrolledWindow" id="scr">
<property name="hexpand">TRUE</property>
<property name="vexpand">TRUE</property>
<child>
<object class="TfeTextView" id="tv">
<property name="wrap-mode">GTK_WRAP_WORD_CHAR</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkDrawingArea" id="da">
<property name="hexpand">TRUE</property>
<property name="vexpand">TRUE</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>
boxh1
makes a tool bar which
has four buttons, Run
, Open
, Save
and Close
. This is similar to the tfe
text
editor in Section 9. There are two differences.
Run
button replaces New
button. A signal
element is added to each button object. It has “name” attribute which is
a signal name and “handler” attribute which is the name of its signal
handler. Options “-WI, –export-dynamic” CFLAG is necessary when you
compile the application. You can achieve this by adding “export_dynamic:
true” argument to the executable function in meson.build
.
And be careful that the handler must be defined without ‘static’
class.boxh2
includes
GtkScrolledWindow and GtkDrawingArea. GtkBox has “homogeneous property”
with TRUE value, so the two children have the same width in the box.
TfeTextView is a child of GtkScrolledWindow.The xml file for the resource compiler is almost same as before. Just substitute “color” for “tfe”.
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/com/github/ToshioCP/color">
<file>color.ui</file>
</gresource>
</gresources>
The main point of this program is a drawing function.
static void
draw_func (GtkDrawingArea *drawing_area, cairo_t *cr, int width, int height, gpointer user_data) {
if (surface) {
cairo_set_source_surface (cr, surface, 0, 0);
cairo_paint (cr);
}
}
The surface
variable in line 3 is a static variable.
static cairo_surface_t *surface = NULL;
The drawing function just copies the surface
to its own
surface with the cairo_paint
function. The surface (pointed
by the static variable surface
) is built by the
run
function.
static void
run (void) {
GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
GtkTextIter start_iter;
GtkTextIter end_iter;
char *contents;
cairo_t *cr;
gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter);
contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE);
if (surface) {
cr = cairo_create (surface);
if (g_strcmp0 ("red", contents) == 0)
cairo_set_source_rgb (cr, 1, 0, 0);
else if (g_strcmp0 ("green", contents) == 0)
cairo_set_source_rgb (cr, 0, 1, 0);
else if (g_strcmp0 ("blue", contents) == 0)
cairo_set_source_rgb (cr, 0, 0, 1);
else if (g_strcmp0 ("white", contents) == 0)
cairo_set_source_rgb (cr, 1, 1, 1);
else if (g_strcmp0 ("black", contents) == 0)
cairo_set_source_rgb (cr, 0, 0, 0);
else if (g_strcmp0 ("light", contents) == 0)
cairo_set_source_rgba (cr, 1, 1, 1, 0.5);
else if (g_strcmp0 ("dark", contents) == 0)
cairo_set_source_rgba (cr, 0, 0, 0, 0.5);
else
cairo_set_source_surface (cr, surface, 0, 0);
cairo_paint (cr);
cairo_destroy (cr);
}
g_free (contents);
}
contents
.surface
points a surface instance,
it is painted as follows.contents
and copied to the surface with cairo_paint
.The drawing area just reflects the surface
. But one
problem is resizing. If a user resizes the main window, the drawing area
is also resized. It makes size difference between the surface and the
drawing area. So, the surface needs to be resized to fit the drawing
area.
It is accomplished by connecting the “resize” signal on the drawing area to a handler.
(GTK_DRAWING_AREA (da), "resize", G_CALLBACK (resize_cb), NULL); g_signal_connect
The handler is as follows.
static void
resize_cb (GtkDrawingArea *drawing_area, int width, int height, gpointer user_data) {
if (surface)
cairo_surface_destroy (surface);
surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
run ();
}
If the variable surface
sets a surface instance, it is
destroyed. A new surface is created and its size fits the drawing area.
The surface is assigned to the variable surface
. The
function run
is called and the surface is colored.
The signal is emitted when:
So, the first surface is created when it is realized.
This is the main file.
Run
signal handler is the point in this program.The following is colorapplication.c
.
#include <gtk/gtk.h>
#include "../tfetextview/tfetextview.h"
static GtkWidget *win;
static GtkWidget *tv;
static GtkWidget *da;
static cairo_surface_t *surface = NULL;
static void
run (void) {
GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
GtkTextIter start_iter;
GtkTextIter end_iter;
char *contents;
cairo_t *cr;
gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter);
contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE);
if (surface) {
cr = cairo_create (surface);
if (g_strcmp0 ("red", contents) == 0)
cairo_set_source_rgb (cr, 1, 0, 0);
else if (g_strcmp0 ("green", contents) == 0)
cairo_set_source_rgb (cr, 0, 1, 0);
else if (g_strcmp0 ("blue", contents) == 0)
cairo_set_source_rgb (cr, 0, 0, 1);
else if (g_strcmp0 ("white", contents) == 0)
cairo_set_source_rgb (cr, 1, 1, 1);
else if (g_strcmp0 ("black", contents) == 0)
cairo_set_source_rgb (cr, 0, 0, 0);
else if (g_strcmp0 ("light", contents) == 0)
cairo_set_source_rgba (cr, 1, 1, 1, 0.5);
else if (g_strcmp0 ("dark", contents) == 0)
cairo_set_source_rgba (cr, 0, 0, 0, 0.5);
else
cairo_set_source_surface (cr, surface, 0, 0);
cairo_paint (cr);
cairo_destroy (cr);
}
g_free (contents);
}
void
run_cb (GtkWidget *btnr) {
run ();
gtk_widget_queue_draw (GTK_WIDGET (da));
}
void
open_cb (GtkWidget *btno) {
tfe_text_view_open (TFE_TEXT_VIEW (tv), GTK_WINDOW (win));
}
void
save_cb (GtkWidget *btns) {
tfe_text_view_save (TFE_TEXT_VIEW (tv));
}
void
close_cb (GtkWidget *btnc) {
gtk_window_destroy (GTK_WINDOW (win));
}
static void
resize_cb (GtkDrawingArea *drawing_area, int width, int height, gpointer user_data) {
if (surface)
cairo_surface_destroy (surface);
surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
run ();
}
static void
draw_func (GtkDrawingArea *drawing_area, cairo_t *cr, int width, int height, gpointer user_data) {
if (surface) {
cairo_set_source_surface (cr, surface, 0, 0);
cairo_paint (cr);
}
}
static void
app_activate (GApplication *application) {
gtk_window_present (GTK_WINDOW (win));
}
static void
app_startup (GApplication *application) {
GtkApplication *app = GTK_APPLICATION (application);
GtkBuilder *build;
GdkDisplay *display;
build = gtk_builder_new_from_resource ("/com/github/ToshioCP/color/color.ui");
win = GTK_WIDGET (gtk_builder_get_object (build, "win"));
gtk_window_set_application (GTK_WINDOW (win), app);
tv = GTK_WIDGET (gtk_builder_get_object (build, "tv"));
da = GTK_WIDGET (gtk_builder_get_object (build, "da"));
g_object_unref(build);
g_signal_connect (GTK_DRAWING_AREA (da), "resize", G_CALLBACK (resize_cb), NULL);
gtk_drawing_area_set_draw_func (GTK_DRAWING_AREA (da), draw_func, NULL, NULL);
display = gdk_display_get_default ();
GtkCssProvider *provider = gtk_css_provider_new ();
gtk_css_provider_load_from_data (provider, "textview {padding: 10px; font-family: monospace; font-size: 12pt;}", -1);
gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_USER);
}
static void
app_shutdown (GApplication *application) {
if (surface)
cairo_surface_destroy (surface);
}
#define APPLICATION_ID "com.github.ToshioCP.color"
int
main (int argc, char **argv) {
GtkApplication *app;
int stat;
app = gtk_application_new (APPLICATION_ID, G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect (app, "startup", G_CALLBACK (app_startup), NULL);
g_signal_connect (app, "shutdown", G_CALLBACK (app_shutdown), NULL);
g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
stat =g_application_run (G_APPLICATION (app), argc, argv);
g_object_unref (app);
return stat;
}
main
. It creates a new application
instance. And connects three signals startup, shutdown and activate to
their handlers. It runs the application. It releases the reference to
the application and returns with stat
value.This file is almost same as before. An argument “export_dynamic: true” is added to executable function.
project('color', 'c')
gtkdep = dependency('gtk4')
gnome=import('gnome')
resources = gnome.compile_resources('resources','color.gresource.xml')
sourcefiles=files('colorapplication.c', '../tfetextview/tfetextview.c')
executable('color', sourcefiles, resources, dependencies: gtkdep, export_dynamic: true)
Type the following to compile the program.
$ meson _build
$ ninja -C _build
The application is made in _build
directory. Type the
following to execute it.
$ _build/color
Type “red”, “green”, “blue”, “white”, black”, “light” or “dark” in
the TfeTextView. No new line charactor is needed. Then, click on the
Run
button. Make sure the color of GtkDrawingArea
changes.
In this program TfeTextView is used to change the color. You can use buttons or menus instead of textview. Probably it is more appropriate. Using textview is unnatural. It is a good practice to make such application by yourself.