小嶋秀樹 | 授業情報 | 研究室
日本語 | English
Raspberry Pi 入門編

シングルボードコンピュータ Raspberry Pi を活用して,Unix (Linux) が動作する小型パソコンのプログラミングを学びます.この入門編では,まず C(C++)言語によるプログラミングの基本を学びます.すでに C (C++) に詳しい人はスキップしても構いません.これに続き,GTK+ による X Window アプリの開発に取り組み,GUI を備えたインタラクティブなアプリを開発する基礎を学びます.

Raspberry Pi (Model A+) Raspberry Pi (Model B) Raspberry Pi (Model B+)

Raspberry Pi には,Model A, Model A+ (上図左), Model B (上図中), Model B+ (上図右) の3種類があります.ここでは例として Model B を扱いますが,他の種類についてもほぼ同じようにプログラミングや配線を行うことができます.なお,Model B+ をお使いの方は,SD カードが microSD カードに変更された点,RCA コネクタからのビデオ出力の代わりにヘッドフォン端子(4極)からのビデオ出力となった点,そしてピンヘッダ P1 に新しく 14 本の端子が追加された点(1〜26 番ピンは変更なし)と P5 が削除された点に注意してください.Model A+ は,Model A 同様にメモリが 256MB(Model B/B+ は 512MB)の点と,USB 端子が1つになり,有線イーサネット端子が無くなった点を除けば,Model B+ と同等であると考えてよいでしょう.

C プログラミング超入門

キャラクタ端末上で動作する簡単なプログラムから始めます.パソコン(Mac・Windows など)から SSH で Raspberry Pi にログインし,Raspberry Pi 上で vncserver を立ち上げてください.あとは logout して構いません.(これが面倒であれば,SSH でログインした端末上でプログラミングしても構いません.その場合は「キャラクタ端末上での C プログラミング」までスキップしてください.)

mac$ ssh pi@kozPi.local
pi@kozpi.local's password: ********
Linux kozPi 3.10.25+ #622 PREEMPT Fri Jan 3 18:41:00 GMT 2014 armv6l
...
pi@kozPi ~ $ vncserver :1 -geometry 1024x768 -depth 24
pi@kozPi ~ $ logout

パソコン上で VNC クライアントを起動し,Raspberry Pi のデスクトップを画面共有します.そのデスクトップ上にある LXTerminal(端末アプリケーション)のアイコンをダブルクリックし,適当な位置・大きさで,キャラクタ端末を開いてください.(下図の例では,デスクトップ背景色・端末背景色/文字色/文字サイズなどをデフォルトから変更してあります.)

この端末上で,以下のように,~/Projects というディレクトリを作り,さらに,その中に cTutorials というディレクトリを作り,その中で cTutorial1.c という新規ファイルをエディタで作成します.

pi@kozPi ~ $ ls
Desktop  ocr_pi.png  python_games
pi@kozPi ~ $ mkdir Projects
pi@kozPi ~ $ ls
Desktop  Projects  ocr_pi.png  python_games
pi@kozPi ~ $ cd Projects
pi@kozPi ~/Projects $ mkdir cTutorial
pi@kozPi ~/Projects $ cd cTutorials
pi@kozPi ~/Projects/cTutorials $ vi cTutorial1.c

vi は機能が豊富なエディタですが,その操作にはある程度の慣れが必要です.もっと直観的に使えるエディタがよければ nano を使ってみてください.標準でインストール済のはずです.nano を起動し,^G(「コントロールキー」を押しながら「G」)と打つことで操作方法の説明が表示されます.

より高機能なエディタを使いたい人には emacs がオススメです.別途インストール(sudo apt-get install emacs)してください.emacs を起動するとき,emacs -nw cTutorial1.c のように -nw オプションを付けることで,キャラクタ端末上で emacs を使うことができます.emacs 上で f1 t(「ファンクションキー1番」に続き「t」)と打ち込むことで,チュートリアルコースを始めることができます.

【キャラクタ端末上での C プログラミング】

上述した手順で,cTutorial1.c というソースプログラムを新規編集します.プログラミング学習者にはお馴染みのハローワールドです.打ち込んだら保存して,エディタを終了してください.

cTutorial1.c
#include <stdio.h>
int main ()
{
    printf("Hello, world!\n");
    printf("    from Raspberry Pi\n");
}

このソースプログラム cTutorial1.c をコンパイル・実行するには,つぎのような手順を踏みます.ここで登場する cc(gcc)は,C 言語のソースプログラム(cTutorial1.c)を実行可能プログラム(cTutorial1)に変換する「コンパイラ(+リンカ)」です.

pi@kozPi ~/Projects/cTutorials $ cc cTutorial1.c -o cTutorial1
実行可能プログラム cTutorial1 が生成される.
pi@kozPi ~/Projects/cTutorials $ ./cTutorial1
Hello, world!
    from Raspberry Pi
pi@kozPi ~/Projects/cTutorials $ _

