BlinkGTK イベント処理チュートリアル (C言語)

最終更新: 2026-07-29
対象バージョン: BlinkGTK v1.1.0 以降
レベル: 初級〜上級
言語: 日本語 |

English


目次

  1. はじめに
  2. 基礎編 - シグナル接続の基本
  3. 初級編 - 単一シグナルの活用
  4. 中級編 - 複数シグナルの組み合わせ
  5. 上級編 - 完全なブラウザアプリケーション
  6. パターン集
  7. トラブルシューティング

はじめに

このチュートリアルでは、BlinkGTKのGObjectシグナルシステムを使用したイベント駆動プログラミングを学びます。

学習内容

前提知識

必要な環境

# 必要なパッケージ
- gtk4
- blinkgtk

# コンパイル例
gcc your-program.c $(pkg-config --cflags --libs blinkgtk-0.1) -o your-program

基礎編 - シグナル接続の基本

GObjectシグナルとは

GObjectシグナルは、GTK/GLibのイベント駆動プログラミングの中核です:

BlinkGTKのシグナル:
| シグナル名 | 発火タイミング | 用途 |
|-----------|--------------|------|
| load-changed | ロード状態変化 | プログレス表示 |
| load-failed | ロード失敗 | エラー処理 |
| title-changed | タイトル変更 | ウィンドウタイトル更新 |
| uri-changed | URI変更 | アドレスバー更新 |

最初のシグナルハンドラ

最もシンプルな例から始めましょう:

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

/* シグナルハンドラ */
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 = "読み込み開始";
            break;
        case BLINK_LOAD_REDIRECTED:
            event_name = "リダイレクト";
            break;
        case BLINK_LOAD_COMMITTED:
            event_name = "コミット";
            break;
        case BLINK_LOAD_FINISHED:
            event_name = "読み込み完了";
            break;
    }

    g_print("イベント: %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();

    /* シグナル接続 */
    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);

    /* 注意 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;
}

コンパイルと実行:

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

出力例:

イベント: 読み込み開始
イベント: コミット
イベント: 読み込み完了

user_dataの活用

シグナルハンドラに追加データを渡す方法:

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

/* アプリケーションデータ */
typedef struct {
    GtkWidget* window;
    GtkWidget* status_label;
    int load_count;
} AppData;

/* シグナルハンドラ */
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),
                              "読み込み中...");
            break;

        case BLINK_LOAD_FINISHED:
            data->load_count++;
            char* message = g_strdup_printf("完了 (%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("準備完了");

    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);

    /* シグナル接続(dataを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);

    /* 注意 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;
}

ポイント:


初級編 - 単一シグナルの活用

実用例1: プログレスバー表示

load-changedシグナルでプログレスバーを更新:

#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),
                              "読み込み開始...");
            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),
                              "リダイレクト中...");
            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),
                              "レンダリング中...");
            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), "完了");

            /* 1秒後にプログレスバーを非表示 */
            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("準備完了");

    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);

    /* シグナル接続 */
    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);

    /* 注意 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;
}

動作:

  1. ロード開始: プログレスバー表示、0%
  2. リダイレクト: 30%
  3. コミット: 60%
  4. 完了: 100%、1秒後に非表示

実用例2: エラーハンドリング

load-failedシグナルでエラーダイアログを表示:

#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);

    /* エラーメッセージを生成 */
    const char* error_message = "不明なエラー";

    switch (error_code) {
        case -2:
            error_message = "ページの読み込みに失敗しました";
            break;
        case -7:
            error_message = "接続がタイムアウトしました";
            break;
        case -105:
            error_message = "サーバーが見つかりませんでした";
            break;
        case -106:
            error_message = "インターネットに接続されていません";
            break;
    }

    /* エラーダイアログ表示 */
    GtkWidget* dialog = gtk_message_dialog_new(
        window,
        GTK_DIALOG_MODAL,
        GTK_MESSAGE_ERROR,
        GTK_BUTTONS_OK,
        "読み込みエラー"
    );

    gtk_message_dialog_format_secondary_text(
        GTK_MESSAGE_DIALOG(dialog),
        "%s\n\nURL: %s\nエラーコード: %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();

    /* シグナル接続 */
    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);

    /* 存在しない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);

    /* 注意 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;
}

実用例3: ウィンドウタイトル更新

title-changedシグナルでウィンドウタイトルを更新:

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

    /* "タイトル - アプリ名" 形式 */
    char* window_title = g_strdup_printf("%s - MyBrowser", title);
    gtk_window_set_title(window, window_title);
    g_free(window_title);

    g_print("タイトル変更: %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");

    /* シグナル接続 */
    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));
}

