C言語とGTK+3で表計算ソフトみたいな表を作る

735, 2023-11-05

目次

C言語とGTK+3で表計算ソフトみたいな表

C言語のGTK+3は非常に高速なGUIライブラリです。
そのため高速処理が必要なGUIアプリ制作に適しています。

今回はこのGTK+3で表計算ソフトみたいな表を作ってみたいと思います。
以下が実行風景です。

c-gtk3-table

ソースコード全文

ソースコードは以下になります。

/* GTK+3で表計算ソフトみたいな表。
 * Licence: MIT
 * Since: 2023-11-05
 */
#include <gtk/gtk.h>

// 上端と左端のラベルを含めた横幅と高さ
static const gint WIDTH = 4;
static const gint HEIGHT = 3;

// ウィジェットを配置するグリッド
static GtkWidget *grid;

// ドラッグしている間TRUEになるフラグ
static gboolean resizing = FALSE;

/**
 * GridでボタンをプレスしたらフラグをTRUEに。 
 */
static gboolean on_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data) {
    if (event->button == GDK_BUTTON_PRIMARY && event->type == GDK_BUTTON_PRESS) {
        resizing = TRUE;
    }
    return TRUE;
}

/**
 * GridでボタンをリリースしたらフラグをFALSEに。 
 */
static gboolean on_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data) {
    if (event->button == GDK_BUTTON_PRIMARY && event->type == GDK_BUTTON_RELEASE) {
        resizing = FALSE;
    }
    return TRUE;
}

/**
 * 上端か左端のラベルをドラッグしている間、ラベルのサイズを変更する。
 */
static gboolean on_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data) {
    if (!resizing) {
        return TRUE;
    }

    // event->x, event->y
    // マウスの下にあるウィジェットのXY座標からの相対座標

    // マウスの下にあるウィジェットのウィンドウ内の座標
    gint wx, wy;
    gdk_window_get_position(event->window, &wx, &wy);

    // デバッグ
    g_print("win_x %d wy %d\nevent->x %f event->y %f\n"
        , wx, wy, event->x, event->y);

    // 0行目x列目のラベルを調整
    for (gint x = 0; x < WIDTH; x++) {
        GtkAllocation alloc;
        GtkWidget *event_box = gtk_grid_get_child_at(GTK_GRID(grid), x, 0);
        if (event_box == NULL) {
            continue;
        }

        gtk_widget_get_allocation(event_box, &alloc);

        // ウィジェット内にマウスがあるかを判定
        if (wx >= alloc.x && wx < alloc.x + alloc.width &&
            wy >= alloc.y && wy < alloc.y + alloc.height) {
            g_print("resize\n");
            gtk_widget_set_size_request(event_box, event->x, -1);
        }
    }

    // y行目0列目のラベルを調整
    for (gint y = 0; y < HEIGHT; y++) {
        GtkAllocation alloc;
        GtkWidget *event_box = gtk_grid_get_child_at(GTK_GRID(grid), 0, y);
        if (event_box == NULL) {
            continue;
        }

        gtk_widget_get_allocation(event_box, &alloc);

        // ウィジェット内にマウスがあるかを判定
        if (wx >= alloc.x && wx < alloc.x + alloc.width &&
            wy >= alloc.y && wy < alloc.y + alloc.height) {
            g_print("resize\n");
            gtk_widget_set_size_request(event_box, -1, event->y);
        }
    }

    return TRUE;
}

