BlinkGTK Event Handling Tutorial (C Language)

Last Updated: 2026-01-01
Target Version: BlinkGTK v0.9.28-dev or later
Level: Beginner to Advanced
Language: English |

日本語


Table of Contents

  1. Introduction
  2. Basics - Signal Connection
  3. Beginner - Using Single Signals
  4. Intermediate - Combining Multiple Signals
  5. Advanced - Complete Browser Application
  6. Pattern Collection
  7. Troubleshooting

Introduction

This tutorial teaches event-driven programming using BlinkGTK's GObject signal system.

What You'll Learn

Prerequisites

Required Environment

# Required packages
- gtk4
- blinkgtk

# Compilation example
gcc your-program.c $(pkg-config --cflags --libs blinkgtk-0.1) -o your-program

Basics - Signal Connection

What are GObject Signals?

GObject signals are the core of GTK/GLib event-driven programming:

BlinkGTK Signals:
| Signal Name | Firing Timing | Usage |
|-------------|--------------|-------|
| load-changed | Load state changed | Progress display |
| load-failed | Load failed | Error handling |
| title-changed | Title changed | Window title update |
| uri-changed | URI changed | Address bar update |

Your First Signal Handler

Let's start with the simplest example:

#include <gtk/gtk.h>
#include <blink_gtk/blink_gtk.h>

/* Signal handler */
static void on_load_changed(GtkWidget* web_view,
                            BlinkLoadEvent load_event,
                            gpointer user_data) {
    const char* event_name = "";

    switch (load_event) {
        case BLINK_LOAD_STARTED:
            event_name = "Started";
            break;
        case BLINK_LOAD_REDIRECTED:
            event_name = "Redirected";
            break;
        case BLINK_LOAD_COMMITTED:
            event_name = "Committed";
            break;
        case BLINK_LOAD_FINISHED:
            event_name = "Finished";
            break;
    }

    g_print("Event: %s\n", event_name);
}

static void on_activate(GtkApplication* app, gpointer user_data) {
    GtkWidget* window = gtk_application_window_new(app);
    GtkWidget* web_view = blink_web_view_new();

    /* Connect signal */
    g_signal_connect(web_view, "load-changed",
                     G_CALLBACK(on_load_changed), NULL);

    gtk_window_set_child(GTK_WINDOW(window), web_view);
    blink_web_view_load_uri(BLINK_WEB_VIEW(web_view), "https://example.com");
    gtk_window_present(GTK_WINDOW(window));
}

int main(int argc, char* argv[]) {
    blink_gtk_init(&argc, &argv);  /* GTK4 は引数なし */

    GtkApplication* app = gtk_application_new("org.example.signals", 0);
    g_signal_connect(app, "activate", G_CALLBACK(on_activate), NULL);

    /* Note g_application_run() は使用禁止 — Chromium RunLoop が飢餓状態になる
     * g_application_register/activate で GtkApplication シグナルを発火させてから
     * blink_gtk_run_main_loop() でChromium RunLoop を起動する */
    g_application_register(G_APPLICATION(app), NULL, NULL);
    g_application_activate(G_APPLICATION(app));
    int status = blink_gtk_run_main_loop();

    g_object_unref(app);
    blink_gtk_shutdown();
    return status;
}

Compile and Run:

gcc basic-signal.c $(pkg-config --cflags --libs blinkgtk-0.1) -o basic-signal
./basic-signal

Output:

Event: Started
Event: Committed
Event: Finished

Using user_data

How to pass additional data to signal handlers:

#include <gtk/gtk.h>
#include <blink_gtk/blink_gtk.h>

/* Application data */
typedef struct {
    GtkWidget* window;
    GtkWidget* status_label;
    int load_count;
} AppData;

/* Signal handler */
static void on_load_changed(GtkWidget* web_view,
                            BlinkLoadEvent load_event,
                            gpointer user_data) {
    AppData* data = (AppData*)user_data;

    switch (load_event) {
        case BLINK_LOAD_STARTED:
            gtk_label_set_text(GTK_LABEL(data->status_label),
                              "Loading...");
            break;

        case BLINK_LOAD_FINISHED:
            data->load_count++;
            char* message = g_strdup_printf("Done (#%d)", data->load_count);
            gtk_label_set_text(GTK_LABEL(data->status_label), message);
            g_free(message);
            break;

        default:
            break;
    }
}

static void on_activate(GtkApplication* app, gpointer user_data) {
    AppData* data = g_new0(AppData, 1);

    data->window = gtk_application_window_new(app);
    data->status_label = gtk_label_new("Ready");

    GtkWidget* web_view = blink_web_view_new();
    GtkWidget* vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);

    gtk_box_append(GTK_BOX(vbox), data->status_label);
    gtk_box_append(GTK_BOX(vbox), web_view);

    /* Connect signal (pass data as user_data) */
    g_signal_connect(web_view, "load-changed",
                     G_CALLBACK(on_load_changed), data);

    gtk_window_set_child(GTK_WINDOW(data->window), vbox);
    gtk_window_set_default_size(GTK_WINDOW(data->window), 800, 600);

    blink_web_view_load_uri(BLINK_WEB_VIEW(web_view), "https://example.com");
    gtk_window_present(GTK_WINDOW(data->window));
}

int main(int argc, char* argv[]) {
    blink_gtk_init(&argc, &argv);  /* GTK4 は引数なし */

    GtkApplication* app = gtk_application_new("org.example.userdata", 0);
    g_signal_connect(app, "activate", G_CALLBACK(on_activate), NULL);

    /* Note g_application_run() は使用禁止 — Chromium RunLoop が飢餓状態になる
     * g_application_register/activate で GtkApplication シグナルを発火させてから
     * blink_gtk_run_main_loop() でChromium RunLoop を起動する */
    g_application_register(G_APPLICATION(app), NULL, NULL);
    g_application_activate(G_APPLICATION(app));
    int status = blink_gtk_run_main_loop();

    g_object_unref(app);
    blink_gtk_shutdown();
    return status;
}