中級編 - 複数シグナルの組み合わせ

ブラウザUIの構築

複数のシグナルを組み合わせた実用的なブラウザUI:

#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* web_view;
} BrowserUI;

/* load-changed: ロード状態でUI更新 */
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:
            /* プログレスバー表示 */
            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), "読み込み中...");
            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), "完了");

            /* ナビゲーションボタンの状態更新 */
            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: エラー表示 */
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("エラー (コード: %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: ウィンドウタイトル更新 */
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: アドレスバー更新 */
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);
}

/* ボタンクリックハンドラ */
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);
}

/* シグナル接続 */
static void setup_signals(BrowserUI* ui) {
    /* WebViewシグナル */
    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);

    /* ボタンシグナル */
    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);

    /* ウィンドウ */
    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);

    /* ツールバー */
    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);

    /* プログレスバー */
    ui->progress_bar = gtk_progress_bar_new();
    gtk_widget_set_visible(ui->progress_bar, FALSE);

    /* ステータスバー */
    ui->status_label = gtk_label_new("準備完了");
    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();

    /* レイアウト */
    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);

    /* シグナル接続 */
    setup_signals(ui);

    /* 初期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);

    /* 注意 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;
}

コンパイル:

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

機能:


上級編 - 完全なブラウザアプリケーション

機能拡張版ブラウザ

Settings APIを統合した完全なブラウザ:

#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), "読み込み中...");
            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), "完了");

            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 = "不明なエラー";
    switch (error_code) {
        case -2: error_msg = "読み込み失敗"; break;
        case -7: error_msg = "タイムアウト"; break;
        case -105: error_msg = "サーバーが見つかりません"; break;
        case -106: error_msg = "インターネット未接続"; break;
    }

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

    /* エラーダイアログ */
    GtkWidget* dialog = gtk_message_dialog_new(
        GTK_WINDOW(ui->window),
        GTK_DIALOG_MODAL,
        GTK_MESSAGE_ERROR,
        GTK_BUTTONS_OK,
        "読み込みエラー"
    );
    gtk_message_dialog_format_secondary_text(
        GTK_MESSAGE_DIALOG(dialog),
        "%s\n\nURL: %s\nエラーコード: %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;

    /* アドレスバー更新 */
    gtk_editable_set_text(GTK_EDITABLE(ui->url_entry), uri);

    /* セキュリティアイコン更新 */
    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,
                                    "このページは安全です");
    } else {
        gtk_image_set_from_icon_name(GTK_IMAGE(ui->security_icon),
                                     "dialog-warning");
        gtk_widget_set_tooltip_text(ui->security_icon,
                                    "このページは保護されていません");
    }
}

/* Settings変更 */
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 ? "有効" : "無効");
}

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("画像読み込み: %s\n", active ? "有効" : "無効");
}

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("ローカルストレージ: %s\n", active ? "有効" : "無効");
}

/* ボタンハンドラ */
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);
}

/* シグナル接続 */
static void setup_signals(AdvancedBrowserUI* ui) {
    /* WebViewシグナル */
    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);

    /* ボタン */
    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);

    /* ウィンドウ */
    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);

    /* ツールバー */
    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 パネル */
    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("画像:"));
    gtk_box_append(GTK_BOX(settings_box), ui->images_switch);
    gtk_box_append(GTK_BOX(settings_box), gtk_label_new("ストレージ:"));
    gtk_box_append(GTK_BOX(settings_box), ui->storage_switch);

    /* プログレスバー */
    ui->progress_bar = gtk_progress_bar_new();
    gtk_widget_set_visible(ui->progress_bar, FALSE);

    /* ステータスバー */
    ui->status_label = gtk_label_new("準備完了");
    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();

    /* レイアウト */
    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);

    /* シグナル接続 */
    setup_signals(ui);

    /* 初期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);

    /* 注意 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;
}

コンパイル:

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

機能:


パターン集

パターン1: ローディングインジケーター

シンプルなスピナー表示:

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);
    }
}

パターン2: 履歴管理

シンプルな履歴リスト:

typedef struct {
    GList* history;  // 文字列リスト
    int current_index;
} HistoryManager;

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

    /* 現在位置より先の履歴を削除 */
    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);
    }

    /* 新しいURIを追加 */
    history->history = g_list_append(history->history, g_strdup(uri));
    history->current_index = g_list_length(history->history) - 1;
}