青字の部分がプログラム cTutorial1 の出力です.プログラムの実行が終われば,プロンプトが表示され,再びコマンド待ちとなります.

【簡単なプログラミング演習】

ここでは C プログラミングの詳しい解説はしませんが,端末(キャラクタ出力とキーボード入力)を使った簡単なプログラムを例示しますので,その動作原理を確認・理解しておいてください.

cTutorial2.c
#include <stdio.h>
#define LENGTH 100
int main ()
{
    int sum = 0;
    char line[LENGTH];
    while (fgets(line, LENGTH, stdin) != NULL) {
        int val;
        val = atoi(line);
        if (val == 0)
            sum = 0;
        else
            sum = sum + val;
        printf("sum = %d\n", sum);
    }
    printf("bye!\n");
}

このプログラムは,標準入力(stdin),つまり端末のキーボードから1行ずつ文字列(line)を読み込み,その文字列を整数値に変換(atoi())し,その合計を計算していきます.0 が入力された場合は,それまで足し合わせた合計値(sum)を 0 にリセットします.2行目の #define LENGTH 100 はマクロ定義とよばれ,その行以降のソースプログラムに現れる LENGTH という識別子が 100 に置き換えられます.

このプログラムをコンパイル・実行すると,つぎのような結果が得られます.

pi@kozPi ~/Projects/cTutorials $ cc cTutorial2.c -o cTutorial2
pi@kozPi ~/Projects/cTutorials $ ./cTutorial2
123
sum = 123
234
sum = 357
345
sum = 702
0
sum = 0
456
sum = 456
567
sum = 1023
bye!
pi@kozPi ~/Projects/cTutorials $ _

上の実行例では,sum = 1023 が表示された後,端末(の行頭)で ^D(「コントロールを押しながらd)を打つことで,EOF(end of file)をプログラムの標準入力に送り込み,プログラムを終了させています.EOF を受け取った fgets()NULL を値として返すので,while ループを抜けることができるわけです.(端末から ^C を送ってプログラムを強制終了させることもできますが,その場合 bye! は表示されません.)

【コマンドライン引数について】

作成したプログラムを実行するとき,引数を渡すことができます.たとえば,./cTutorial3 123 234 のようにプログラムを起動した場合,3つのトークンからなる「コマンドライン」を main 関数で受け取ることができます.

cTutorial3.c
#include <stdio.h>
int main (int argc, char *argv[])
{
    int i, sum = 0;
    for (i = 1; i < argc; i++)
        sum += atoi(argv[i]);
    printf("sum = %d\n", sum);
}
pi@kozPi ~/Projects/cTutorials $ cc cTutorial3.c -o cTutorial3
pi@kozPi ~/Projects/cTutorials $ ./cTutorial3 123 234 345
sum = 702
pi@kozPi ~/Projects/cTutorials $ _

main 関数が受け取る argc は引数の数(./cTutorial3 123 234 の場合は 3)であり,argv は引数となるトークン文字列の配列です.argv[0]"./cTutorial3" という文字列,argv[1]"123" という文字列,argv[2]"234" という文字列となります.プログラム名 "./cTutorial3" も含まれていることに注意してください.

GTK+ によるデスクトップアプリの開発

デスクトップ(X Window)上で動作するグラフィカルユーザインタフェースをもったアプリケーションを開発します.そのために利用する GUI ライブラリが GTK+ (The GIMP Toolkit) です.GTK+ によって CPU や OS の差を吸収できます.また,C 言語をベースとしていますが,C++ や Java, その他スクリプト言語(Perl, Python, JavaScript など)からも GTK+ を利用できます.GTK によって開発されたアプリケーションとしては,GIMP や Firefox などが有名です.

GTK+(ここでは GTK+ 3)を利用するには,下記の手順でインストールする必要があります.詳しい解説(英文)は,https://developer.gnome.org/gtk3/stable/ をご覧ください.

pi@kozPi ~ $ sudo apt-get install libgtk-3-dev
【GTK+ プログラミング(その1)】

まずは「ハローワールド」から始めます.~/Projects/gtkTutorials というディレクトリを作り,そこに gtkTutorial1.c というファイルを新規作成しましょう.

Hello, world!
gtkTutorial1.c
#include <gtk/gtk.h>
int main (int argc, char *argv[])
{
    //  親ウィンドウと GUI 要素(のポインタ変数)
    GtkWidget *window, *label;
    //  gtk+ の初期化(オマジナイ)
    gtk_init(&argc, &argv);

    //  親ウィンドウの生成(+殺し方の指定)
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    g_signal_connect(window, "delete-event",
                     G_CALLBACK(gtk_main_quit), NULL );
    //  ラベルの生成(+親ウィンドウへの配置)
    label = gtk_label_new("Hello, world!");
    gtk_container_add(GTK_CONTAINER(window), label);

    //  GUI 要素・親ウィンドウの表示
    gtk_widget_show(label);
    gtk_widget_show(window);

    //  イベント処理ループ
    gtk_main();
}

