Logo miruBytes
Introduction to Linux GUI applications (GTK) | Part 2

Introduction to Linux GUI applications (GTK) | Part 2

November 20, 2024
12 min read
Table of Contents

Introduction

Hello, everyone! Lana here, welcome back to the series where I teach you how to make applications using GTK. In the previous post, we created a simple Hello World application. Today we will be building upon that so make sure to give it a read if you are new here :3

In a GTK application, the interface is primarily defined by the use of widgets, which are the building blocks for creating interactive elements and layouts. From a web-developer’s point of view, widgets like GtkButton, GtkLabel and GtkEntry are analogous to <button>, <label> and <input>. Here’s a list of widgets provided by GTK along with their visual illustration. It can be used as reference and a good starting point while we create our application.

We’ll be building a simple To-Do list application today. This project introduces essential concepts like input handling and state management, along with layout organization and event handling in GTK.

So let’s get started.

Prerequisites

Although it isn’t essential, if you haven’t setup your development environment yet, refer to the previous post if you face issues while setting up the IDE or CMake.

Dependencies

  • GTK 4
  • GDK
  • GObject
  • GLib

You can install them for your linux distribution by using,

Ubuntu/Debian
sudo apt install libgtk-4-dev
Fedora
sudo dnf install gtk4-devel
Arch Linux
sudo pacman -S gtk4

CMake Configuration

CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(ToDoList C)
 
set(CMAKE_C_STANDARD 11)
 
set(SOURCE_FILES main.c)
 
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED gtk4)
 
include_directories(${GTK_INCLUDE_DIRS})
link_directories(${GTK_LIBRARY_DIRS})
 
add_executable(${PROJECT_NAME} ${SOURCE_FILES})
target_link_libraries(${PROJECT_NAME} ${GTK_LIBRARIES})

Writing the Application

Let’s get into writing the application, we will first make the window and then add in the child widgets one by one,

Setting up the Main Window

First we will be writing the boilerplate code required to create a new window :p

main.c
#include <gtk/gtk.h>
 
 
static void
activate (GtkApplication *app,
          gpointer        user_data)
{
    GtkWidget *window;
 
    window = gtk_application_window_new (app);
    gtk_window_set_title (GTK_WINDOW (window), "To-Do List");
    gtk_window_set_default_size (GTK_WINDOW (window), 300, 400);
 
    gtk_window_present (GTK_WINDOW (window));
}
 
int
main (int    argc,
      char **argv)
{
    GtkApplication *app;
    int status;
 
    app = gtk_application_new ("com.mirubytes.ToDoList", G_APPLICATION_DEFAULT_FLAGS);
    g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
    status = g_application_run (G_APPLICATION (app), argc, argv);
    g_object_unref (app);
 
    return status;
}

Here we have set the basic fields such as window title, default_size and ApplicationId and bootstrapped the window to open on application startup.

Adding the Basic Layout

We have yoinked a reference image from the internet for the purpose of not having to design a layout :p

Reference image layout for a To-Do list application

In the image above you can see that there are two sections which can be divided vertically. The upper section used for getting user input for task name and the bottom section to list the tasks we have added.

First are gonna define a vertical container for rest of the widgets, within the window. We are also adding a margin of 10px.

main.c
static void
activate (GtkApplication *app,
          gpointer        user_data)
{
    GtkWidget *window;
 
    //Main Window
    window = gtk_application_window_new (app);
    gtk_window_set_title (GTK_WINDOW (window), "To-Do List");
    gtk_window_set_default_size (GTK_WINDOW (window), 300, 400);
 
    //Main vertically oriented container box
    GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
    gtk_widget_set_margin_top(vbox, 10);
    gtk_widget_set_margin_bottom(vbox, 10);
    gtk_widget_set_margin_start(vbox, 10);
    gtk_widget_set_margin_end(vbox, 10);
    gtk_window_set_child(GTK_WINDOW(window), vbox);
 
    gtk_window_present (GTK_WINDOW (window));
}

Imaging showing the main container of the application

We will now add in the layout for handling user input

