GTK4 Tutorial Pt. 2

Kiran Chauhan
11 min readMar 7, 2021

--

Value your freedom, or you will lose it, teaches history. Richard M. Stallman, Linux, GNU, and freedom

This is the second article in the series GTK4 Tutorial. You can read the first article at here.

The program is continue running in terminal because of the while loop. In order to terminate the loop, we just need to make the loop condition false (only when close button or x icon is clicked).

GTK is an event driven toolkit, which means it will sleep in while loop of g_main_context_iteration() until an event occurs and control is passed to the appropriate function. This passing of control is done using the idea of "signals". When an event occurs, such as the press of a mouse button, the appropriate signal will be "emitted" by the widget that was pressed. This is how GTK does most of its useful work. There are signals that all widgets inherit, such as "destroy", and there are signals that are widget specific, such as "toggled" on a toggle button.

In our case, when we click on close button, “click” event occur and it emit the “destroy” signal. We just need to catch this signal and write a code or function against it. To perform this action, we need set up a signal handler to catch these signals and call the appropriate function. This is done by using a function such as g_signal_connect().

g_signal_connect(instance, detailed_signal, c_handler, data);

The first argument is the widget (or an instance) which will be emitting the signal, and the second the name of the signal you wish to catch (mostly in literal string such as, "destroy"). The third is the function (function name or complete function body) you wish to be called when it is caught, and the fourth, the data you wish to have passed to this function.

In order to terminate the process, we need to catch the “destroy” signal from window widget. The first argument should be window with type of GtkWidget.

g_signal_connect(window, detailed_signal, c_handler, data);

I just wrote window here because window is GtkWidget type. Next, lets mention the name of signal which are interested to catch e.g. "destroy".

g_signal_connect(window, "destroy", c_handler, data);

c_handler is a function or function name that is going to execute when the "destroy" signal is caught from window instance. In GTK4, we call it callback function and type of this callback function is GCallback. So, we need to cast the normal function to GCallback function. This can be done via G_CALLBACK macro. Just pass name of the function to this macro and rest will be taken care. Let's assume that our function name is quit_app.

g_signal_connect(window, "destroy", G_CALLBACK(quit_app), data);

No parenthesis after quit_app as we're going to write either function name or function itself (but not going to call the function).

Finally, we’re going to pass some data to this function which is in our case, the pointer to done variable. In this way, we can update the value of done to FALSE to terminate the loop.

g_signal_connect(window, "destroy", G_CALLBACK(quit_app), &done);

This line is perfect! Let’s add it into our hello.c program.

#include<gtk/gtk.h>

int main() {
gtk_init();
gboolean done = FALSE;

GtkWidget *window;
window = gtk_window_new();
g_signal_connect(window, "destroy", G_CALLBACK(quit_app), &done);
gtk_widget_show(window);

while(!done) {
g_main_context_iteration(NULL, TRUE);
}

return 0;
}

Our program wouldn’t run. Because, we haven’t define the quit_app function. This quit_app function is the special callback function and it should generally be of the form.

void user_function(GtkWidget *object, gpointer user_data);

The first argument will be a pointer to the widget that emitted the signal, and the last a pointer to the data given as the last argument to the g_signal_connect() function as shown above.

Let’s update the name from user_function to quit_app.

void quit_app(GtkWidget *object, gpointer user_data);

Let’s call the first parameter widget instead of object (this is personal preference, you can leave it, if you are okay with default).

void quit_app(GtkWidget *widget, gpointer user_data);

The user_data is the data we passed as last argument to g_signal_connect(). Let's change it to simple data as parameter name (again this is personal preference, you can leave it, if you are okay with default).

void quit_app(GtkWidget *widget, gpointer data)

This is the callback function that will run when destroy signal is caught from window instance.

void quit_app(GtkWidget *widget, gpointer data) {
/* Logic should go here. */
}

Perfect! Let’s add this before the main() function as we're going to call it from the main() function.

#include<gtk/gtk.h>

void quit_app(GtkWidget *widget, gpointer data) {
/* Logic should go here. */
}

int main() {
gtk_init();
gboolean done = FALSE;

GtkWidget *window;
window = gtk_window_new();
g_signal_connect(window, "destroy", G_CALLBACK(quit_app), &done);
gtk_widget_show(window);

while(!done) {
g_main_context_iteration(NULL, TRUE);
}

return 0;
}