このソースプログラムをコンパイルするには,cc gtkTutorial1.c -o gtkTutorial1 `pkg-config --cflags --libs gtk+-3.0` のようにします.後半部分で GTK+ ライブラリ(およびヘッダファイル)の場所をコンパイラに伝えています.引用符はバッククオート(逆引用符)なので,間違えないように入力してください.

pi@kozPi ~/Projects/gtkTutorials $ cc gtkTutorial1.c -o gtkTutorial1 `pkg-config --cflags --libs gtk+-3.0`
pi@kozPi ~/Projects/gtkTutorials $ ./gtkTutorial1

gtkTutorial1 を実行すると,デスクトップ上に小さなウィンドウが開き,その中に "Hello, world!"(というテキストを保持したラベル)が現れます.ウィンドウ上部には,ウィンドウを制御(最小化・最大化・閉じる)するためのタイトルバーがあります.ウィンドウの右下にある三角は,大きさを調整するためのタブです (設定により無い場合もあります).タイトルバーにある「閉じる [X]」ボタンを押すと,このウィンドウは消滅します.プログラムは終了し,端末に再びプロンプトが表示されます.

バッククオート: バッククオートで囲まれた部分は,まずその中身がコマンドとして実行され,その結果(標準出力)となる文字列に置き換えられます.上の例では,pkg-config --cflags --libs gtk+-3.0 が実行され,その結果の文字列(gtk+ のヘッダファイルやライブラリファイルの場所についての情報)がコンパイラ cc に引数として渡されます.ためにし pkg-config --cflags --libs gtk+-3.0 をコマンドラインで実行してみてください.また,pkg-config --cflags gtk+-3.0pkg-config --libs gtk+-3.0 も試してみてください.

【GTK+ プログラミング(その2)】

つぎにボタンを作ってみましょう.プログラムの基本構造は gtkTutorial1.c と同じです.ただし,ボタンが押されたときに実行される関数(コールバック)を仕込んでおきます.コールバックの名前は何でもよいのですが,ここでは button_clicked() としておきます.

Push me!
gtkTutorial2.c
#include <gtk/gtk.h>
//  ボタン用コールバック
void button_clicked (GtkWidget *widget, gpointer data)
{
    g_print("The button was clicked!\n");
}
//  メイン
int main (int argc, char *argv[])
{
    //  親ウィンドウと GUI 要素(のポインタ変数)
    GtkWidget *window, *button;
    //  gtk+ の初期化(オマジナイ)
    gtk_init(&argc, &argv);

    //  親ウィンドウの生成(+殺し方の指定)
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    g_signal_connect(window, "delete-event",
                     G_CALLBACK(gtk_main_quit), NULL );
    //  ボタンの生成(+イベント処理の指定,+親ウィンドウへの配置)
    button = gtk_button_new_with_label("Push me!");
    g_signal_connect(button, "clicked", 
                     G_CALLBACK(button_clicked), NULL );
    gtk_container_add(GTK_CONTAINER(window), button);

    //  GUI 要素・親ウィンドウの表示
    gtk_widget_show(button);
    gtk_widget_show(window);

    //  イベント処理ループ
    gtk_main();
}

プログラムを実行すると,"Push me!" というボタンが現れます.これを押すと(何度でも)端末に "The button was clicked!" と表示されます.ウィンドウの「閉じる」ボタンを押したときの動作は,前の例と同じです.

このプログラムは,(1) GTK+ の初期化処理,(2) 親ウィンドウの生成,(3) GUI 要素の生成・配置,(4) イベント処理ループの実行から成ります.(1)〜(3) はその意味を直観的に理解できるはずです.最後の (4) は,「イベント」を待ち,それを検出したら,対応するアクション(「コールバック」の実行)を行うことを繰り返します.疑似コードで記述すれば,gtk_main() はおよそ次のような無限ループからなります.

void gtk_main ()
{
    while (1) {
        Event e;
        e = getOneEvent();
        processOneEvent(e);
    }
}

なお「イベント」とは,おもに,マウスクリックやキークリック,タイマー(一定時間ごとに発生)によって発生します.応答が必要なウィジェットに,対応すべきイベントごとのコールバックを用意するのが,GTK+ の「イベント駆動型」プログラミングの特徴です.

【GTK+ プログラミング(その3)】

こんどはラベルとボタンを並列させてみましょう.複数の GUI 要素(ウィジェットとよぶ)を縦または横に並べるための箱を「ボックス」といいます.ボックスは,並べる方向を指定して生成することができます.ボックスもウィジェットの一種です.