main.c
static void
activate (GtkApplication *app,
          gpointer        user_data)
{
    GtkWidget *window;
 
    //Main Window
    window = gtk_application_window_new (app);
    gtk_window_set_title (GTK_WINDOW (window), "To-Do List");
    gtk_window_set_default_size (GTK_WINDOW (window), 300, 400);
 
    //Main vertically oriented container box
    GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
    gtk_widget_set_margin_top(vbox, 10);
    gtk_widget_set_margin_bottom(vbox, 10);
    gtk_widget_set_margin_start(vbox, 10);
    gtk_widget_set_margin_end(vbox, 10);
    gtk_window_set_child(GTK_WINDOW(window), vbox);
 
    //Container with entry-field and add button
    GtkWidget *top_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
    gtk_box_set_homogeneous(GTK_BOX(top_box), FALSE);
    gtk_box_append(GTK_BOX(vbox), top_box);

    GtkWidget *entry = gtk_entry_new();
    gtk_widget_set_hexpand(entry, TRUE);
    gtk_widget_set_margin_top(entry, 5);
    gtk_widget_set_margin_bottom(entry, 5);
    gtk_box_append(GTK_BOX(top_box), entry);

    GtkWidget *add_button = gtk_button_new_with_label("Add");
    gtk_box_append(GTK_BOX(top_box), add_button);
 
    gtk_window_present (GTK_WINDOW (window));
}

Here we are adding three widgets:

  • top_box which is the container at the top of our vbox container. gtk_box_set_homogeneous(GTK_BOX(top_box), FALSE) sets that the children of our top_box container won’t be of the same size.
  • entry which is the entry field widget for taking in user input.
  • add_button which is the button which will be used to add tasks to the list.

The image shows the container handling user input which includes the entry and addbutton widgets

Next step involves adding the second section of our application i.e. the list of tasks

main.c
static void
activate (GtkApplication *app,
          gpointer        user_data)
{
    GtkWidget *window;
 
    //Main Window
    window = gtk_application_window_new (app);
    gtk_window_set_title (GTK_WINDOW (window), "To-Do List");
    gtk_window_set_default_size (GTK_WINDOW (window), 300, 400);
 
    //Main vertically oriented container box
    GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
    gtk_widget_set_margin_top(vbox, 10);
    gtk_widget_set_margin_bottom(vbox, 10);
    gtk_widget_set_margin_start(vbox, 10);
    gtk_widget_set_margin_end(vbox, 10);
    gtk_window_set_child(GTK_WINDOW(window), vbox);
 
    //Container with entry-field and add button
    GtkWidget *top_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
    gtk_box_set_homogeneous(GTK_BOX(top_box), FALSE);
    gtk_box_append(GTK_BOX(vbox), top_box);
 
    GtkWidget *entry = gtk_entry_new();
    gtk_widget_set_hexpand(entry, TRUE);
    gtk_widget_set_margin_top(entry, 5);
    gtk_widget_set_margin_bottom(entry, 5);
    gtk_box_append(GTK_BOX(top_box), entry);
 
    GtkWidget *add_button = gtk_button_new_with_label("Add");
    gtk_box_append(GTK_BOX(top_box), add_button);
 
    //Widget which will display our list of tasks
    GtkWidget *task_list = gtk_list_box_new();
    gtk_widget_set_vexpand(task_list, TRUE);
    gtk_box_append(GTK_BOX(vbox), task_list);
 
    gtk_window_present (GTK_WINDOW (window));
}

The image shows all the layout widgets in the container

Now we are gonna define the layout of the list elements. We can use GtkCheckButton for this purpose. We will be defining it in a new function which will be responsible for the creation of the task widgets.

main.c
static GtkWidget* create_new_task(const char *title) {
    GtkWidget *task_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
    GtkWidget *checkbutton = gtk_check_button_new_with_label(title);
    gtk_box_append(GTK_BOX(task_box), checkbutton);
    return task_box;
}

Here we are taking a string as parameter and using that string to create the elements for our list of tasks.

Thus our layout is finished, although it’s not functional at the moment. Let’s add the logic behind the working of our application.

Adding the Application Logic

Our layout is finished, although it doesn’t really do anything. We need to define the logic for our application to be functional.

Task Creation

We will first edit our previously made create_new_task() function. This will be the callback function which will be invoked when the add_button is clicked.

In GTK it’s essential to adhere to the function signature, in this case, our callback function should include the parameters GtkButton *button and user data pointer.

We will be passing two parameters to the callback function:

  • GtkEntry *entry which is the input field widget.
  • GtkListBox *task_list which is the widget containing the list of tasks.

Since callbacks in GTK can only be invoked with a single user data parameter, we need to define a struct first to encapsulate the data and then pass the pointer of that struct as user data.

main.c
struct task_data {
    GtkEntry *input;
    GtkListBox *list;
};

The callback function should look something like this,