Key Points:


Beginner - Using Single Signals

Example 1: Progress Bar Display

Update progress bar with load-changed signal:

#include <gtk/gtk.h>
#include <blink_gtk/blink_gtk.h>

typedef struct {
    GtkWidget* progress_bar;
    GtkWidget* status_label;
} ProgressData;

static void on_load_changed(GtkWidget* web_view,
                            BlinkLoadEvent load_event,
                            gpointer user_data) {
    ProgressData* data = (ProgressData*)user_data;

    switch (load_event) {
        case BLINK_LOAD_STARTED:
            /* 0% */
            gtk_progress_bar_set_fraction(
                GTK_PROGRESS_BAR(data->progress_bar), 0.0);
            gtk_widget_set_visible(data->progress_bar, TRUE);
            gtk_label_set_text(GTK_LABEL(data->status_label),
                              "Loading started...");
            break;

        case BLINK_LOAD_REDIRECTED:
            /* 30% */
            gtk_progress_bar_set_fraction(
                GTK_PROGRESS_BAR(data->progress_bar), 0.3);
            gtk_label_set_text(GTK_LABEL(data->status_label),
                              "Redirecting...");
            break;

        case BLINK_LOAD_COMMITTED:
            /* 60% */
            gtk_progress_bar_set_fraction(
                GTK_PROGRESS_BAR(data->progress_bar), 0.6);
            gtk_label_set_text(GTK_LABEL(data->status_label),
                              "Rendering...");
            break;

        case BLINK_LOAD_FINISHED:
            /* 100% */
            gtk_progress_bar_set_fraction(
                GTK_PROGRESS_BAR(data->progress_bar), 1.0);
            gtk_label_set_text(GTK_LABEL(data->status_label), "Done");

            /* Hide progress bar after 1 second */
            g_timeout_add(1000, (GSourceFunc)gtk_widget_hide,
                         data->progress_bar);
            break;
    }
}

static void on_activate(GtkApplication* app, gpointer user_data) {
    ProgressData* data = g_new0(ProgressData, 1);

    GtkWidget* window = gtk_application_window_new(app);
    GtkWidget* web_view = blink_web_view_new();

    data->progress_bar = gtk_progress_bar_new();
    data->status_label = gtk_label_new("Ready");

    gtk_widget_set_visible(data->progress_bar, FALSE);

    GtkWidget* vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_box_append(GTK_BOX(vbox), data->progress_bar);
    gtk_box_append(GTK_BOX(vbox), data->status_label);
    gtk_box_append(GTK_BOX(vbox), web_view);

    /* Connect signal */
    g_signal_connect(web_view, "load-changed",
                     G_CALLBACK(on_load_changed), data);

    gtk_window_set_child(GTK_WINDOW(window), vbox);
    gtk_window_set_default_size(GTK_WINDOW(window), 800, 600);

    blink_web_view_load_uri(BLINK_WEB_VIEW(web_view), "https://www.chromium.org");
    gtk_window_present(GTK_WINDOW(window));
}

int main(int argc, char* argv[]) {
    blink_gtk_init(&argc, &argv);  /* GTK4 は引数なし */

    GtkApplication* app = gtk_application_new("org.example.progress", 0);
    g_signal_connect(app, "activate", G_CALLBACK(on_activate), NULL);

    /* Note g_application_run() は使用禁止 — Chromium RunLoop が飢餓状態になる
     * g_application_register/activate で GtkApplication シグナルを発火させてから
     * blink_gtk_run_main_loop() でChromium RunLoop を起動する */
    g_application_register(G_APPLICATION(app), NULL, NULL);
    g_application_activate(G_APPLICATION(app));
    int status = blink_gtk_run_main_loop();

    g_object_unref(app);
    blink_gtk_shutdown();
    return status;
}

Behavior:

  1. Load start: Show progress bar, 0%
  2. Redirect: 30%
  3. Commit: 60%
  4. Finish: 100%, hide after 1 second

Example 2: Error Handling

Display error dialog with load-failed signal:

#include <gtk/gtk.h>
#include <blink_gtk/blink_gtk.h>

static void on_load_failed(GtkWidget* web_view,
                          const char* failing_url,
                          int error_code,
                          gpointer user_data) {
    GtkWindow* window = GTK_WINDOW(user_data);

    /* Generate error message */
    const char* error_message = "Unknown error";

    switch (error_code) {
        case -2:
            error_message = "Failed to load page";
            break;
        case -7:
            error_message = "Connection timed out";
            break;
        case -105:
            error_message = "Server not found";
            break;
        case -106:
            error_message = "No internet connection";
            break;
    }

    /* Show error dialog */
    GtkWidget* dialog = gtk_message_dialog_new(
        window,
        GTK_DIALOG_MODAL,
        GTK_MESSAGE_ERROR,
        GTK_BUTTONS_OK,
        "Loading Error"
    );

    gtk_message_dialog_format_secondary_text(
        GTK_MESSAGE_DIALOG(dialog),
        "%s\n\nURL: %s\nError code: %d",
        error_message, failing_url, error_code
    );

    g_signal_connect(dialog, "response",
                     G_CALLBACK(gtk_window_destroy), NULL);

    gtk_widget_show(dialog);
}

static void on_activate(GtkApplication* app, gpointer user_data) {
    GtkWidget* window = gtk_application_window_new(app);
    GtkWidget* web_view = blink_web_view_new();

    /* Connect signal */
    g_signal_connect(web_view, "load-failed",
                     G_CALLBACK(on_load_failed), window);

    gtk_window_set_child(GTK_WINDOW(window), web_view);
    gtk_window_set_default_size(GTK_WINDOW(window), 800, 600);

    /* Test with non-existent URL */
    blink_web_view_load_uri(BLINK_WEB_VIEW(web_view), "https://this-does-not-exist.example");
    gtk_window_present(GTK_WINDOW(window));
}