Hello, push me!
gtkTutorial3.c
#include <gtk/gtk.h>
//  ボタン用コールバック
void button_clicked (GtkWidget *widget, gpointer data)
{
    g_print("The button was clicked!\n");
}
//  メイン
int main (int argc, char *argv[])
{
    //  親ウィンドウと GUI 要素(のポインタ変数)
    GtkWidget *window, *box, *label, *button;
    //  gtk+ の初期化(オマジナイ)
    gtk_init(&argc, &argv);

    //  親ウィンドウの生成(+殺し方の指定)
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    g_signal_connect(window, "delete-event",
                     G_CALLBACK(gtk_main_quit), NULL );
    //  縦ボックスの生成(+親ウィンドウへの配置)
    box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_container_add(GTK_CONTAINER(window), box);
    //  ラベルの生成(+ボックスへの配置)
    label = gtk_label_new("Hello, world!");
    gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 0);
    //  ボタンの生成(+ボックスへの配置)
    button = gtk_button_new_with_label("Push me!");
    g_signal_connect(button, "clicked", G_CALLBACK(button_clicked), NULL);
    gtk_box_pack_start(GTK_BOX(box), button, TRUE, TRUE, 0);
    
    //  GUI 要素・親ウィンドウの表示
    gtk_widget_show(label);
    gtk_widget_show(button);
    gtk_widget_show(box);
    gtk_widget_show(window);

    //  イベント処理ループ
    gtk_main();
}

ここでは GTK_ORIENTATION_VERTICAL を指定して縦ボックスを生成し,そこにまずラベルを,つぎにボタンを詰め込んでいます.横ボックスは GTK_ORIENTATION_HORIZONTAL を指定することで生成できます.詰め込むために使う関数は gtk_box_pack_start() で,ボックスの上から下へ・左から右への方向に,要素を詰め込んでいきます.逆方向から詰め込みたい場合は,gtk_box_pack_end() を使ってください.

こうして生成・配置されたウィジェットですが,そのままでは画面に表示されていません.gtk_widget_show() によってウィジェットを「表示」させる必要があります.基本的に内側(ラベルやボタン)から外側(ボックスや親ウィンドウ)への順番で「表示」させるとよいでしょう.逆に,gtk_widget_hide() によってウィジェットを「非表示」にすることもできます.アプリケーションの内部状態に応じて,見せるインタフェースと隠すインタフェースを区別することが可能です.

【GTK+ プログラミング(その4)】

ボックスの中にボックスを入れることも可能です.ちょうど弁当箱のように,縦横に仕切ることで,自由にウィジェットを配置することができます.つぎの例では,ウィンドウの上側にラベル,下側には左右に2つのボタンを配置しています.

Menu
gtkTutorial4.c
#include <gtk/gtk.h>
//  ボタン用コールバック
void button1_clicked (GtkWidget *widget, gpointer data)
{
    g_print("menu #1: You will get a kara-age bento!\n");
}
void button2_clicked (GtkWidget *widget, gpointer data)
{
    g_print("menu #2: You will get a maguro-don bento!\n");
}
//  メイン
int main (int argc, char *argv[])
{
    //  親ウィンドウと GUI 要素(のポインタ変数)
    GtkWidget *window, *vbox, *hbox, *label, *button1, *button2;
    //  gtk+ の初期化(オマジナイ)
    gtk_init(&argc, &argv);

    //  親ウィンドウの生成(+殺し方の指定)
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    g_signal_connect(window, "delete-event",
                     G_CALLBACK(gtk_main_quit), NULL );
    //  縦ボックスの生成(+親ウィンドウへの配置)
    vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_container_add(GTK_CONTAINER(window), vbox);
    //  ラベルの生成(+横ボックスへの配置)
    label = gtk_label_new("Menu");
    gtk_box_pack_start(GTK_BOX(vbox), label, TRUE, TRUE, 0);
    //  横ボックスの生成(+横ボックスへの配置)
    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
    //  ボタンの生成(+ボックスへの配置)
    button1 = gtk_button_new_with_label("Chicken");
    g_signal_connect(button1, "clicked", 
                     G_CALLBACK(button1_clicked), NULL );
    gtk_box_pack_start(GTK_BOX(hbox), button1, TRUE, TRUE, 0);
    button2 = gtk_button_new_with_label("Fish");
    g_signal_connect(button2, "clicked", 
                     G_CALLBACK(button2_clicked), NULL );
    gtk_box_pack_start(GTK_BOX(hbox), button2, TRUE, TRUE, 0);
    
    //  GUI 要素・親ウィンドウの表示
    gtk_widget_show(button1);
    gtk_widget_show(button2);
    gtk_widget_show(hbox);
    gtk_widget_show(label);
    gtk_widget_show(vbox);
    gtk_widget_show(window);

    //  イベント処理ループ
    gtk_main();
}

実行時に現れるウィンドウ(の内部)は下図のようになります.外側の黒枠が window,つぎの赤枠(点線)が vbox,下側にある青枠(点線)が hbox です.下図では,これら枠線の間にわずかなスキマが見えますが,実際にはスキマはありません.2つのボタンの大きさが,与えたラベル("Chicken", "Fish")に応じて異なっていることに注意してください.また,よく見ると,上側のラベル("Menu")の高さと,下側のボタンの高さは,少しだけ異なっています.