Now, let’s write the logic in quit_app() function. The logic is simple - we're going to update the value of data to be TRUE as follows.

gboolean *done = data;
*done = TRUE;

Let’s put this within quit_app() function.

#include<gtk/gtk.h>

void quit_app(GtkWidget *widget, gpointer data) {
gboolean *done = data;
*done = TRUE;
}

int main() {
gtk_init();
gboolean done = FALSE;

GtkWidget *window;
window = gtk_window_new();
g_signal_connect(window, "destroy", G_CALLBACK(quit_app), &done);
gtk_widget_show(window);

while(!done) {
g_main_context_iteration(NULL, TRUE);
}

return 0;
}

Finally, add g_main_context_wakeup(NULL) at the end of quit_app() function. I'll not going to explain this in more detail and I ask you to go with as it (for now, just remember that you need to write).

#include<gtk/gtk.h>

void quit_app(GtkWidget *widget, gpointer data) {
gboolean *done = data;
*done = TRUE;

g_main_context_wakeup(NULL);
}

int main() {
gtk_init();
gboolean done = FALSE;

GtkWidget *window;
window = gtk_window_new();
g_signal_connect(window, "destroy", G_CALLBACK(quit_app), &done);
gtk_widget_show(window);

while(!done) {
g_main_context_iteration(NULL, TRUE);
}

return 0;
}

This line make sure that the g_main_context_iteration() is no longer running. And these are the few lines I was talking about to write in order to terminate the process in terminal. Let's run the application and confirm that when we close the application it actually terminate the process (when I say, let's run the program, you need to run these two commands unless I update the instruction on how to run the GTK4 program).

gcc hello.c `pkg-config --cflags --libs gtk4` -o hello
./hello

Perfect! When I click on close button or x icon, it close the window as well as terminate the process in terminal.

Personally speaking this is an extra coding. I mean we should expect that when we close the application, it should actually close the application (except few applications that doesn’t do. But, they have their own reasons to continue running the application in background)! I’ll cover this topic in next article again. But, now it is time go further and add some functionalities. I don’t consider closing an application is a functionality (even though it is. But, that something we should except. It is like creating a form and then adding a submit button as functionality. So, that user can submit form.)!

Let’s display a simple hello, world text in this blank window. We’re going to use GtkLabel for displaying the text or label (as they say it in GTK4). Let's define a label variable with type GtkWidget (by default, all most all widgets will be GtkWidget type).

#include<gtk/gtk.h>

void quit_app(GtkWidget *widget, gpointer data) {
gboolean *done = data;
*done = TRUE;

g_main_context_wakeup(NULL);
}

int main() {
gtk_init();
gboolean done = FALSE;

GtkWidget *window;
GtkWidget *label;

window = gtk_window_new();
g_signal_connect(window, "destroy", G_CALLBACK(quit_app), &done);
gtk_widget_show(window);

while(!done) {
g_main_context_iteration(NULL, TRUE);
}

return 0;
}

It is good idea to place all variables declaration nearby. Defining the variable doesn’t make it a label or text. In order to create a label, we actually need to invoke the gtk_label_new() method.

gtk_label_new (const char *str);

This method accept one argument a label or text that we want to display which is in our case “hello, world”.

#include<gtk/gtk.h>

void quit_app(GtkWidget *widget, gpointer data) {
gboolean *done = data;
*done = TRUE;

g_main_context_wakeup(NULL);
}

int main() {
gtk_init();
gboolean done = FALSE;

GtkWidget *window;
GtkWidget *label;

window = gtk_window_new();
g_signal_connect(window, "destroy", G_CALLBACK(quit_app), &done);

label = gtk_label_new("hello, world");

gtk_widget_show(window);

while(!done) {
g_main_context_iteration(NULL, TRUE);
}
return 0;
}

Perfect! The label is created. But, it’ll not display. Let’s try by running the program again.

gcc hello.c `pkg-config --cflags --libs gtk4` -o hello
./hello
hello.c output without hello, world label

See! Window is displaying blank. In order to display the label, we need to add it into window instance using gtk_window_set_child() method.

gtk_window_set_child(GtkWindow *window, GtkWidget *child);

This gtk_window_set_child() accepts two arguments - The window (e.g. parent window) and the widget (e.g. child widget label). Let's add this method calling line before we display the window as follows.