int main(int argc, char* argv[]) {
    blink_gtk_init(&argc, &argv);  /* GTK4 は引数なし */

    GtkApplication* app = gtk_application_new("org.example.error", 0);
    g_signal_connect(app, "activate", G_CALLBACK(on_activate), NULL);

    /* Note g_application_run() は使用禁止 — Chromium RunLoop が飢餓状態になる
     * g_application_register/activate で GtkApplication シグナルを発火させてから
     * blink_gtk_run_main_loop() でChromium RunLoop を起動する */
    g_application_register(G_APPLICATION(app), NULL, NULL);
    g_application_activate(G_APPLICATION(app));
    int status = blink_gtk_run_main_loop();

    g_object_unref(app);
    blink_gtk_shutdown();
    return status;
}

Example 3: Window Title Update

Update window title with title-changed signal:

static void on_title_changed(GtkWidget* web_view,
                            const char* title,
                            gpointer user_data) {
    GtkWindow* window = GTK_WINDOW(user_data);

    /* "Title - AppName" format */
    char* window_title = g_strdup_printf("%s - MyBrowser", title);
    gtk_window_set_title(window, window_title);
    g_free(window_title);

    g_print("Title changed: %s\n", title);
}

static void on_activate(GtkApplication* app, gpointer user_data) {
    GtkWidget* window = gtk_application_window_new(app);
    GtkWidget* web_view = blink_web_view_new();

    gtk_window_set_title(GTK_WINDOW(window), "MyBrowser");

    /* Connect signal */
    g_signal_connect(web_view, "title-changed",
                     G_CALLBACK(on_title_changed), window);

    gtk_window_set_child(GTK_WINDOW(window), web_view);
    gtk_window_set_default_size(GTK_WINDOW(window), 800, 600);

    blink_web_view_load_uri(BLINK_WEB_VIEW(web_view), "https://example.com");
    gtk_window_present(GTK_WINDOW(window));
}

Intermediate - Combining Multiple Signals

Building Browser UI

Practical browser UI combining multiple signals:

#include <gtk/gtk.h>
#include <blink_gtk/blink_gtk.h>

/* Application data */
typedef struct {
    GtkWidget* window;
    GtkWidget* url_entry;
    GtkWidget* back_button;
    GtkWidget* forward_button;
    GtkWidget* reload_button;
    GtkWidget* stop_button;
    GtkWidget* progress_bar;
    GtkWidget* status_label;
    GtkWidget* web_view;
} BrowserUI;

/* load-changed: Update UI based on load state */
static void on_load_changed(GtkWidget* web_view,
                            BlinkLoadEvent load_event,
                            gpointer user_data) {
    BrowserUI* ui = (BrowserUI*)user_data;

    switch (load_event) {
        case BLINK_LOAD_STARTED:
            /* Show progress bar */
            gtk_progress_bar_set_fraction(
                GTK_PROGRESS_BAR(ui->progress_bar), 0.0);
            gtk_widget_set_visible(ui->progress_bar, TRUE);

            /* Show stop button, hide reload button */
            gtk_widget_set_visible(ui->stop_button, TRUE);
            gtk_widget_set_visible(ui->reload_button, FALSE);

            gtk_label_set_text(GTK_LABEL(ui->status_label), "Loading...");
            break;

        case BLINK_LOAD_COMMITTED:
            gtk_progress_bar_set_fraction(
                GTK_PROGRESS_BAR(ui->progress_bar), 0.5);
            break;

        case BLINK_LOAD_FINISHED: {
            gtk_progress_bar_set_fraction(
                GTK_PROGRESS_BAR(ui->progress_bar), 1.0);
            gtk_widget_set_visible(ui->progress_bar, FALSE);

            /* Show reload button, hide stop button */
            gtk_widget_set_visible(ui->stop_button, FALSE);
            gtk_widget_set_visible(ui->reload_button, TRUE);

            gtk_label_set_text(GTK_LABEL(ui->status_label), "Done");

            /* Update navigation button states */
            gboolean can_go_back = blink_web_view_can_go_back(BLINK_WEB_VIEW(web_view));
            gboolean can_go_forward = blink_web_view_can_go_forward(BLINK_WEB_VIEW(web_view));

            gtk_widget_set_sensitive(ui->back_button, can_go_back);
            gtk_widget_set_sensitive(ui->forward_button, can_go_forward);
            break;
        }

        default:
            break;
    }
}

/* load-failed: Show error */
static void on_load_failed(GtkWidget* web_view,
                          const char* failing_url,
                          int error_code,
                          gpointer user_data) {
    BrowserUI* ui = (BrowserUI*)user_data;

    char* message = g_strdup_printf("Error (code: %d)", error_code);
    gtk_label_set_text(GTK_LABEL(ui->status_label), message);
    g_free(message);

    gtk_widget_set_visible(ui->progress_bar, FALSE);
    gtk_widget_set_visible(ui->stop_button, FALSE);
    gtk_widget_set_visible(ui->reload_button, TRUE);
}

/* title-changed: Update window title */
static void on_title_changed(GtkWidget* web_view,
                            const char* title,
                            gpointer user_data) {
    BrowserUI* ui = (BrowserUI*)user_data;

    char* window_title = g_strdup_printf("%s - MyBrowser", title);
    gtk_window_set_title(GTK_WINDOW(ui->window), window_title);
    g_free(window_title);
}

/* uri-changed: Update address bar */
static void on_uri_changed(GtkWidget* web_view,
                          const char* uri,
                          gpointer user_data) {
    BrowserUI* ui = (BrowserUI*)user_data;

    gtk_editable_set_text(GTK_EDITABLE(ui->url_entry), uri);
}