幅/高さを揃える: 2つのボタン("Chicken", "Fish")の幅が異なっているのが気持ち悪いですね.これを揃えるには,つぎのように,ボックスに「均等幅」を指定して,要素の幅を同じにします.改造してみてください.(横方向だけでなく縦方向の「均等幅」も可能です.)

    ...
    //  横ボックスの生成(+均等幅+縦ボックスへの配置)
    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_set_homogeneous(GTK_BOX(hbox), TRUE);
    gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
    ...

要素間にスキマを与える: 2つのボタン("Chicken", "Fish")の間にスキマがないのが気持ち悪いですね.スキマを与えるには,つぎのように,ボックス生成時に「要素間隔」を指定します.改造してみてください.

    ...
    //  横ボックスの生成(+要素間隔・均等幅+縦ボックスへの配置)
    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
    gtk_box_set_homogeneous(GTK_BOX(hbox), TRUE);
    gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
    ...

要素の両端にパディングを与える: 要素間のスキマではなく,要素の両端(内側)にパディング(つめもの)を与えることも考えられます.つぎのように,パッキング時に「パディング」を指定します.改造してみてください.

    ...
    //  横ボックスの生成(+均等幅+縦ボックスへの配置)
    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_set_homogeneous(GTK_BOX(hbox), TRUE);
    gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
    //  ボタンの生成(+ボックスへの配置)
    button1 = gtk_button_new_with_label("Chicken");
    g_signal_connect(button1, "clicked", 
                     G_CALLBACK(button1_clicked), NULL );
    gtk_box_pack_start(GTK_BOX(hbox), button1, TRUE, TRUE, 5);
    button2 = gtk_button_new_with_label("Fish");
    g_signal_connect(button2, "clicked", 
                     G_CALLBACK(button2_clicked), NULL );
    gtk_box_pack_start(GTK_BOX(hbox), button2, TRUE, TRUE, 5);
    ...

ボックスの両端にパディングを与える: 要素間はスキマをあけずに,要素を入れたボックスの両端にのみパディングを与えるには,ボックスの中にボックスを(パディング付きで)詰めればよいでしょう.つぎのように,二重ボックスをつくってみてください.

    ...
    //  横二重ボックスの生成
    GtkWidget *hbox0;
    hbox0 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_pack_start(GTK_BOX(vbox), hbox0, TRUE, TRUE, 0);
    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_set_homogeneous(GTK_BOX(hbox), TRUE);
    gtk_box_pack_start(GTK_BOX(hbox0), hbox, TRUE, TRUE, 10);
    //  ボタンの生成(+ボックスへの配置)
    button1 = gtk_button_new_with_label("Chicken");
    g_signal_connect(button1, "clicked", 
                     G_CALLBACK(button1_clicked), NULL );
    gtk_box_pack_start(GTK_BOX(hbox), button1, TRUE, TRUE, 0);
    button2 = gtk_button_new_with_label("Fish");
    g_signal_connect(button2, "clicked", 
                     G_CALLBACK(button2_clicked), NULL );
    gtk_box_pack_start(GTK_BOX(hbox), button2, TRUE, TRUE, 0);
    ...
    //  GUI 要素・親ウィンドウの表示
    gtk_widget_show(button1);
    gtk_widget_show(button2);
    gtk_widget_show(hbox);
    gtk_widget_show(hbox0);
    gtk_widget_show(label);
    gtk_widget_show(vbox);
    gtk_widget_show(window);
    ...

拡張フラグ・充填フラグ: gtk_box_pack_start() の第3引数は「拡張フラグ」,第4引数は「充填フラグ」で,どちらもボックスに埋め込まれたときに要素がどのように拡張するのかを指定します.拡張フラグが FALSE であれば,要素は最小の大きさ(内部の文字列などによって決まる)を保つようになり,TRUE であればボックスを充填するように拡張します.拡張フラグが TRUE のとき,充填フラグも TRUE であれば要素自体が大きくなり,逆に FALSE であれば要素の両端にパディングが加えられて,要素の配置が実行されます.また,ボックスに「要素間隔」や「均等幅」を指定したときも,それら条件を満たした上で,上述のように要素の拡張・配置が実行されます.(その原理は,与えられた条件(均等幅・要素間隔・パディング量・拡張/充填フラグ)を満たす最もコンパクトな配置をとるようになります.)

縦方向の配置も同じ: 水平方向の要素配置について解説してきましたが,縦方向についても,スキマとパディングの与え方によって,同じように細かい配置が可能です.試してみてください.

GTK+ によるデスクトップアプリの開発:その先へ

ここでは GTK+ のさまざまなウィジェットを利用するときの一般的な方法について解説します.また,GLib や Pango といったサブモジュールへのアクセス方法についても,その手がかりを例示します.

【GTK+ プログラミング(その5)】