int main(int argc, char *argv[]) {
    // GTKを初期化
    gtk_init(&argc, &argv);

    // ウィンドウを作成
    GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(window), "Table");
    g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    // グリッドを作成
    grid = gtk_grid_new();
    gtk_container_add(GTK_CONTAINER(window), grid);
    gtk_widget_set_margin_start(grid, 10);
    gtk_widget_set_margin_end(grid, 10);
    gtk_widget_set_margin_top(grid, 10);
    gtk_widget_set_margin_bottom(grid, 10);

    // 列間のスペースを2ピクセルに設定する場合
    gtk_grid_set_column_spacing(GTK_GRID(grid), 2);

    // HEIGHT * WIDTH だけグリッドを初期化
    for (gint y = 0; y < HEIGHT; y++) {
        if (y == 0) {
            // yが0なら上端のラベルを設定する
            for (gint x = 1; x < WIDTH; x++) {
                // 左端は除外するからxは1から
                gchar text[100];
                snprintf(text, sizeof text, "%c", (65 + (x-1)));
                GtkWidget *label = gtk_label_new(text);
                GtkWidget *event_box = gtk_event_box_new();
                gtk_container_add(GTK_CONTAINER(event_box), label);
                gtk_grid_attach(GTK_GRID(grid), event_box, x, y, 1, 1);                
            }
            continue;
        }
        for (gint x = 0; x < WIDTH; x++) {
            if (x == 0) {
                // xが0なら左端のラベルを設定
                gchar text[100];
                snprintf(text, sizeof text, "%d", y);
                GtkWidget *label = gtk_label_new(text);  // 数字を設定
                gtk_widget_set_size_request(label, 30, -1);
                GtkWidget *event_box = gtk_event_box_new();
                gtk_container_add(GTK_CONTAINER(event_box), label);
                gtk_grid_attach(GTK_GRID(grid), event_box, x, y, 1, 1);
            } else {
                GtkWidget *entry = gtk_entry_new();
                GtkWidget *event_box = gtk_event_box_new();
                gtk_container_add(GTK_CONTAINER(event_box), entry);
                gtk_grid_attach(GTK_GRID(grid), event_box, x, y, 1, 1);
            }
        }
    }

    // グリッドがシグナルを受信するように設定
    gtk_widget_set_events(grid, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK);
    g_signal_connect(G_OBJECT(grid), "button-press-event", G_CALLBACK(on_button_press), NULL);
    g_signal_connect(G_OBJECT(grid), "button-release-event", G_CALLBACK(on_button_release), NULL);
    g_signal_connect(G_OBJECT(grid), "motion-notify-event", G_CALLBACK(on_motion_notify), NULL);

    // ウィジェットを全て表示
    gtk_widget_show_all(window);

    // GTKの実行
    gtk_main();

    return 0;
}

アプリの概要

今回のアプリはGtkGridという行列にウィジェットをセットできるウィジェットを使っています。
このグリッドを特定の横幅と高さで初期化してラベルやエントリーを配置します。

グリッドの上端と左端のラベルをドラッグして動かすと行や列の幅が変わる、という実装です。
いろいろやってみたんですが、行や列を0に近くすることはできませんでした。

ラベルの境目をぐりぐり動かすと対応する行や列の幅が変わりますので、実行してみてください。

ソースコードの解説

ソースコードの解説になります。

定数とグローバル変数

// 上端と左端のラベルを含めた横幅と高さ
static const gint WIDTH = 4;
static const gint HEIGHT = 3;

// ウィジェットを配置するグリッド
static GtkWidget *grid;

// ドラッグしている間TRUEになるフラグ
static gboolean resizing = FALSE;

WIDTHはグリッドの横幅、HEIGHTはグリッドの高さです。
つまり4x3の行列でグリッドを初期化します。

gridはメインとなるグリッドのポインタです。
resizingはマウスでラベルの境目をドラッグしているときにTRUEにするフラグです。
このフラグがTRUEの間、行列の幅を変化させます。

main関数の処理

main関数の処理です。

GTKの初期化とウィンドウの作成

int main(int argc, char *argv[]) {
    // GTKを初期化
    gtk_init(&argc, &argv);

    // ウィンドウを作成
    GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(window), "Table");
    g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    ...
}

gtk_init()でGTKを初期化します。
ウィンドウを作るのでgtk_window_new()でトップレベルウィンドウを作ります。
gtk_window_set_title()でウィンドウのタイトルを設定します。
ウィンドウのdestroyシグナルにgtk_main_quit()を紐づけておきます。