/* Button click handlers */
static void on_back_clicked(GtkButton* button, gpointer user_data) {
    BrowserUI* ui = (BrowserUI*)user_data;
    blink_web_view_go_back(BLINK_WEB_VIEW(ui->web_view));
}

static void on_forward_clicked(GtkButton* button, gpointer user_data) {
    BrowserUI* ui = (BrowserUI*)user_data;
    blink_web_view_go_forward(BLINK_WEB_VIEW(ui->web_view));
}

static void on_reload_clicked(GtkButton* button, gpointer user_data) {
    BrowserUI* ui = (BrowserUI*)user_data;
    blink_web_view_reload(BLINK_WEB_VIEW(ui->web_view));
}

static void on_stop_clicked(GtkButton* button, gpointer user_data) {
    BrowserUI* ui = (BrowserUI*)user_data;
    blink_web_view_stop(BLINK_WEB_VIEW(ui->web_view));
}

static void on_url_activate(GtkEntry* entry, gpointer user_data) {
    BrowserUI* ui = (BrowserUI*)user_data;
    const char* url = gtk_editable_get_text(GTK_EDITABLE(entry));
    blink_web_view_load_uri(BLINK_WEB_VIEW(ui->web_view), url);
}

/* Connect signals */
static void setup_signals(BrowserUI* ui) {
    /* WebView signals */
    g_signal_connect(ui->web_view, "load-changed",
                     G_CALLBACK(on_load_changed), ui);
    g_signal_connect(ui->web_view, "load-failed",
                     G_CALLBACK(on_load_failed), ui);
    g_signal_connect(ui->web_view, "title-changed",
                     G_CALLBACK(on_title_changed), ui);
    g_signal_connect(ui->web_view, "uri-changed",
                     G_CALLBACK(on_uri_changed), ui);

    /* Button signals */
    g_signal_connect(ui->back_button, "clicked",
                     G_CALLBACK(on_back_clicked), ui);
    g_signal_connect(ui->forward_button, "clicked",
                     G_CALLBACK(on_forward_clicked), ui);
    g_signal_connect(ui->reload_button, "clicked",
                     G_CALLBACK(on_reload_clicked), ui);
    g_signal_connect(ui->stop_button, "clicked",
                     G_CALLBACK(on_stop_clicked), ui);
    g_signal_connect(ui->url_entry, "activate",
                     G_CALLBACK(on_url_activate), ui);
}

static void on_activate(GtkApplication* app, gpointer user_data) {
    BrowserUI* ui = g_new0(BrowserUI, 1);

    /* Window */
    ui->window = gtk_application_window_new(app);
    gtk_window_set_title(GTK_WINDOW(ui->window), "MyBrowser");
    gtk_window_set_default_size(GTK_WINDOW(ui->window), 1024, 768);

    /* Toolbar */
    GtkWidget* toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
    gtk_widget_set_margin_start(toolbar, 5);
    gtk_widget_set_margin_end(toolbar, 5);
    gtk_widget_set_margin_top(toolbar, 5);
    gtk_widget_set_margin_bottom(toolbar, 5);

    ui->back_button = gtk_button_new_from_icon_name("go-previous");
    ui->forward_button = gtk_button_new_from_icon_name("go-next");
    ui->reload_button = gtk_button_new_from_icon_name("view-refresh");
    ui->stop_button = gtk_button_new_from_icon_name("process-stop");
    ui->url_entry = gtk_entry_new();

    gtk_widget_set_visible(ui->stop_button, FALSE);
    gtk_widget_set_sensitive(ui->back_button, FALSE);
    gtk_widget_set_sensitive(ui->forward_button, FALSE);

    gtk_widget_set_hexpand(ui->url_entry, TRUE);

    gtk_box_append(GTK_BOX(toolbar), ui->back_button);
    gtk_box_append(GTK_BOX(toolbar), ui->forward_button);
    gtk_box_append(GTK_BOX(toolbar), ui->reload_button);
    gtk_box_append(GTK_BOX(toolbar), ui->stop_button);
    gtk_box_append(GTK_BOX(toolbar), ui->url_entry);

    /* Progress bar */
    ui->progress_bar = gtk_progress_bar_new();
    gtk_widget_set_visible(ui->progress_bar, FALSE);

    /* Status bar */
    ui->status_label = gtk_label_new("Ready");
    gtk_widget_set_halign(ui->status_label, GTK_ALIGN_START);
    gtk_widget_set_margin_start(ui->status_label, 5);
    gtk_widget_set_margin_bottom(ui->status_label, 2);

    /* WebView */
    ui->web_view = blink_web_view_new();

    /* Layout */
    GtkWidget* vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_box_append(GTK_BOX(vbox), toolbar);
    gtk_box_append(GTK_BOX(vbox), ui->progress_bar);
    gtk_box_append(GTK_BOX(vbox), ui->web_view);
    gtk_box_append(GTK_BOX(vbox), ui->status_label);

    gtk_widget_set_vexpand(ui->web_view, TRUE);

    gtk_window_set_child(GTK_WINDOW(ui->window), vbox);

    /* Connect signals */
    setup_signals(ui);

    /* Initial URL */
    blink_web_view_load_uri(BLINK_WEB_VIEW(ui->web_view), "https://www.chromium.org");

    gtk_window_present(GTK_WINDOW(ui->window));
}

int main(int argc, char* argv[]) {
    blink_gtk_init(&argc, &argv);  /* GTK4 は引数なし */

    GtkApplication* app = gtk_application_new("org.example.browser", 0);
    g_signal_connect(app, "activate", G_CALLBACK(on_activate), NULL);

    /* Note g_application_run() は使用禁止 — Chromium RunLoop が飢餓状態になる
     * g_application_register/activate で GtkApplication シグナルを発火させてから
     * blink_gtk_run_main_loop() でChromium RunLoop を起動する */
    g_application_register(G_APPLICATION(app), NULL, NULL);
    g_application_activate(G_APPLICATION(app));
    int status = blink_gtk_run_main_loop();

    g_object_unref(app);
    blink_gtk_shutdown();
    return status;
}