まず「GTK+ のウィジェット一覧」を見てください.これらウィジェットを組み合わせることで,GUI を構成するのが,GTK+ プログラミングの基本です.

たとえば GtkCheckButton(上図)を使いたいとしましょう.ブラウザ上で「ウィジェット一覧」から GtkCheckButton(上図)の部分をクリックするとその解説ページに移動します.そこには,関数名(Synopsis)などが書かれています.とくに生成関数(名前に new が含まれている関数)は必ず使うので要チェックです.

Synopsis (GtkCheckButton)
#include <gtk/gtk.h>
struct      GtkCheckButton;
GtkWidget * gtk_check_button_new                (void);
GtkWidget * gtk_check_button_new_with_label     (const gchar *label);
GtkWidget * gtk_check_button_new_with_mnemonic  (const gchar *label);

ユーザがチェックを入れたときにどのようなイベントが発生し,どのような状態変化が起こるのかを知るには,シグナル(Signals)の項目を参照します.ところが,GtkCheckButton の解説ページにはこの項目がありません.そこで,オブジェクト階層(Object Hierarchy)を1段上に移動し,GtkToggleButton の解説に移動します.

Object Hierarchy (GtkCheckButton)
GObject
 +----GInitiallyUnowned
       +----GtkWidget
             +----GtkContainer
                   +----GtkBin
                         +----GtkButton
                               +----GtkToggleButton
                                     +----GtkCheckButton
                                           +----GtkRadioButton

GtkToggleButton は GtkCheckButton の親クラスになります.ここには関数名(Synopsis)の他にシグナル(Signals)やシグナル詳細(Signal Details)の項目があり,つぎのようになっています.

Synopsis (GtkToggleButton)
...
void        gtk_toggle_button_toggled       (GtkToggleButton *toggle_button);
gboolean    gtk_toggle_button_get_active    (GtkToggleButton *toggle_button);
void        gtk_toggle_button_set_active    (GtkToggleButton *toggle_button,
                                             gboolean is_active);
...
Signals (GtkToggleButton)
"toggled"                                        : Run First
Signal Details (GtkToggleButton)
void        user_function   (GtkToggleButton *togglebutton,
                             gpointer         user_data)     : Run First

GtkCheckButton はチェック状態が変化したときに(GtkToggleButton と同じように)"toggled" というシグナルが発生します.対応するコールバックは void user_function(GtkToggleButton *togglebutton, gpointer user_data) という形式をとります.togglebutton にはシグナル発生元のウィジェットへのポインタが入ります.また,オン・オフの状態を gtk_toggle_button_get_active() という関数によって得ることができ,また gtk_toggle_button_set_active() という関数によって変えることができます.これらシグナルや関数の使い方は,つぎの例を参考にしてください.

Toppings
gtkTutorial5.c
#include <gtk/gtk.h>
//  ボタン用コールバック
void button1_toggled (GtkWidget *widget, gpointer data)
{
    g_print("check button #1: ");
    if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget)))
        g_print("Add mayonnaise\n");
    else
        g_print("No mayonnaise\n");
}
void button2_toggled (GtkWidget *widget, gpointer data)
{
    g_print("check button #2: ");
    if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget)))
        g_print("Add mustard\n");
    else
        g_print("No mustard\n");
}
//  メイン
int main (int argc, char *argv[])
{
    GtkWidget *window, *vbox, *button1, *button2;
    gtk_init(&argc, &argv);

    //  親ウィンドウの生成(+殺し方の指定)
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    g_signal_connect(window, "delete-event",
                     G_CALLBACK(gtk_main_quit), NULL );
    //  縦ボックスの生成(+親ウィンドウへの配置)
    vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_container_add(GTK_CONTAINER(window), vbox);
    //  ボタンの生成(+ボックスへの配置)
    button1 = gtk_check_button_new_with_label("Mayonnaise");
    g_signal_connect(button1, "toggled", 
                     G_CALLBACK(button1_toggled), NULL );
    gtk_box_pack_start(GTK_BOX(vbox), button1, TRUE, FALSE, 0);
    button2 = gtk_check_button_new_with_label("Mustard");
    g_signal_connect(button2, "toggled", 
                     G_CALLBACK(button2_toggled), NULL );
    gtk_box_pack_start(GTK_BOX(vbox), button2, TRUE, FALSE, 0);
    
    //  GUI 要素・親ウィンドウの表示
    gtk_widget_show(button1);
    gtk_widget_show(button2);
    gtk_widget_show(vbox);
    gtk_widget_show(window);

    //  イベント処理ループ
    gtk_main();
}

コールバックの中で GTK_TOGGLE_BUTTON(widget) のようにキャストしているのは,GtkWidget へのポインタである widget を GtkToggleButton として gtk_toggle_button_get_active() に与える必要があるからです.それ以外にも同様のキャストが何カ所かありますが,どれも同じ理由で必要です.(というか,C++ っぽいプログラミングスタイルを C で実現させたのが,その背後にある理由です.)

【GTK+ プログラミング(その6)】