グリッドの作成と設定

    // グリッドを作成
    grid = gtk_grid_new();
    gtk_container_add(GTK_CONTAINER(window), grid);
    gtk_widget_set_margin_start(grid, 10);
    gtk_widget_set_margin_end(grid, 10);
    gtk_widget_set_margin_top(grid, 10);
    gtk_widget_set_margin_bottom(grid, 10);

    // 列間のスペースを2ピクセルに設定する場合
    gtk_grid_set_column_spacing(GTK_GRID(grid), 2);

gtk_grid_new()でグリッドを作成します。
gtk_container_add()でウィンドウにグリッドを配置します。

gtk_widget_set_margin_start()でグリッドの左の空白。
gtk_widget_set_margin_end()でグリッドの右の空白。
gtk_widget_set_margin_top()でグリッドの上の空白。
gtk_widget_set_margin_bottom()でグリッドの下の空白を設定します。
こうするとウィンドウ内の上下左右に空白ができます。

gtk_grid_set_column_spacing()で列の間隔を2ピクセルに設定します。

グリッドの行列を初期化

    // HEIGHT * WIDTH だけグリッドを初期化
    for (gint y = 0; y < HEIGHT; y++) {
        if (y == 0) {
            // yが0なら上端のラベルを設定する
            for (gint x = 1; x < WIDTH; x++) {
                // 左端は除外するからxは1から
                gchar text[100];
                snprintf(text, sizeof text, "%c", (65 + (x-1)));
                GtkWidget *label = gtk_label_new(text);
                GtkWidget *event_box = gtk_event_box_new();
                gtk_container_add(GTK_CONTAINER(event_box), label);
                gtk_grid_attach(GTK_GRID(grid), event_box, x, y, 1, 1);                
            }
            continue;
        }
        for (gint x = 0; x < WIDTH; x++) {
            if (x == 0) {
                // xが0なら左端のラベルを設定
                gchar text[100];
                snprintf(text, sizeof text, "%d", y);
                GtkWidget *label = gtk_label_new(text);  // 数字を設定
                gtk_widget_set_size_request(label, 30, -1);
                GtkWidget *event_box = gtk_event_box_new();
                gtk_container_add(GTK_CONTAINER(event_box), label);
                gtk_grid_attach(GTK_GRID(grid), event_box, x, y, 1, 1);
            } else {
                GtkWidget *entry = gtk_entry_new();
                GtkWidget *event_box = gtk_event_box_new();
                gtk_container_add(GTK_CONTAINER(event_box), entry);
                gtk_grid_attach(GTK_GRID(grid), event_box, x, y, 1, 1);
            }
        }
    }

グリッドの行列を初期化します。
HEIGHTWIDTHでfor文を入れ子にして回します。
その入れ子の中でif文で分岐してラベルやエントリーを設定します。

y == 0のときは上端のラベルになりますので、これのときはx = 1から列を初期化します。
y == 0 && x == 0の時は、左上端になりますのでここは空にしておきます。

if (y == 0) {
    // yが0なら上端のラベルを設定する
    for (gint x = 1; x < WIDTH; x++) {
        // 左端は除外するからxは1から
        gchar text[100];
        snprintf(text, sizeof text, "%c", (65 + (x-1)));
        GtkWidget *label = gtk_label_new(text);
        ...
    }
    ...
}

textx座標から生成したアルファベット大文字を保存します。
65はアスキーコードでAになりますので、これから加算していきます。
もっともこの方法だと26字以上になったときに変な値になりますので、ここは改良する必要があります。

    GtkWidget *event_box = gtk_event_box_new();
    gtk_container_add(GTK_CONTAINER(event_box), label);
    gtk_grid_attach(GTK_GRID(grid), event_box, x, y, 1, 1);                

ラベルを配置しますが、ラベルはGtkEventBoxでラップしておきます。
こうするとドラッグしたときにラベルの受信するシグナルを無視することができます。
gridにはbutton-press-eventなどのシグナルをバインドしますが、このシグナルがラベルなどとコリジョン(衝突)するのでこうしています。