#include<gtk/gtk.h>

void quit_app(GtkWidget *widget, gpointer data) {
gboolean *done = data;
*done = TRUE;

g_main_context_wakeup(NULL);
}

int main() {
gtk_init();
gboolean done = FALSE;

GtkWidget *window;
GtkWidget *label;

window = gtk_window_new();
g_signal_connect(window, "destroy", G_CALLBACK(quit_app), &done);

label = gtk_label_new("hello, world");
gtk_window_set_child(window, label);

gtk_widget_show(window);

while(!done) {
g_main_context_iteration(NULL, TRUE);
}

return 0;
}

Let’s run this program and see how our label looks!

gcc hello.c `pkg-config --cflags --libs gtk4` -o hello
hello.c: In function 'main':
hello.c:22:24: warning: passing argument 1 of 'gtk_window_set_child' from incompatible pointer type [-Wincompatible-pointer-types]
22 | gtk_window_set_child(window, label);
| ^~~~~~
| |
| GtkWidget * {aka struct _GtkWidget *}
In file included from /usr/include/gtk-4.0/gtk/gtkaboutdialog.h:30,
from /usr/include/gtk-4.0/gtk/gtk.h:34,
from hello.c:1:
/usr/include/gtk-4.0/gtk/gtkwindow.h:231:59: note: expected 'GtkWindow *' {aka 'struct _GtkWindow *'} but argument is of type 'GtkWidget *' {aka 'struct _GtkWidget *'}
231 | void gtk_window_set_child (GtkWindow *window,
| ~~~~~~~~~~~~~~^~~~~~

Ah! We get the error. Let’s try to read it and see what is is saying (personally speaking, GTK errors are quite useful in almost all cases). As per error, what they’re saying is, gtk_window_set_chid() first argument should be type of GtkWindow widget whereas we passed GtkWidget type window. In order to pass the window with GtkWindow type, we need to cast using GTK_WINDOW macro. This macro accept one argument which is widget that we want to cast into window. So, let's update our code as follows.

#include<gtk/gtk.h>

void quit_app(GtkWidget *widget, gpointer data) {
gboolean *done = data;
*done = TRUE;

g_main_context_wakeup(NULL);
}

int main() {
gtk_init();
gboolean done = FALSE;

GtkWidget *window;
GtkWidget *label;

window = gtk_window_new();
g_signal_connect(window, "destroy", G_CALLBACK(quit_app), &done);

label = gtk_label_new("hello, world");

gtk_window_set_child(GTK_WINDOW(window), label);

gtk_widget_show(window);

while(!done) {
g_main_context_iteration(NULL, TRUE);
}

return 0;
}

Note: Casting widget from more general widget type (e.g. GtkWidget) to specific widget type (e.g. GtkWindow) is quite normal in GTK application. You'll see lots of type conversions or type casting. But, the good news is, GTK has nice support for this type casting. You just need to call the appropriate macro. In GTK, all macro are in capital.

Perfect! Let’s run this updated program now (this is the last time, I’m writing following two run commands. Onward now, when I say run, you need to run these command and I’ll directly talk about the output unless I want talk about the error).

gcc hello.c `pkg-config --cflags --libs gtk4` -o hello
./hello
window with hello, world label

Notice the window is small. Previously, we had a window with size 200x200. But, now it is small because GTK calculates the size based on what is inside. If there is nothing inside the window, it’ll display 200x200 else based on widget’s require size.

Let’s update the default size of window using gtk_window_set_default_size().

gtk_window_set_default_size (GtkWindow *window, int width, int height);

This method accept three arguments — the window, width, and height of window. Pay attention to first argument. It accepts the window with type GtkWidow not GtkWidget. So, we need to cast the window instance to GtkWindow using GTK_WINDOW macro.

#include<gtk/gtk.h>

void quit_app(GtkWidget *widget, gpointer data) {
gboolean *done = data;
*done = TRUE;

g_main_context_wakeup(NULL);
}

int main() {
gtk_init();
gboolean done = FALSE;

GtkWidget *window;
GtkWidget *label;

window = gtk_window_new();
gtk_window_set_default_size(GTK_WINDOW(window), 400, 400);
g_signal_connect(window, "destroy", G_CALLBACK(quit_app), &done);

label = gtk_label_new("hello, world");

gtk_window_set_child(GTK_WINDOW(window), label);

gtk_widget_show(window);

while(!done) {
g_main_context_iteration(NULL, TRUE);
}

return 0;
}

Let's run this program and see how our output looks!

hello, world window with 400x400 size

Let’s set the title of this window using gtk_window_set_title() method.

void gtk_window_set_title(GtkWindow *window, const char *title);

This method accept two arguments — The window instance, and title text as string. Again, I’m going to cast the window variable.

#include<gtk/gtk.h>

void quit_app(GtkWidget *widget, gpointer data) {
gboolean *done = data;
*done = TRUE;

g_main_context_wakeup(NULL);
}

int main() {
gtk_init();
gboolean done = FALSE;

GtkWidget *window;
GtkWidget *label;

window = gtk_window_new();
gtk_window_set_title(GTK_WINDOW(window), "hello, world application");
gtk_window_set_default_size(GTK_WINDOW(window), 400, 400);
g_signal_connect(window, "destroy", G_CALLBACK(quit_app), &done);

label = gtk_label_new("hello, world");
gtk_window_set_child(GTK_WINDOW(window), label);

gtk_widget_show(window);
while(!done) {
g_main_context_iteration(NULL, TRUE);
}

return 0;
}

Let’s run the program and see the output. Looks much better than previously.

hello, world application within title

GtkWindow has lots of methods that you can use it on window. I just used only few. But, in future, we'll use many of them. I'll also going to talk more about conventions and how to read the documentation (don't worry if you are still figuring out what is going on).

Before, we go further, let’s do some modification on our label using some of the GtkLabel methods.

I want to left-align the label in window. I can do it using gtk_label_set_xalign() method.

void gtk_label_set_xalign(GtkLabel *self, float xalign);

This method accept two arguments — the label and align value between 0 to 1. Our label is type of GtkWidget. So, we need to cast it using GTK_LABEL macro as follows.

#include<gtk/gtk.h>

void quit_app(GtkWidget *widget, gpointer data) {
gboolean *done = data;
*done = TRUE;

g_main_context_wakeup(NULL);
}

int main() {
gtk_init();
gboolean done = FALSE;

GtkWidget *window;
GtkWidget *label;

window = gtk_window_new();
gtk_window_set_default_size(GTK_WINDOW(window), 400, 400);
g_signal_connect(window, "destroy", G_CALLBACK(quit_app), &done);

label = gtk_label_new("hello, world");
gtk_label_set_xalign(GTK_LABEL(label), 0);
gtk_window_set_child(GTK_WINDOW(window), label);

gtk_widget_show(window);

while(!done) {
g_main_context_iteration(NULL, TRUE);
}

return 0;
}

Let’s run this program and see how it looks!

hello, world application with left-aligned text

Next, I want to display this label top-left corner. We already left-aligned. We just need to top-aligned and we’re good to go. We can top-align using gtk_label_set_yalign() method.

void gtk_label_set_yalign(GtkLabel *self, float yalign);

Again, this method accept two arguments — the label and align value between 0 to 1.

#include<gtk/gtk.h>

void quit_app(GtkWidget *widget, gpointer data) {
gboolean *done = data;
*done = TRUE;

g_main_context_wakeup(NULL);
}
int main() {
gtk_init();
gboolean done = FALSE;

GtkWidget *window;
GtkWidget *label;

window = gtk_window_new();
gtk_window_set_default_size(GTK_WINDOW(window), 400, 400);
g_signal_connect(window, "destroy", G_CALLBACK(quit_app), &done);

label = gtk_label_new("hello, world");
gtk_label_set_xalign(GTK_LABEL(label), 0);
gtk_label_set_yalign(GTK_LABEL(label), 0);

gtk_window_set_child(GTK_WINDOW(window), label);
gtk_widget_show(window);

while(!done) {
g_main_context_iteration(NULL, TRUE);
}

return 0;
}

Let’s run this program and confirm the output (it should be as we expected).

top-left aligned text in hello, world application

This GtkLabel has lots of useful methods. We're going to use many of them as we need in future articles (while creating applications). That's it for this article, in next article, I'm going to talk about how to create an application in "modern" GTK way and few naming conversions, and coding styles. From the next article, we'll start to polish the rough edges in learning.

--

--