いままでユーザインタフェースの構成要素となるウィジェットとその扱いについて見てきましたが,それ以外の重要な構成要素として「時計」や「タイマ」があげられます.これらは GTK+ に含まれている GLib というライブラリによって提供されています.つぎのプログラム(デジタル時計)の赤字の部分がそれらに該当します.

Clock
gtkTutorial6.c
#include <gtk/gtk.h>
//  時刻表示用ラベル
GtkWidget *label;
//  タイマ用コールバック
gboolean timer_1s (gpointer data)
{
    //  現在時刻の取得
    GDateTime *gdt = g_date_time_new_now_local();
    gint h = g_date_time_get_hour(gdt),
         m = g_date_time_get_minute(gdt),
         s = g_date_time_get_second(gdt);
    //  文字列 "12:34.56" に変換
    gchar buf[100];
    g_sprintf(buf, "%2d:%02d.%02d", h, m, s);
    //  ラベルの内容を変更
    gtk_label_set_text(GTK_LABEL(label), buf);
    //  TRUE を返せば次回もタイマ有効,FALSE ならば無効
    return TRUE;
}
//  メイン
int main (int argc, char *argv[])
{
    //  gtk+ の初期化
    GtkWidget *window;
    gtk_init(&argc, &argv);
    //  親ウィンドウにラベル("wait...")を入れる
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    g_signal_connect(window, "delete-event",
                     G_CALLBACK(gtk_main_quit), NULL );
    label = gtk_label_new("wait...");
    gtk_widget_set_size_request(label, 120, 40);
    gtk_container_add(GTK_CONTAINER(window), label);
    //  タイマ設定(1000ms ごとに timer_1s を呼び出す)
    g_timeout_add(1000, timer_1s, NULL);
    //  window とその中身をすべて表示
    gtk_widget_show_all(window);
    //  イベント処理ループ
    gtk_main();
}

1000ms ごとに timer_1s(NULL) が呼び出され,現在時刻(時・分・秒)を文字列に整形した後,ラベルの内容をその文字列に変更しています.タイマ用コールバックが TRUE を返せば,さらに 1000ms 後に timer_1s(NULL) が呼び出されます.FALSE を返した場合は,それ以降,タイマは無効になります.(その場合,同じ時刻が表示されたままになります.)

グローバル変数として label を使っているのがキモチ悪いので,以下のように変更してみましょう.コールバック関数が呼び出されるとき,引数として label が与えられます.コールバック側では,受け取った label に対して,文字列変更の操作をしています.

#include <gtk/gtk.h>
//  タイマ用コールバック
gboolean timer_1s (gpointer data)
{
    GtkWidget *theLabel = GTK_WIDGET(data);
    //  現在時刻の取得
    GDateTime *gdt = g_date_time_new_now_local();
    ...
    //  ラベルの内容を変更
    gtk_label_set_text(GTK_LABEL(theLabel), buf);
    ...
}
//  メイン
int main (int argc, char *argv[])
{
    //  gtk+ の初期化
    GtkWidget *window, *label;
    gtk_init(&argc, &argv);
    ...
    //  タイマ設定(1000ms ごとに timer_1s(label) を呼び出す)
    g_timeout_add(1000, timer_1s, label);
    ...
}

ラベル文字列の装飾: GTK+ では,文字列の描画は Pango(παν語)というサブモジュールが担当しています.文字のサイズや色など,さまざまな文字装飾を「タグ付け」することができます.つぎのように改造してみてください.

//  タイマ用コールバック
gboolean timer_1s (gpointer data)
{
    ...
    gchar buf[1000];
    g_sprintf(buf, "<span size='24576'>%2d:%02d</span><span size='16384'>.%02d</span>", h, m, s);
    gtk_label_set_markup(GTK_LABEL(theLabel), buf);
    ...
}

サイズ(size)にはポイント(1/72 インチ)に 1024 を掛けた値を与えます.上の例では,時分の部分は 24pt,秒は 16pt にしてあります.他によく使うタグとして,weight ('bold', etc.),foreground ('#ff0000', etc.),font_family ('sans', 'monospace', etc.) などがあります.詳しくは「Pango Text Attribute Markup Language」を参照してください.

GTK+ によるデスクトップアプリの開発:そのさらに先へ

ここでは GTK+ 上で図形(四角や円や直線など)を描画するためのサブモジュール cairo について解説します.

【GTK+ プログラミング(その7)】

描画エリアとなるウィジェット GtkDrawingArea に,四角や円や直線などを自由に描画することができます.描画エリアの生成時に大きさ(たとえば 300x200 ピクセル)を指定しておくとよいでしょう.描画エリアへの描画は,コールバック関数("draw" イベントへの応答関数)の中に記述します.具体的には,つぎのプログラムを参照してください.