gtk_grid_attach()でグリッドにevent_boxを配置します。
このとき行列のxyの位置に配置します。
末尾の1, 1は行列をまたぐ数です。

    for (gint y = 0; y < HEIGHT; y++) {
        ...
        for (gint x = 0; x < WIDTH; x++) {
            if (x == 0) {
                // xが0なら左端のラベルを設定
                gchar text[100];
                snprintf(text, sizeof text, "%d", y);
                GtkWidget *label = gtk_label_new(text);  // 数字を設定
                gtk_widget_set_size_request(label, 30, -1);
                GtkWidget *event_box = gtk_event_box_new();
                gtk_container_add(GTK_CONTAINER(event_box), label);
                gtk_grid_attach(GTK_GRID(grid), event_box, x, y, 1, 1);
            } else {
                GtkWidget *entry = gtk_entry_new();
                GtkWidget *event_box = gtk_event_box_new();
                gtk_container_add(GTK_CONTAINER(event_box), entry);
                gtk_grid_attach(GTK_GRID(grid), event_box, x, y, 1, 1);
            }
        }
    }

行列の列のxが0のときは左端のラベルになるので、ラベルにyから生成した数字を入れておきます。
gtk_widget_set_size_request()でラベルの最小の横幅を30に設定します。こうするとラベルがきつきつしないで済みます。

xが0じゃない時はラベルの代わりにエントリー(入力欄)を設定します。

グリッドのシグナルをコールバック関数にバインド

    // グリッドがシグナルを受信するように設定
    gtk_widget_set_events(grid, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK);
    g_signal_connect(G_OBJECT(grid), "button-press-event", G_CALLBACK(on_button_press), NULL);
    g_signal_connect(G_OBJECT(grid), "button-release-event", G_CALLBACK(on_button_release), NULL);
    g_signal_connect(G_OBJECT(grid), "motion-notify-event", G_CALLBACK(on_motion_notify), NULL);

gtk_widget_set_events()でグリッドがシグナルを受信するようにしておきます。
それからg_signal_connect()で以下のシグナルに関数をバインドします。

  • button-press-event

  • button-release-event

  • motion-notify-event

button-press-eventはマウスのボタンがクリックされたら発行されます。
button-release-eventはボタンがリリースされたときです。
motion-notify-eventはマウスのポインターが移動している時に発行されます。

ウィジェットの表示とGTKの実行

    // ウィジェットを全て表示
    gtk_widget_show_all(window);

    // GTKの実行
    gtk_main();

main関数の最後でgtk_widget_show_all()windowウィジェット以下のウィジェットをすべて表示します。
これをしないとウィジェットが表示されません。ウィンドウ以外のウィジェットでも同様です。
動的にウィジェットを追加するときはこれを忘れないようにしてください。

gtk_mainでGTKを実行します。

プレス&リリースのコールバック関数

/**
 * GridでボタンをプレスしたらフラグをTRUEに。 
 */
static gboolean on_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data) {
    if (event->button == GDK_BUTTON_PRIMARY && event->type == GDK_BUTTON_PRESS) {
        resizing = TRUE;
    }
    return TRUE;
}

/**
 * GridでボタンをリリースしたらフラグをFALSEに。 
 */
static gboolean on_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data) {
    if (event->button == GDK_BUTTON_PRIMARY && event->type == GDK_BUTTON_RELEASE) {
        resizing = FALSE;
    }
    return TRUE;
}

on_button_press()はボタンが押されたとき、on_button_release()はボタンが離されたときに実行されます。

event->buttonGDK_BUTTONN_PRIMARYは主ボタンで、通常はマウスの左ボタンです。
event->typeGDK_BUTTON_PRESSは押された場合、GDK_BUTTON_RELEASEは離された場合です。