パターン3: タイトルと favicon

タイトルと簡易的なfavicon表示:

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;

    /* タイトルを省略表示 */
    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;

    /* HTTPSページにはロックアイコン */
    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");
    }
}

パターン4: カスタムエラーページ

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>エラー</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>ページを読み込めません</h1>"
        "  <p>次のURLの読み込みに失敗しました:</p>"
        "  <p class='error-code'>%s</p>"
        "  <p>エラーコード: <span class='error-code'>%d</span></p>"
        "  <button onclick='location.href=\"%s\"'>再試行</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);
}

パターン5: ダウンロード通知

簡易的なダウンロード検出(load-failedで代用):

static void on_load_failed(GtkWidget* web_view,
                          const char* failing_url,
                          int error_code,
                          gpointer user_data) {
    /* ダウンロード可能なファイルタイプを検出 */
    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,
            "ダウンロード"
        );

        gtk_message_dialog_format_secondary_text(
            GTK_MESSAGE_DIALOG(dialog),
            "このファイルをダウンロードしますか?\n\n%s",
            failing_url
        );

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

        if (response == GTK_RESPONSE_YES) {
            g_print("ダウンロード開始: %s\n", failing_url);
            /* 実際のダウンロード処理 */
        }
    }
}

トラブルシューティング

シグナルが発火しない

症状: シグナルハンドラが呼ばれない

原因と対策:

  1. シグナル名のタイプミス

    // 非対応 間違い
    g_signal_connect(web_view, "load_changed", ...);  // アンダースコア
    
    // 対応 正しい
    g_signal_connect(web_view, "load-changed", ...);  // ハイフン
  2. WebViewが初期化される前に接続

    // 非対応 間違い
    g_signal_connect(web_view, "load-changed", ...);
    web_view = blink_web_view_new();
    
    // 対応 正しい
    web_view = blink_web_view_new();
    g_signal_connect(web_view, "load-changed", ...);
  3. GTKメインループが動いていない

    blink_web_view_load_uri(BLINK_WEB_VIEW(web_view), "https://example.com");
    // 非対応 シグナルは発火しない(メインループがない)
    
    blink_gtk_run_main_loop();  // 対応 メインループ開始(g_application_run()は使用禁止)

user_dataがNULLでクラッシュ

症状: シグナルハンドラ内でSegmentation fault

対策:

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

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

メモリリーク

症状: メモリ使用量が増え続ける

原因と対策:

  1. 文字列のリーク

    /* 非対応 メモリリーク */
    const char* ua = blink_web_view_get_user_agent(web_view);
    
    /* 対応 正しい */
    char* ua = blink_web_view_get_user_agent(web_view);
    g_print("UA: %s\n", ua);
    g_free(ua);  // 必須
  2. シグナルハンドラが切断されていない

    /* ハンドラIDを保存 */
    gulong handler_id = g_signal_connect(web_view, "load-changed",
                                         G_CALLBACK(on_load_changed), NULL);
    
    /* WebView破棄前に切断 */
    g_signal_handler_disconnect(web_view, handler_id);

プログレスバーが更新されない

症状: プログレスバーが0%のまま

対策:

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:
            gtk_progress_bar_set_fraction(
                GTK_PROGRESS_BAR(data->progress_bar), 0.0);
            gtk_widget_set_visible(data->progress_bar, TRUE);  // 表示を忘れずに
            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;
    }
}

まとめ

このチュートリアルでは、BlinkGTKのGObjectシグナルシステムを使用したイベント駆動プログラミングを学びました。

学んだこと

次のステップ

サンプルコード

完全なサンプルコードはdocs/04-user-guides/examples/にあります:


作成者: BlinkGTK開発チーム
ライセンス: BSD 3-Clause
フィードバック: daisy19@gmail.com