main.c
static void create_new_task(GtkButton *button, const struct task_data *data) {
}

Next step is to get the text from the entry widget and add it to the list of tasks,

main.c
static void create_new_task(GtkButton *button, const struct task_data *data) {
    GtkWidget *task_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 30);

    GtkEntryBuffer *buffer = gtk_entry_get_buffer(data->input);

    const gchar *task_text = gtk_entry_buffer_get_text(buffer);

    GtkWidget *checkbutton = gtk_check_button_new_with_label(task_text);
    gtk_box_append(GTK_BOX(task_box), checkbutton);
    gtk_list_box_append(data->list, task_box);
}

Each GtkEntry widget has an associated GtkEntryBuffer, which manages the text input. To retrieve the text entered by the user, we first obtain the GtkEntryBuffer from the GtkEntry widget and then use it to fetch the text.

After obtaining the text, we create a GtkCheckButton with the retrieved text as its label. We then add this checkbutton to a container task_box and append task_box to the list task_list to display it as part of the to-do list.

Now that we have our create_new_task function ready, it’s time to tie it to a button click event. To do this, we need to set up a callback in the activate function, which will be invoked whenever the user clicks the “Add” button.

GTK, button click events are handled using signals, and we can associate a function with a button’s “clicked” signal. When the button is clicked, GTK automatically calls the function we’ve connected to this signal. This can be done by,

main.c
static void
activate (GtkApplication *app,
          gpointer        user_data)
{
    GtkWidget *window;
 
    //Main Window
    window = gtk_application_window_new (app);
    gtk_window_set_title (GTK_WINDOW (window), "To-Do List");
    gtk_window_set_default_size (GTK_WINDOW (window), 300, 400);
 
    //Main vertically oriented container box
    GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
    gtk_widget_set_margin_top(vbox, 10);
    gtk_widget_set_margin_bottom(vbox, 10);
    gtk_widget_set_margin_start(vbox, 10);
    gtk_widget_set_margin_end(vbox, 10);
    gtk_window_set_child(GTK_WINDOW(window), vbox);
 
    //Container with entry-field and add button
    GtkWidget *top_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
    gtk_box_set_homogeneous(GTK_BOX(top_box), FALSE);
    gtk_box_append(GTK_BOX(vbox), top_box);
 
    GtkWidget *entry = gtk_entry_new();
    gtk_widget_set_hexpand(entry, TRUE);
    gtk_widget_set_margin_top(entry, 5);
    gtk_widget_set_margin_bottom(entry, 5);
    gtk_box_append(GTK_BOX(top_box), entry);
 
    GtkWidget *add_button = gtk_button_new_with_label("Add");
    gtk_box_append(GTK_BOX(top_box), add_button);
 
    //Widget which will display our list of tasks
    GtkWidget *task_list = gtk_list_box_new();
    gtk_widget_set_vexpand(task_list, TRUE);
    gtk_box_append(GTK_BOX(vbox), task_list);
 
    struct task_data *data = g_new(struct task_data, 1);
    data->input = GTK_ENTRY(entry);
    data->list = GTK_LIST_BOX(task_list);

    g_signal_connect(add_button, "clicked", G_CALLBACK (create_new_task), data );
 
    gtk_window_present (GTK_WINDOW (window));
}

Here we are initialising our struct and then passing the pointer to it in the callback function, as data.

The image demonstrates task creation in our To-do list application

Yay! Our application should append tasks to the list when we click the Add button now.

Task Deletion

Currently we cannot remove the tasks we have created, well, taking things off a to-do list is the most important part :p. What we want is, upon clicking the check-box, the task is removed from the list. We can use the toggled signal emitted by checkbutton for achieving this purpose.

Just like the previous function, we will define a callback function in the following way,

main.c
static void delete_task(GtkCheckButton *check_button, gpointer *data) {}

If we want to remove a widget, it can simply be done by getting the parent container and calling the remove function.

main.c
static void delete_task(GtkWidget *check_button, gpointer data) {
    GtkWidget *task_box = gtk_widget_get_parent(check_button);
    GtkWidget *list_box_row = gtk_widget_get_parent(task_box);
    GtkWidget *list = gtk_widget_get_parent(list_box_row);

    gtk_list_box_remove(GTK_LIST_BOX(list), list_box_row);
}

Every container implements an explicit remove function for removing children. In this case we are using gtk_list_box_remove. Now we just need to tie the toggled signal to this callback function. It can be done when the checkbuttons are being created by adding,