Compile:

gcc browser-ui.c $(pkg-config --cflags --libs blinkgtk-0.1) -o browser-ui
./browser-ui

Features:


Advanced - Complete Browser Application

Feature-Rich Browser

Complete browser with Settings API integration:

#include <gtk/gtk.h>
#include <blink_gtk/blink_gtk.h>

typedef struct {
    GtkWidget* window;
    GtkWidget* url_entry;
    GtkWidget* back_button;
    GtkWidget* forward_button;
    GtkWidget* reload_button;
    GtkWidget* stop_button;
    GtkWidget* progress_bar;
    GtkWidget* status_label;
    GtkWidget* security_icon;
    GtkWidget* web_view;

    /* Settings */
    GtkWidget* js_switch;
    GtkWidget* images_switch;
    GtkWidget* storage_switch;
} AdvancedBrowserUI;

/* load-changed */
static void on_load_changed(GtkWidget* web_view,
                            BlinkLoadEvent load_event,
                            gpointer user_data) {
    AdvancedBrowserUI* ui = (AdvancedBrowserUI*)user_data;

    switch (load_event) {
        case BLINK_LOAD_STARTED:
            gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ui->progress_bar), 0.0);
            gtk_widget_set_visible(ui->progress_bar, TRUE);
            gtk_widget_set_visible(ui->stop_button, TRUE);
            gtk_widget_set_visible(ui->reload_button, FALSE);
            gtk_label_set_text(GTK_LABEL(ui->status_label), "Loading...");
            break;

        case BLINK_LOAD_COMMITTED:
            gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ui->progress_bar), 0.5);
            break;

        case BLINK_LOAD_FINISHED:
            gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ui->progress_bar), 1.0);
            gtk_widget_set_visible(ui->progress_bar, FALSE);
            gtk_widget_set_visible(ui->stop_button, FALSE);
            gtk_widget_set_visible(ui->reload_button, TRUE);
            gtk_label_set_text(GTK_LABEL(ui->status_label), "Done");

            gboolean can_back = blink_web_view_can_go_back(BLINK_WEB_VIEW(web_view));
            gboolean can_forward = blink_web_view_can_go_forward(BLINK_WEB_VIEW(web_view));
            gtk_widget_set_sensitive(ui->back_button, can_back);
            gtk_widget_set_sensitive(ui->forward_button, can_forward);
            break;

        default:
            break;
    }
}

/* load-failed */
static void on_load_failed(GtkWidget* web_view,
                          const char* failing_url,
                          int error_code,
                          gpointer user_data) {
    AdvancedBrowserUI* ui = (AdvancedBrowserUI*)user_data;

    const char* error_msg = "Unknown error";
    switch (error_code) {
        case -2: error_msg = "Loading failed"; break;
        case -7: error_msg = "Timed out"; break;
        case -105: error_msg = "Server not found"; break;
        case -106: error_msg = "Internet disconnected"; break;
    }

    char* message = g_strdup_printf("%s (code: %d)", error_msg, error_code);
    gtk_label_set_text(GTK_LABEL(ui->status_label), message);
    g_free(message);

    /* Error dialog */
    GtkWidget* dialog = gtk_message_dialog_new(
        GTK_WINDOW(ui->window),
        GTK_DIALOG_MODAL,
        GTK_MESSAGE_ERROR,
        GTK_BUTTONS_OK,
        "Loading Error"
    );
    gtk_message_dialog_format_secondary_text(
        GTK_MESSAGE_DIALOG(dialog),
        "%s\n\nURL: %s\nError code: %d",
        error_msg, failing_url, error_code
    );
    g_signal_connect(dialog, "response", G_CALLBACK(gtk_window_destroy), NULL);
    gtk_widget_show(dialog);

    gtk_widget_set_visible(ui->progress_bar, FALSE);
    gtk_widget_set_visible(ui->stop_button, FALSE);
    gtk_widget_set_visible(ui->reload_button, TRUE);
}

/* title-changed */
static void on_title_changed(GtkWidget* web_view,
                            const char* title,
                            gpointer user_data) {
    AdvancedBrowserUI* ui = (AdvancedBrowserUI*)user_data;
    char* window_title = g_strdup_printf("%s - AdvancedBrowser", title);
    gtk_window_set_title(GTK_WINDOW(ui->window), window_title);
    g_free(window_title);
}

/* uri-changed */
static void on_uri_changed(GtkWidget* web_view,
                          const char* uri,
                          gpointer user_data) {
    AdvancedBrowserUI* ui = (AdvancedBrowserUI*)user_data;

    /* Update address bar */
    gtk_editable_set_text(GTK_EDITABLE(ui->url_entry), uri);

    /* Update security icon */
    if (g_str_has_prefix(uri, "https://")) {
        gtk_image_set_from_icon_name(GTK_IMAGE(ui->security_icon),
                                     "dialog-password");
        gtk_widget_set_tooltip_text(ui->security_icon,
                                    "This connection is secure");
    } else {
        gtk_image_set_from_icon_name(GTK_IMAGE(ui->security_icon),
                                     "dialog-warning");
        gtk_widget_set_tooltip_text(ui->security_icon,
                                    "This connection is not secure");
    }
}

/* Settings changes */
static void on_js_switch_changed(GtkSwitch* widget, GParamSpec* pspec,
                                gpointer user_data) {
    AdvancedBrowserUI* ui = (AdvancedBrowserUI*)user_data;
    gboolean active = gtk_switch_get_active(widget);
    blink_web_view_set_javascript_enabled(BLINK_WEB_VIEW(ui->web_view), active);
    g_print("JavaScript: %s\n", active ? "enabled" : "disabled");
}