Cairo Flag
gtkTutorial7.c
#include <gtk/gtk.h>
//  描画コールバック
gboolean draw_canvas (GtkWidget *widget, cairo_t *cr, gpointer data)
{
    guint width = gtk_widget_get_allocated_width(widget),
          height = gtk_widget_get_allocated_height(widget);
    //  色(第4要素はアルファ=不透明度)
    GdkRGBA colorWhite = {1.0, 1.0, 1.0, 1.0},
            colorRed = {0.843, 0.075, 0.271, 1.0};
    //  白塗り長方形
    gdk_cairo_set_source_rgba(cr, &colorWhite);
    cairo_rectangle(cr, 0.0, 0.0, width, height);
    cairo_fill(cr);
    //  赤玉塗り(cen_x, cen_y, hankei, start_rad, end_rad)
    gdk_cairo_set_source_rgba(cr, &colorRed);
    cairo_arc(cr, width * 0.5, height * 0.5, MIN(width, height) * 0.3,
              0.0, 2.0 * G_PI );
    cairo_fill(cr);
    //  おまじない(親ウィンドウにも "draw" を伝搬)
    return FALSE;
}
//  メイン
int main (int argc, char *argv[])
{
    GtkWidget *window, *canvas;
    gtk_init(&argc, &argv);
    //  親ウィンドウの生成(+見出し・殺し方の指定)
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(window), "bento");
    g_signal_connect(window, "delete-event",
                     G_CALLBACK(gtk_main_quit), NULL );
    //  描画エリアの生成(+親ウィンドウへの配置)
    canvas = gtk_drawing_area_new();
    gtk_widget_set_size_request(canvas, 300, 200);
    gtk_container_add(GTK_CONTAINER(window), canvas);
    g_signal_connect(canvas, "draw",
                     G_CALLBACK(draw_canvas), NULL );
    //  OK, GO!
    gtk_widget_show_all(window);
    gtk_main();
}

コールバック関数(ここでは draw_canvas)は,第2引数として cairo_t を受け取ります.これは描画コンテキストと呼ばれるもので,色や線スタイルなどを設定・保持するものです.このコンテキストに色などを指定しながら,cairo_rectangle() や cairo_arc() によっって図形パスを生成し,cairo_fill() によってそのパスを塗りつぶしたり,あるいは cairo_stroke() によってパス輪郭を描画したりできます.(その他のパス生成関数については「Cairo パス関連関数一覧」を参照してください.)

GtkDrawingArea はマウス(やキーボード)の操作を受け取ることができます.たとえば,マウスクリックをイベントとして受け取り,クリックされた位置に赤玉を描画するプログラムに改造すれば,つぎのようになります.

Cairo Dots
#include <gtk/gtk.h>
//  赤玉
#define SPOTS_MAX 100
gint spots_num = 0;
gfloat spots_fx[SPOTS_MAX], spots_fy[SPOTS_MAX];
//  描画コールバック
gboolean draw_canvas (GtkWidget *widget, cairo_t *cr, gpointer data)
{
    ...
    //  赤玉塗り(cen_x, cen_y, hankei, start_rad, end_rad)
    gdk_cairo_set_source_rgba(cr, &colorRed);
    int i;
    for (i = 0; i < spots_num; i++) {
        cairo_arc(cr, width * spots_fx[i], height * spots_fy[i], 20,
                  0.0, 2.0 * G_PI );
        cairo_fill(cr);
    }
    //  おまじない(親ウィンドウにも "draw" を伝搬)
    return FALSE;
}
//  マウスクリックのコールバック
gboolean add_spot (GtkWidget *widget, GdkEventButton *event, gpointer data)
{
    guint width = gtk_widget_get_allocated_width(widget),
          height = gtk_widget_get_allocated_height(widget);
    gint px, py;
    //  マウスの位置を取得
    gdk_window_get_device_position(event->window,
                                   gtk_get_current_event_device(),
                                   &px, &py, NULL );
    //  canvas 上の相対位置を記録
    if (spots_num < SPOTS_MAX) {
        spots_fx[spots_num] = (float) px / width;
        spots_fy[spots_num] = (float) py / height;
        spots_num++;
    }
    //  クリック直後に再描画するため canvas に "draw" イベントを送る
    gtk_widget_queue_draw(widget);
    return FALSE;
}
//  メイン
int main (int argc, char *argv[])
{
    ...
    //  描画エリアの生成(+親ウィンドウへの配置)
    canvas = gtk_drawing_area_new();
    gtk_widget_set_size_request(canvas, 300, 200);
    gtk_container_add(GTK_CONTAINER(window), canvas);
    g_signal_connect(canvas, "draw",
                     G_CALLBACK(draw_canvas), NULL );
    //  クリックで add_spot + 捕獲するイベントを設定
    g_signal_connect(canvas, "button-press-event",
                     G_CALLBACK(add_spot), NULL );
    gtk_widget_set_events(canvas,
        gtk_widget_get_events(canvas) | GDK_BUTTON_PRESS_MASK );
    //  OK, GO!
    gtk_widget_show_all(window);
    gtk_main();
}