それぞれif文が真になったらフラグを折ったり立てたりします。
このフラグ(resizing)が立つとドラッグ時の処理が実行されます。

ラベルの境界をドラッグしている時の処理

/**
 * 上端か左端のラベルをドラッグしている間、ラベルのサイズを変更する。
 */
static gboolean on_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data) {
    if (!resizing) {
        return TRUE;
    }

    ...
}

on_motion_notify()はマウスのドラッグが発生しているときに呼び出されます。
resizingフラグが立っていなかったらreturnして処理を中断します。

ウィジェットの座標を得る

    // event->x, event->y
    // マウスの下にあるウィジェットのXY座標からの相対座標

    // マウスの下にあるウィジェットのウィンドウ内の座標
    gint wx, wy;
    gdk_window_get_position(event->window, &wx, &wy);

    // デバッグ
    g_print("win_x %d wy %d\nevent->x %f event->y %f\n"
        , wx, wy, event->x, event->y);

event->xevent->yにはマウスの下にあるウィジェットのXY座標からの相対座標が入っています。
つまり座標は0から始まります。
ウィジェットがウィンドウの左端から100pxにあっても、この座標はウィジェットが基点になりますので0からになります。

gdk_window_get_position()はマウスの下にあるウィジェットの、ウィンドウ内の座標を取得します。
つまりウィンドウの左上端が0, 0で右下端がwidth, heightになります。

マウス座標の判定とラベルのサイズの調整

    // 0行目x列目のラベルを調整
    for (gint x = 0; x < WIDTH; x++) {
        GtkAllocation alloc;
        GtkWidget *event_box = gtk_grid_get_child_at(GTK_GRID(grid), x, 0);
        if (event_box == NULL) {
            continue;
        }

        gtk_widget_get_allocation(event_box, &alloc);

        // ウィジェット内にマウスがあるかを判定
        if (wx >= alloc.x && wx < alloc.x + alloc.width &&
            wy >= alloc.y && wy < alloc.y + alloc.height) {
            g_print("resize\n");
            gtk_widget_set_size_request(event_box, event->x, -1);
        }
    }

    // y行目0列目のラベルを調整
    for (gint y = 0; y < HEIGHT; y++) {
        GtkAllocation alloc;
        GtkWidget *event_box = gtk_grid_get_child_at(GTK_GRID(grid), 0, y);
        if (event_box == NULL) {
            continue;
        }

        gtk_widget_get_allocation(event_box, &alloc);

        // ウィジェット内にマウスがあるかを判定
        if (wx >= alloc.x && wx < alloc.x + alloc.width &&
            wy >= alloc.y && wy < alloc.y + alloc.height) {
            g_print("resize\n");
            gtk_widget_set_size_request(event_box, -1, event->y);
        }
    }

マウス座標がラベルのウィジェットの範囲内にあったら、そのラベルの横幅や高さを調整します。
上端のラベルについてはX座標についてfor文を回し、左端のラベルについてはY座標についてfor文を回します。

gtk_grid_get_child_at()でグリッド内のウィジェットを取得できます。
これでウィジェットを取得して、そのウィジェットの座標と高さをgtk_widget_get_allocation()で取得します。

wxwyそれからウィジェットの座標とサイズ情報のallocを使って、wxwyallocのサイズの中に収まっているかif文でチェックします。
サイズに収まっていたらgtk_widget_set_size_request()でウィジェットのサイズをevent->xevent->yに合わせて設定します。-1を設定すると自動になります。

event->xにはマウスの下のウィジェットのXY座標からの相対座標が入っていますので、これを関数に渡すだけで、ウィジェットのサイズが伸縮します。

return TRUE

motion-notify-eventの関数はTRUEを返すと他のハンドラーにイベントを伝搬しなくなります。
FALSEを返すとイベントが伝搬します。

おわりに

今回はC言語とGTK+3で表計算ソフトみたいな表を作ってみました。
GTK+3は高速なGUIライブラリなので、こういった表みたいなアプリも作れますね。



この記事のアンケートを送信する