static void on_images_switch_changed(GtkSwitch* widget, GParamSpec* pspec,
                                    gpointer user_data) {
    AdvancedBrowserUI* ui = (AdvancedBrowserUI*)user_data;
    gboolean active = gtk_switch_get_active(widget);
    blink_web_view_set_images_enabled(BLINK_WEB_VIEW(ui->web_view), active);
    g_print("Image loading: %s\n", active ? "enabled" : "disabled");
}

static void on_storage_switch_changed(GtkSwitch* widget, GParamSpec* pspec,
                                     gpointer user_data) {
    AdvancedBrowserUI* ui = (AdvancedBrowserUI*)user_data;
    gboolean active = gtk_switch_get_active(widget);
    blink_web_view_set_local_storage_enabled(BLINK_WEB_VIEW(ui->web_view), active);
    g_print("Local storage: %s\n", active ? "enabled" : "disabled");
}

/* Button handlers */
static void on_back_clicked(GtkButton* button, gpointer user_data) {
    AdvancedBrowserUI* ui = (AdvancedBrowserUI*)user_data;
    blink_web_view_go_back(BLINK_WEB_VIEW(ui->web_view));
}

static void on_forward_clicked(GtkButton* button, gpointer user_data) {
    AdvancedBrowserUI* ui = (AdvancedBrowserUI*)user_data;
    blink_web_view_go_forward(BLINK_WEB_VIEW(ui->web_view));
}

static void on_reload_clicked(GtkButton* button, gpointer user_data) {
    AdvancedBrowserUI* ui = (AdvancedBrowserUI*)user_data;
    blink_web_view_reload(BLINK_WEB_VIEW(ui->web_view));
}

static void on_stop_clicked(GtkButton* button, gpointer user_data) {
    AdvancedBrowserUI* ui = (AdvancedBrowserUI*)user_data;
    blink_web_view_stop(BLINK_WEB_VIEW(ui->web_view));
}

static void on_url_activate(GtkEntry* entry, gpointer user_data) {
    AdvancedBrowserUI* ui = (AdvancedBrowserUI*)user_data;
    const char* url = gtk_editable_get_text(GTK_EDITABLE(entry));
    blink_web_view_load_uri(BLINK_WEB_VIEW(ui->web_view), url);
}

/* Signal connection */
static void setup_signals(AdvancedBrowserUI* ui) {
    /* WebView signals */
    g_signal_connect(ui->web_view, "load-changed",
                     G_CALLBACK(on_load_changed), ui);
    g_signal_connect(ui->web_view, "load-failed",
                     G_CALLBACK(on_load_failed), ui);
    g_signal_connect(ui->web_view, "title-changed",
                     G_CALLBACK(on_title_changed), ui);
    g_signal_connect(ui->web_view, "uri-changed",
                     G_CALLBACK(on_uri_changed), ui);

    /* Buttons */
    g_signal_connect(ui->back_button, "clicked",
                     G_CALLBACK(on_back_clicked), ui);
    g_signal_connect(ui->forward_button, "clicked",
                     G_CALLBACK(on_forward_clicked), ui);
    g_signal_connect(ui->reload_button, "clicked",
                     G_CALLBACK(on_reload_clicked), ui);
    g_signal_connect(ui->stop_button, "clicked",
                     G_CALLBACK(on_stop_clicked), ui);
    g_signal_connect(ui->url_entry, "activate",
                     G_CALLBACK(on_url_activate), ui);

    /* Settings */
    g_signal_connect(ui->js_switch, "notify::active",
                     G_CALLBACK(on_js_switch_changed), ui);
    g_signal_connect(ui->images_switch, "notify::active",
                     G_CALLBACK(on_images_switch_changed), ui);
    g_signal_connect(ui->storage_switch, "notify::active",
                     G_CALLBACK(on_storage_switch_changed), ui);
}

