C言語とGTK+3で表計算ソフトみたいな表を作る
目次
C言語とGTK+3で表計算ソフトみたいな表
C言語のGTK+3は非常に高速なGUIライブラリです。
そのため高速処理が必要なGUIアプリ制作に適しています。
今回はこのGTK+3で表計算ソフトみたいな表を作ってみたいと思います。
以下が実行風景です。
ソースコード全文
ソースコードは以下になります。
/* 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); } } }
グリッドの行列を初期化します。
HEIGHT
とWIDTH
で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); ... } ... }
text
にx
座標から生成したアルファベット大文字を保存します。
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
を配置します。
このとき行列のx
とy
の位置に配置します。
末尾の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->button
のGDK_BUTTONN_PRIMARY
は主ボタンで、通常はマウスの左ボタンです。
event->type
のGDK_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->x
とevent->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()
で取得します。
wx
とwy
それからウィジェットの座標とサイズ情報のalloc
を使って、wx
とwy
がalloc
のサイズの中に収まっているかif文でチェックします。
サイズに収まっていたらgtk_widget_set_size_request()
でウィジェットのサイズをevent->x
やevent->y
に合わせて設定します。-1を設定すると自動になります。
event->x
にはマウスの下のウィジェットのXY座標からの相対座標が入っていますので、これを関数に渡すだけで、ウィジェットのサイズが伸縮します。
return TRUE
motion-notify-event
の関数はTRUE
を返すと他のハンドラーにイベントを伝搬しなくなります。
FALSE
を返すとイベントが伝搬します。
おわりに
今回はC言語とGTK+3で表計算ソフトみたいな表を作ってみました。
GTK+3は高速なGUIライブラリなので、こういった表みたいなアプリも作れますね。