main.c
static void create_new_task(GtkButton *button, const struct task_data *data) {
    GtkWidget *task_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 30);
 
    GtkEntryBuffer *buffer = gtk_entry_get_buffer(data->input);
 
    const gchar *task_text = gtk_entry_buffer_get_text(buffer);
 
    GtkWidget *checkbutton = gtk_check_button_new_with_label(task_text);
    g_signal_connect(checkbutton, "toggled", G_CALLBACK (delete_task), NULL);
 
    gtk_box_append(GTK_BOX(task_box), checkbutton);
    gtk_list_box_append(data->list, task_box);
}

Image showing the checkboxes being activated which initiates deletion of task

The final application code should look something like,

main.c
#include <gtk/gtk.h>
 
struct task_data {
    GtkEntry *input;
    GtkListBox *list;
};
 
static void delete_task(GtkWidget *check_button, gpointer data) {
    GtkWidget *task_box = gtk_widget_get_parent(check_button);
    GtkWidget *list_box_row = gtk_widget_get_parent(task_box);
    GtkWidget *list = gtk_widget_get_parent(list_box_row);
 
    gtk_list_box_remove(GTK_LIST_BOX(list), list_box_row);
}
 
static void create_new_task(GtkButton *button, const struct task_data *data) {
    GtkWidget *task_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 30);
 
    GtkEntryBuffer *buffer = gtk_entry_get_buffer(data->input);
 
    const gchar *task_text = gtk_entry_buffer_get_text(buffer);
 
    GtkWidget *checkbutton = gtk_check_button_new_with_label(task_text);
    g_signal_connect(checkbutton, "toggled", G_CALLBACK (delete_task), NULL);
 
    gtk_box_append(GTK_BOX(task_box), checkbutton);
    gtk_list_box_append(data->list, task_box);
}
 
static void
activate (GtkApplication *app,
          gpointer        user_data)
{
    GtkWidget *window;
 
    //Main Window
    window = gtk_application_window_new (app);
    gtk_window_set_title (GTK_WINDOW (window), "To-Do List");
    gtk_window_set_default_size (GTK_WINDOW (window), 300, 400);
 
    //Main vertically oriented container box
    GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
    gtk_widget_set_margin_top(vbox, 10);
    gtk_widget_set_margin_bottom(vbox, 10);
    gtk_widget_set_margin_start(vbox, 10);
    gtk_widget_set_margin_end(vbox, 10);
    gtk_window_set_child(GTK_WINDOW(window), vbox);
 
    //Container with entry-field and add button
    GtkWidget *top_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
    gtk_box_set_homogeneous(GTK_BOX(top_box), FALSE);
    gtk_box_append(GTK_BOX(vbox), top_box);
 
    GtkWidget *entry = gtk_entry_new();
    gtk_widget_set_hexpand(entry, TRUE);
    gtk_widget_set_margin_top(entry, 5);
    gtk_widget_set_margin_bottom(entry, 5);
    gtk_box_append(GTK_BOX(top_box), entry);
 
    GtkWidget *add_button = gtk_button_new_with_label("Add");
    gtk_box_append(GTK_BOX(top_box), add_button);
 
    //Widget which will display our list of tasks
    GtkWidget *task_list = gtk_list_box_new();
    gtk_widget_set_vexpand(task_list, TRUE);
    gtk_box_append(GTK_BOX(vbox), task_list);
 
    struct task_data *data = g_new(struct task_data, 1);
    data->input = GTK_ENTRY(entry);
    data->list = GTK_LIST_BOX(task_list);
 
    g_signal_connect(add_button, "clicked", G_CALLBACK (create_new_task), data );
 
    gtk_window_present (GTK_WINDOW (window));
}
 
 
int
main (int    argc,
      char **argv)
{
    GtkApplication *app;
    int status;
 
 
    app = gtk_application_new ("com.mirubytes.ToDoList", G_APPLICATION_DEFAULT_FLAGS);
    g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
    status = g_application_run (G_APPLICATION (app), argc, argv);
    g_object_unref (app);
 
 
    return status;
}

Conclusion

We’ve successfully built the core functionality of our to-do application! Now, users can add tasks to the list and remove them with a single click on the checkbox. Through this project, we’ve explored GTK’s widgets, signal handling, and how to dynamically create and remove elements within a GtkListBox.

Our application still looks pretty ugly, we can fix that by styling our widgets. We shall discuss that in the next post :3.

Thank you for sticking till the end and hope you have a great day ;)

References