static void on_activate(GtkApplication* app, gpointer user_data) {
    AdvancedBrowserUI* ui = g_new0(AdvancedBrowserUI, 1);

    /* Window */
    ui->window = gtk_application_window_new(app);
    gtk_window_set_title(GTK_WINDOW(ui->window), "AdvancedBrowser");
    gtk_window_set_default_size(GTK_WINDOW(ui->window), 1200, 800);

    /* Toolbar */
    GtkWidget* toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
    gtk_widget_set_margin_start(toolbar, 5);
    gtk_widget_set_margin_end(toolbar, 5);
    gtk_widget_set_margin_top(toolbar, 5);
    gtk_widget_set_margin_bottom(toolbar, 5);

    ui->back_button = gtk_button_new_from_icon_name("go-previous");
    ui->forward_button = gtk_button_new_from_icon_name("go-next");
    ui->reload_button = gtk_button_new_from_icon_name("view-refresh");
    ui->stop_button = gtk_button_new_from_icon_name("process-stop");
    ui->security_icon = gtk_image_new_from_icon_name("dialog-warning");
    ui->url_entry = gtk_entry_new();

    gtk_widget_set_visible(ui->stop_button, FALSE);
    gtk_widget_set_sensitive(ui->back_button, FALSE);
    gtk_widget_set_sensitive(ui->forward_button, FALSE);
    gtk_widget_set_hexpand(ui->url_entry, TRUE);

    gtk_box_append(GTK_BOX(toolbar), ui->back_button);
    gtk_box_append(GTK_BOX(toolbar), ui->forward_button);
    gtk_box_append(GTK_BOX(toolbar), ui->reload_button);
    gtk_box_append(GTK_BOX(toolbar), ui->stop_button);
    gtk_box_append(GTK_BOX(toolbar), ui->security_icon);
    gtk_box_append(GTK_BOX(toolbar), ui->url_entry);

    /* Settings panel */
    GtkWidget* settings_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
    gtk_widget_set_margin_start(settings_box, 5);
    gtk_widget_set_margin_end(settings_box, 5);
    gtk_widget_set_margin_bottom(settings_box, 5);

    ui->js_switch = gtk_switch_new();
    ui->images_switch = gtk_switch_new();
    ui->storage_switch = gtk_switch_new();

    gtk_switch_set_active(GTK_SWITCH(ui->js_switch), TRUE);
    gtk_switch_set_active(GTK_SWITCH(ui->images_switch), TRUE);
    gtk_switch_set_active(GTK_SWITCH(ui->storage_switch), TRUE);

    gtk_box_append(GTK_BOX(settings_box), gtk_label_new("JavaScript:"));
    gtk_box_append(GTK_BOX(settings_box), ui->js_switch);
    gtk_box_append(GTK_BOX(settings_box), gtk_label_new("Images:"));
    gtk_box_append(GTK_BOX(settings_box), ui->images_switch);
    gtk_box_append(GTK_BOX(settings_box), gtk_label_new("Storage:"));
    gtk_box_append(GTK_BOX(settings_box), ui->storage_switch);

    /* Progress bar */
    ui->progress_bar = gtk_progress_bar_new();
    gtk_widget_set_visible(ui->progress_bar, FALSE);

    /* Status bar */
    ui->status_label = gtk_label_new("Ready");
    gtk_widget_set_halign(ui->status_label, GTK_ALIGN_START);
    gtk_widget_set_margin_start(ui->status_label, 5);
    gtk_widget_set_margin_bottom(ui->status_label, 2);

    /* WebView */
    ui->web_view = blink_web_view_new();

    /* Layout */
    GtkWidget* vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_box_append(GTK_BOX(vbox), toolbar);
    gtk_box_append(GTK_BOX(vbox), settings_box);
    gtk_box_append(GTK_BOX(vbox), ui->progress_bar);
    gtk_box_append(GTK_BOX(vbox), ui->web_view);
    gtk_box_append(GTK_BOX(vbox), ui->status_label);

    gtk_widget_set_vexpand(ui->web_view, TRUE);

    gtk_window_set_child(GTK_WINDOW(ui->window), vbox);

    /* Connect signals */
    setup_signals(ui);

    /* Initial URL */
    blink_web_view_load_uri(BLINK_WEB_VIEW(ui->web_view), "https://www.chromium.org");

    gtk_window_present(GTK_WINDOW(ui->window));
}

int main(int argc, char* argv[]) {
    blink_gtk_init(&argc, &argv);  /* GTK4 は引数なし */

    GtkApplication* app = gtk_application_new("org.example.advanced", 0);
    g_signal_connect(app, "activate", G_CALLBACK(on_activate), NULL);

    /* Note g_application_run() は使用禁止 — Chromium RunLoop が飢餓状態になる
     * g_application_register/activate で GtkApplication シグナルを発火させてから
     * blink_gtk_run_main_loop() でChromium RunLoop を起動する */
    g_application_register(G_APPLICATION(app), NULL, NULL);
    g_application_activate(G_APPLICATION(app));
    int status = blink_gtk_run_main_loop();

    g_object_unref(app);
    blink_gtk_shutdown();
    return status;
}

Compile:

gcc advanced-browser.c $(pkg-config --cflags --libs blinkgtk-0.1) -o advanced-browser
./advanced-browser

Features:


Pattern Collection

Pattern 1: Loading Indicator

Simple spinner display:

typedef struct {
    GtkWidget* spinner;
    GtkWidget* web_view;
} SpinnerData;

static void on_load_changed(GtkWidget* web_view,
                            BlinkLoadEvent load_event,
                            gpointer user_data) {
    SpinnerData* data = (SpinnerData*)user_data;

    if (load_event == BLINK_LOAD_STARTED) {
        gtk_spinner_start(GTK_SPINNER(data->spinner));
        gtk_widget_set_visible(data->spinner, TRUE);
    } else if (load_event == BLINK_LOAD_FINISHED) {
        gtk_spinner_stop(GTK_SPINNER(data->spinner));
        gtk_widget_set_visible(data->spinner, FALSE);
    }
}

Pattern 2: History Management

Simple history list:

typedef struct {
    GList* history;  // String list
    int current_index;
} HistoryManager;

static void on_uri_changed(GtkWidget* web_view,
                          const char* uri,
                          gpointer user_data) {
    HistoryManager* history = (HistoryManager*)user_data;

    /* Remove forward history */
    while (history->current_index < g_list_length(history->history) - 1) {
        GList* last = g_list_last(history->history);
        g_free(last->data);
        history->history = g_list_delete_link(history->history, last);
    }

    /* Add new URI */
    history->history = g_list_append(history->history, g_strdup(uri));
    history->current_index = g_list_length(history->history) - 1;
}

Pattern 3: Title and Favicon

Title and simple favicon display:

typedef struct {
    GtkWidget* tab_label;
    GtkWidget* tab_icon;
} TabData;

static void on_title_changed(GtkWidget* web_view,
                            const char* title,
                            gpointer user_data) {
    TabData* tab = (TabData*)user_data;

    /* Truncate long titles */
    char* display_title;
    if (strlen(title) > 20) {
        display_title = g_strdup_printf("%.17s...", title);
    } else {
        display_title = g_strdup(title);
    }

    gtk_label_set_text(GTK_LABEL(tab->tab_label), display_title);
    g_free(display_title);
}

static void on_uri_changed(GtkWidget* web_view,
                          const char* uri,
                          gpointer user_data) {
    TabData* tab = (TabData*)user_data;

    /* Lock icon for HTTPS pages */
    if (g_str_has_prefix(uri, "https://")) {
        gtk_image_set_from_icon_name(GTK_IMAGE(tab->tab_icon),
                                     "dialog-password");
    } else {
        gtk_image_set_from_icon_name(GTK_IMAGE(tab->tab_icon),
                                     "text-html");
    }
}

Pattern 4: Custom Error Page

Display error page with HTML:

static void on_load_failed(GtkWidget* web_view,
                          const char* failing_url,
                          int error_code,
                          gpointer user_data) {
    char* error_html = g_strdup_printf(
        "<!DOCTYPE html>"
        "<html>"
        "<head>"
        "  <meta charset='UTF-8'>"
        "  <title>Error</title>"
        "  <style>"
        "    body {"
        "      font-family: sans-serif;"
        "      text-align: center;"
        "      padding: 50px;"
        "      background: #f5f5f5;"
        "    }"
        "    h1 { color: #d32f2f; }"
        "    .error-code { color: #666; font-family: monospace; }"
        "    button {"
        "      padding: 10px 20px;"
        "      font-size: 16px;"
        "      background: #1976d2;"
        "      color: white;"
        "      border: none;"
        "      border-radius: 4px;"
        "      cursor: pointer;"
        "    }"
        "    button:hover { background: #1565c0; }"
        "  </style>"
        "</head>"
        "<body>"
        "  <h1>Unable to Load Page</h1>"
        "  <p>Failed to load the following URL:</p>"
        "  <p class='error-code'>%s</p>"
        "  <p>Error code: <span class='error-code'>%d</span></p>"
        "  <button onclick='location.href=\"%s\"'>Retry</button>"
        "</body>"
        "</html>",
        failing_url, error_code, failing_url
    );

    blink_web_view_load_html(BLINK_WEB_VIEW(web_view), error_html, "about:error");
    g_free(error_html);
}

Pattern 5: Download Notification

Simple download detection (using load-failed):

static void on_load_failed(GtkWidget* web_view,
                          const char* failing_url,
                          int error_code,
                          gpointer user_data) {
    /* Detect downloadable file types */
    if (g_str_has_suffix(failing_url, ".pdf") || g_str_has_suffix(failing_url, ".zip") || g_str_has_suffix(failing_url, ".tar.gz")) {

        GtkWidget* dialog = gtk_message_dialog_new(
            NULL,
            GTK_DIALOG_MODAL,
            GTK_MESSAGE_QUESTION,
            GTK_BUTTONS_YES_NO,
            "Download"
        );

        gtk_message_dialog_format_secondary_text(
            GTK_MESSAGE_DIALOG(dialog),
            "Do you want to download this file?\n\n%s",
            failing_url
        );

        int response = gtk_dialog_run(GTK_DIALOG(dialog));
        gtk_widget_destroy(dialog);

        if (response == GTK_RESPONSE_YES) {
            g_print("Download started: %s\n", failing_url);
            /* Actual download implementation */
        }
    }
}

Troubleshooting

Signals Not Firing

Symptom: Signal handler not being called

Causes and Solutions:

  1. Signal name typo

    // No Wrong
    g_signal_connect(web_view, "load_changed", ...);  // Underscore
    
    // Yes Correct
    g_signal_connect(web_view, "load-changed", ...);  // Hyphen
  2. Connecting before WebView initialization

    // No Wrong
    g_signal_connect(web_view, "load-changed", ...);
    web_view = blink_web_view_new();
    
    // Yes Correct
    web_view = blink_web_view_new();
    g_signal_connect(web_view, "load-changed", ...);
  3. GTK main loop not running

    blink_web_view_load_uri(BLINK_WEB_VIEW(web_view), "https://example.com");
    // No Signals won't fire (no main loop)
    
    blink_gtk_run_main_loop();  // Yes Start main loop (g_application_run() is forbidden)

user_data is NULL (Crashes)

Symptom: Segmentation fault in signal handler

Solution:

static void on_load_changed(GtkWidget* web_view,
                            BlinkLoadEvent load_event,
                            gpointer user_data) {
    /* NULL check */
    if (user_data == NULL) {
        g_warning("user_data is NULL");
        return;
    }

    MyData* data = (MyData*)user_data;
    /* ... */
}

Memory Leaks

Symptom: Memory usage keeps growing

Causes and Solutions:

  1. String leaks

    /* No Memory leak */
    const char* ua = blink_web_view_get_user_agent(web_view);
    
    /* Yes Correct */
    char* ua = blink_web_view_get_user_agent(web_view);
    g_print("UA: %s\n", ua);
    g_free(ua);  // Required
  2. Signal handler not disconnected

    /* Save handler ID */
    gulong handler_id = g_signal_connect(web_view, "load-changed",
                                         G_CALLBACK(on_load_changed), NULL);
    
    /* Disconnect before destroying WebView */
    g_signal_handler_disconnect(web_view, handler_id);

Progress Bar Not Updating

Symptom: Progress bar stays at 0%

Solution:

static void on_load_changed(GtkWidget* web_view,
                            BlinkLoadEvent load_event,
                            gpointer user_data) {
    ProgressData* data = (ProgressData*)user_data;

    /* Handle all events */
    switch (load_event) {
        case BLINK_LOAD_STARTED:
            gtk_progress_bar_set_fraction(
                GTK_PROGRESS_BAR(data->progress_bar), 0.0);
            gtk_widget_set_visible(data->progress_bar, TRUE);  // Don't forget to show
            break;

        case BLINK_LOAD_COMMITTED:
            gtk_progress_bar_set_fraction(
                GTK_PROGRESS_BAR(data->progress_bar), 0.5);
            break;

        case BLINK_LOAD_FINISHED:
            gtk_progress_bar_set_fraction(
                GTK_PROGRESS_BAR(data->progress_bar), 1.0);
            break;
    }
}

Summary

This tutorial covered event-driven programming with BlinkGTK's GObject signal system.

What You Learned

Next Steps

Sample Code

Complete sample code is available in docs/04-user-guides/examples/:


Author: BlinkGTK Development Team
License: BSD 3-Clause
Feedback: daisy19@gmail.com