コンテンツにスキップ

GTKプログラミング

出典: フリー教科書『ウィキブックス(Wikibooks)』
プログラミング > GTKプログラミング

GNOME用のGUIアプリの作り方

[編集]

作業の全体像

[編集]

「GTK」なんとか(なんとかの部分にはバージョン番号などが入る) という、Gnomeコミュニティによる公式のGUIアプリ開発ソフトがあるので、そのGTKなんとかを使えば、GNOME用のGUIアプリを作れる。2021年現在ではgtk4まで出ているが、現状の本ページでは主にgtk3について説明する。

なので、まずGTKをインストールすればいい。GnomeはLinuxコミュニティで普及しているので、gtkのインストールにはLinux上でgtkをインストールして開発するのがラクである。

※ Linuxにも種類がいろいろあって(ubuntu とか Fedora とか)、それごとにインストール方法が異なるので、インストール方法の説明は後述する。

GTKは、C言語など既存のプログラミング言語で開発したコードに組み込んで使える。(C言語のコード中に、GTKを呼び出すためのコードを、あなたがテキストファイルなどを用いて追記することになる。)

また、GTKで開発したGUIアプリを試しに使用する場合、通常のC言語の実行ファイル(オブジェクトファイル)と同様に、gccなどのC言語コンパイラでコンパイルして作成出力したオブジェクトファイルを(単にアイコンをダブルクリックするなどして)起動すればいいだけである。


予備知識

[編集]

本書では特にことわりのないかぎり、使用OSとして Linux上でGTKアプリを開発することを前提とする。また、GTKやgccを管理しているコミュニティはGnomeというデスクトップ環境を作っている組織(組織名もGnome)なので、本書で前提とするデスクトップ環境は原則的にGnomeとする。

もし読者が、これら技術的な背景の説明がまったく分からないなら、OSS開発ツール/GUIツールキットを読んでから再び本書に戻ることになるだろう。あるいは、当分のあいだはLinux用GUIアプリの開発は諦め(あきらめ) WindowsでWindows APIなどでWindows用GUIアプリを開発するのが良いだろう。

なので、前提として、まずgccなどのC言語コンパイラをインストールしてあり、あなたもgccやC言語などに、ある程度は習熟している必要がある。

C言語には標準C言語のほかにもC++言語やC#などいくつか派生の言語がありますが、GTKが準拠しているのは標準C言語です。

なので、もし読者が C++ や C# を知らなくてもC言語プログラミングとLinuxさえ知っていれば、とりあえずはGTKプログラミングはできます。

方法

[編集]

インストール例

[編集]

まず、GTKの開発環境をインストールする必要があります。もし開発環境をインストールしておかないと、もしもGTK開発環境で定義されている関数を含む自作プログラムがあっても、その自作プログラムのソースコードのコンパイル自体が不可能です(コンパイルしようとしてもエラーになります)。

なお、GTKで開発されたアプリの実行環境と、GTKの開発環境は、別物です。たとえばリナックスのディストリビューションのひとつ Fedora には、GTKの実行環境が標準でインストールされており、そのため、多くのGTK(で開発された)アプリを動かせます。しかし、GTKの開発環境そのものは、Fedoraには標準ではインストールされていません。

Fedora の場合、gtk3-devel など「gtkなんとかdevel」の書式の名前のアプリーケーションが、gtk3対応の開発環境そのもののアプリです。 もし (gtk3-devel でなくて)「gtk3」 だけをインストールしても(初期状態で既に入っているが)、まだgtk3-develはインストールされていない状態です。なので gtk3アプリを開発するためには別途、 gtk3-devel をインストールする必要があります。


そのため、たとえばOSがFedoraなら、GTKの開発環境をインストールするため、コマンド端末で、

sudo dnf install gtk3-devel

などのコマンドを実行して、GTKの開発環境(Fedoraの場合、「gtk3-devel」などの名前)をインストールします。(なお、「gtk3」の「3」は単なるバージョン番号。将来的にもっと高いバージョン番号になる可能性があるので、読者は適宜、判断してください。)

なお2021年現在のFedoraの最新版が搭載しているのはgtk4なので、そのgtk4の開発環境を入れたいなら

sudo dnf install gtk4-devel

でそのまま入る。ただし、gtk3とgtk4は文法が多少違っているので、そのままではコードは使い回しできません。

※ 本ページの現在の版では、主にgtk3を想定して説明しています。gtk4についてはgtk4公式マニュアル などを参照してください。

なおCent OS 7 の場合、

sudo yum install gtk3-devel

になります(CentOS-7-x86_64-LiveGNOME-1908.iso​ で確認。)

なお、名前の似ている gtk-devel またはgtk+-devel などと gtk3-devel とは異なる開発環境です。これら名前の似ている異なる開発環境を入れても、コンパイルできない場合がよくあります。

なので、かならず、 gtk3-devel を入れるようにしてください。


(※ 注意: ) 2019年の近年、gtkの名前が変更され、「gtk+」から「gtk」に名称が変更された。これにともない、いくつかのOSではインストール時のコマンド名の「gtk+-3.0」も「gtk-3.0」などに変更されている可能性があるので、詳しくは実機で確認のこと。

gtkにはバージョン3以外にもバージョン2などがあるので、develのバージョンとコンパイル時のコマンド(「gtk+-3.0」の部分)のバージョンとを合わせる必要があること。

コーディング

[編集]

その後、テキストエディタで、次のようにコードを書いてください。下記コードは、ウィンドウだけのプログラムです。

ウィンドウだけのプログラム
[編集]

gtk3は下記のようなコードになります。(gtk4ではエラーになります。)

コード例 (Fedora 31 で確認ずみ)
#include <gtk/gtk.h>
int main (int argc, char *argv[])
{
  GtkWidget *window; // ウィジェットを格納するためのポインタ変数を宣言。ポインタ変数で宣言するのは単なるGTKでの決まり事なので、気にしなくてイイ
  
  gtk_init (&argc, &argv); // GTKの初期化

  /* ウィンドウを作成 */
  window = gtk_window_new (GTK_WINDOW_TOPLEVEL); // ここでウィンドウを作成
  gtk_window_set_title (GTK_WINDOW (window), "ウィンドウ"); // ウィンドウのタイトル名として「ウィンドウ」と命名しているだけ

  /*  表示することを設定 */
  gtk_widget_show_all (window);

  /*  メインループ */
  gtk_main ();
  return 0;
}

GTKは、C言語のライブラリで、ここではC言語から利用するので、拡張子を「.c」にしてC言語のソースファイルとして保存してください。他のプログラミング言語からは、バインドを介して利用できますが、ここでは説明しません。例えば、上記のコードを、テスト用のファイルという意味で「test.c」で保存したとしましょう。

上記のコードを実行できるようにするには、コンパイルする必要があります。


まず、そのために、さきほどの章でも述べたように、OSに開発環境(例えばFedoraなら gtk3-devel など)をインストールする必要がある。

sudo dnf install gtk3-devel

のようなコマンドでインストールできるはずである。もし gtk3-devel などをインストールしてないと、#include <gtk/gtk.h>でコンパイルエラーになる。


さて、上記のソースコードをコンパイルするために、オブジェクトファイル名がたとえば「object」なら、

gcc test.c -o object `pkg-config --cflags --libs gtk+-3.0`

のようにコマンド端末に入力します。この入力のさい「`pkg-config --cflags --libs gtk+-3.0`」などの設定をつける必要があります。この設定をつけないと、コンパイルエラーになってしまい、コマンド端末から「No such file or directory」(そのようなファイルまたはディレクトリはありません)などとエラー報告されてしまいます。


コンパイルできたら、あとは実行するだけです。 gcc test.c -o object を使った場合、ユーザープロファイルなど出力先として設定されているフォルダーに実行ファイル「object」が作成されていますので、それをダブルクリックするだけで実行できます。

実行すると、作成したウィンドウが表示されます。

なお、上記コード中に

GtkWidget *window; 

とありますが、前半の「GtkWidget」 とはGTKの提供している型(かた)であり、ウィジェットを扱うための型です。

後半の windowは単なる変数名です。別に「 GtkWidget *variable; 」などと書いても構いませんが、その場合は上記コード中のwindowをすべてvariable に置き換えてください。

コード中の「gtk_window_new」が、GTKでウィンドウを作成する関数です。

例のように「◯◯_new」という関数で、何らかの特徴を持ったウィジェットの ひな形 を作れますので、そうして作ったウィジェットの ひな形 に必要な情報を代入していきます。


作成しただけでは、まだ表示の設定はされていません。

なので、「 gtk_widget_show_all (window);」のように表示の設定を追加する必要があります。


1980年代BASICなどとは違い、GTKでは、けっしてウィンドウを作成しただけではウィンドウを自動表示はしないのです。

GTKでは、ウィンドウの作成と、ウィンドウの表示は、それぞれ別の関数です。なので、ウィンドウを表示したいなら、そのために追加的に関数を記述する必要があります。

窓を開くだけの例
[編集]

さきほど紹介したプログラムと似たようなプログラムですが、単に窓を開くだけのgtkプログラムは、次のようにも書けます。 このように、基本的なプログラムでも、何通りかの書き方があります。

さきほどの説明と似たような内容ですが、基本的な重要テクニックなので、復習するのも良いでしょう。

#include <gtk/gtk.h>
int main (int argc, char *argv[]) { // 標準Cのエントリーポイント
  gtk_init(&argc, &argv); // ツールキットの動作に必要なすべてのものを初期化
  GtkWidget *win = gtk_window_new(GTK_WINDOW_TOPLEVEL); // GtkWidget 型のポインタ変数「win」を宣言し、ウィンドウオブジェクトで初期化。
  gtk_widget_show(win); // リアライズしてマップする。
  gtk_main(); // イベントループ
  return 0; // 正常に終了した場合0を返す。
}

このソースコードのファイル名が「test.c」なら、コンパイル時には、

gcc test.c -o test `pkg-config --cflags --libs gtk+-3.0`

のように、フラグをつけて(「`pkg-config --cflags --libs gtk+-3.0`」の部分)コンパイルすること。単に「gcc test.c」としても、コンパイルエラーになって中断してしまう。

pkg-config --cflags --libs gtk+-3.0

を単体で実行してみるとヘッダーファイルへのパスやライブラリやライブラリのパスなどをせっていることがわかります。

エラーなくコンパイルできたら、オブジェクトファイルをダブルクリックするなどして実行すると、ウィンドウが表示される。表示されたウィンドウのタイトルは、オブジェクト名と同じである。つまり、ウィンドウ名は「test」になっている。

ためしにコンパイル時にオブジェクトファイルの名前を「wiki」に変えてみて

gcc test.c -o wiki `pkg-config --cflags --libs gtk+-3.0`

を実行し、作成されたオブジェクトファイルwikiを実行すると、表示されるウィンドウのタイトルも「wiki」になっている。

ここでは、このプログラム(単に窓を開くだけのgtkプログラム)の説明を行ないます。

2行目で、gtk_init()は、g_type_init()を含んだ全体的な初期化を行ないます。gtkを利用する場合には必ず最初にこの関数を呼ぶ必要があります。また、argcとargvを引数として使っていますが、この引数は省略できないので、必ずargc,argvを定義するようにして下さい。


3行目にある「GtkWidget」は型名であり、名前が事前に決まってるので、勝手に名前を変えてはいけません。

3行目ではGtkWidget型の要素*win の定義を、行っています。

3行目ではgtk_window_newというライブラリ関数を使用しています。このgtk_window_newは、CPlusPlusを使った経験があるなら、たとえるなら gtkのwindowのnew関数のようなものです。ほぼ予想される通りの動作をします。(C++では、あるクラスのnew関数はそのクラスの変数を動的に確保し、確保した変数のポインタを返します。)

コード例 1

[編集]

簡単な例として、ウィンドウ内のタイトルバー下の任意の位置に、文字を表示してみましょう。

下記の例のように「◯◯_new」という関数で、何らかの特徴を持ったウィジェットの ひな形 を作れますので、そうして作ったウィジェットの ひな形 に必要な情報を代入していきます。

#include <gtk/gtk.h>
int main (int argc, char *argv[])
{
   gtk_init(&argc, &argv); // GTKの初期化

   /* ウィンドウ、GtkFixedなど各種ウィジェットを作成 */
   GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
   GtkWidget *fixed = gtk_fixed_new();   
   GtkWidget *label = gtk_label_new("Hello, world!");

   /* GtkFixedをウィンドウにパック */
   gtk_container_add(GTK_CONTAINER(window), fixed);

   /* GtkFixed内の(10,20)にラベルを配置 */
   gtk_fixed_put(GTK_FIXED(fixed), label, 10, 20);

   /*  表示することを設定 _all であることに注意 */
   gtk_widget_show_all(window); 

   /*  メインループ */
   gtk_main();
   return 0;
}


さて、座標を指定して文字表示できるようにするには、

gtk_fixed_new 関数でgtk_fixed というという種類のウィジェット()を作成し、
さらにそれをgtk_container_add() 関数でウィンドウに追加する必要があります。

作成した文字列の代入は、 gtk_label_new で作ったウィジェットで可能です。

そして最後に gtk_widget_show_all 関数で、表示したいウィンドウを指定することにより、そのウィンドウごと文字列などを表示するだけです。

変数の文字列としての表示

[編集]

ラベル・ウィジェットは文字列しか表示できないので、数値計算の計算結果を表示するには、文字列への変換が必要です。

C言語の標準ライブラリ関数 sprintf は数値型などを文字列に置き換えることのできる関数ですので、この関数のgtk版であるg_sprintf関数と、ラベル書き換え関数であるgtk_label_set_text関数を組み合わせることによって、なんらかの計算結果の数値もGTKアプリのウィンドウに文字列として表示可能です。

sprintf を使ったウィンドウでの変数表示のテクニックはGTKだけでなくWindowsでもWin32APIプログラミングで使うテクニックなので覚えておきましょう。( 正確には、Windowsでのウィンドウでの文字表示では _stprintf_s を使う。)

また、前提としてGTKでは数値の変数型としては gint型 や gchar型 を使います。これらは、それぞれ、C言語のint型およびchar型のgtk版の型です。


コード例 (CentOS 7 で確認ずみ)
#include <gtk/gtk.h>
int main(int argc, char **argv) {

  /* 数値計算の内容の記述 */
  gint xxx = 3;
  gint fff = 5;
  gint zzz = xxx + fff; // 計算内容

  gchar buf[100]; // 単なるバッファ(仲介)の文字列 変数。
  g_sprintf(
      buf, "計算結果は%d です。",
      zzz); // 置き換え内容の書式の定義。bufに格納された段階であり、まだ置き換えは実行していない。

  gtk_init(&argc, &argv);
  GtkWidget *window, *fixed, *label;

  /* ウィンドウ、GtkFixedなど各種ウィジェットを作成 */
  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
  fixed = gtk_fixed_new();
  label = gtk_label_new("Hello, world! kubetu");

  gtk_label_set_text(GTK_LABEL(label), buf); // ラベルを上書きしている。

  /* GtkFixedをウィンドウにパック */
  gtk_container_add(GTK_CONTAINER(window), fixed);

  /* GtkFixed内の(10,20)にラベルを配置 */
  gtk_fixed_put(GTK_FIXED(fixed), label, 10, 20);

  /*  表示することを設定 */
  gtk_widget_show_all(window);

  /*  メインループ */
  gtk_main();
  return 0;
}


なお、コード中の計算内容にある型の宣言では、じつは、べつに gint や gchar や g_sprintf を使わなくとも、単なるC言語の int 型 、char型、sprintf を使ってもコンパイルできてウィンドウを作成できます。

ですが、 単なる sprintf​ 関数は gint などgtk版の型を認識しないので、もしも gint型 や gchar型 を使ったら必ず(sprintf​でなく) g_sprintf​ で変換してください。

ウィンドウのサイズ設定

[編集]

ウィンドウのサイズは、 gtk_widget_set_size_request(window, 640, 480); で指定できます。

ネット上では、

gtk_widget_set_size_request(window, 640, 480);
  {

  }

と続けて、 { } 括弧で括られたブロックが書かれる場合がありますが、この括弧は限定のスコープを作るためのもので、必要ないなら無くても構いません。 実際、下記コードのように gtk_widget_set_size_request の後に { } の無いコードを書いても、コンパイル可能であり、正常にウィンドウサイズが更新されます。(Fedora 28 で動作を確認ずみ。)

#include <gtk/gtk.h>
int main (int argc, char **argv){

   gtk_init(&argc, &argv);
   GtkWidget *window, *fixed, *label;

   /* ウィンドウ、GtkFixedなど各種ウィジェットを作成 */
   window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
   gtk_widget_set_size_request(window, 640, 480); // ウィンドウのサイズを設定

   fixed = gtk_fixed_new();   
   label = gtk_label_new("Hello, world!");

   /* GtkFixedをウィンドウにパック */
   gtk_container_add(GTK_CONTAINER(window), fixed);

   /* GtkFixed内の(10,20)にラベルを配置 */
   gtk_fixed_put(GTK_FIXED(fixed), label, 10,20);

   /*  表示することを設定 */
   gtk_widget_show_all(window);

   /*  メインループ */
   gtk_main();
   return 0;
}

コード例 3

[編集]
#include <gtk/gtk.h>
int main (int argc, char **argv){

   gtk_init(&argc, &argv); // GTKの初期化
   GtkWidget *window, *fixed, *button, *label; // ウィジェットを格納するための変数を宣言

   /* ウィンドウ、GtkFixedなど各種ウィジェットを作成 */
   window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
   fixed = gtk_fixed_new();
   button = gtk_button_new_with_label("aaa");
   label = gtk_label_new("bbb");

   /* GtkFixedをウィンドウにパック */
   gtk_container_add(GTK_CONTAINER(window), fixed);

   /* GtkFixed内の(100,100)にボタンを置き、(200,200)にラベルを配置 */
   gtk_fixed_put(GTK_FIXED(fixed), button, 100,100);
   gtk_fixed_put(GTK_FIXED(fixed), label, 200,200);

   /*  表示することを設定 */
   gtk_widget_show_all(window);

   /*  メインループ */
   gtk_main();
   return 0;
}



解説

[編集]

上記のコードのように、メインループgtk_main()までの設定をもとに、メインループgtk_main()でウィンドウなど作成したGUIアプリを表示しつづけます。

メインループがないと、そのまま関数の最後に到達して終了してしまうので、何もウィンドウは表示されません。

ループといっても、ウィンドウ表示中に他の作業もできますので、安心してください。

手前にある gtk_widget_show_all 関数は、ウィンドウ表示の関数ではなく、メインループ実行時にウィンドウ表示することを設定する関数です。


さて、上のコードの例ではGtkFixedウィジェットを作成しボタン、ラベルのウィジェットをGtkFixedウィジェット内に配置しています。このようにウィジェット内のどこにでも他のウィジェットを収納できるウィジェットがGtkFixedウィジェットです。

実際にはgnome-panelの中でもGtkFixedウィジェットが使用されており、"オブジェクト"の配置を自由な位置に行うために役立っています。ただし、GUIを提供する以外に他の項目を設定するために、GtkFixedクラスを継承する形でウィジェットが提供されています。

実際に提供されているウィジェットはPanelWidgetウィジェットと呼ばれ、ソースコード中では./gnome-panel/panel-widget.hで定義されています。このクラスに対応する構造体は最初にGtkFixedを持っていますが、これとCの機能を使うとw:en:glibでいう"継承"を行うことができます。詳しくはOSS開発ツール GUIツールキットを参照してください。

ここまでで、gnome-panel内で"オブジェクト"の配置はGtkFixedクラスの機能によってなされていることが分かりました。他に興味ある点としては、個々の"オブジェクト"がどのように定義されているかや、オブジェクトを導入するための操作などがあります。例えば、"オブジェクト"が配置できる部分を右クリックすると、メニューが開き、追加するオブジェクトを選ぶことができます。この操作はGtkMenuなどで提供される機能ですが、実際にソースコード中でどのようにGtkMenuが用いられているかも調べることができます。


図形の描画

[編集]

図形として線分を表示したり、円などを表示する機能は、図形描画ライブラリが担当している。

最近のGTKでは、図形描画ライブラリにcairoというフリーの画像描画ライブラリを採用している。特に追加インストールすることなく、cairoの図形描画関数が使用できる。 cairo自体は、GTKとは別のアプリなので、詳細はcairoの入門書を参照のこと。

コード例を示すと下記のようになります。

コード例 (CentOS-7-x86_64-LiveGNOME-1908.iso​ で確認ずみ)
#include <gtk/gtk.h>
//#include <gdk/gdk.h>

GtkWidget *window;

static gboolean kansuu(GtkWidget *widget1, cairo_t *handle, gpointer abcde)
{
  //ハンドルの作成
  handle = gdk_cairo_create(gtk_widget_get_window(widget1));

    //線の性質の定義
    cairo_set_line_width (handle, 10); // 線の太さ
    cairo_set_source_rgb (handle, 1.0 , 0.0 , 0.0); // 線の色
    cairo_move_to (handle, 00, 50); // 線の起点
    cairo_line_to (handle, 300, 300); // 線の終点

    // 実際に線を引く関数
    cairo_stroke (handle);

  //長方形の描画
  {
    //長方形(x,y,width,height)の大きさを指定
    cairo_rectangle(handle, 50.0, 20.0, 30.0, 90.0);
    //線の色の指定(Red,Green,Blue)
    cairo_set_source_rgb(handle, 0.0, 0.0, 1.0);
    //実際に長方形を塗りつぶす関数
    cairo_fill(handle);
  }

  cairo_destroy(handle);
  return FALSE;
}

int main(int argc, char **argv)
{
  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
  gtk_window_set_title(GTK_WINDOW(window), "テスト");
  gtk_widget_set_size_request(window, 420,200);

  //ウインドウに図形を描けるように設定
  gtk_widget_set_app_paintable(window, TRUE);


  //ウインドウが表示されたときにkansuu()を呼び出す
  g_signal_connect(G_OBJECT(window), "draw", G_CALLBACK(kansuu), NULL);

  gtk_widget_show_all(window);
  gtk_main();

  return 0;
}

2019年の現在、GTK3で cairo を使うと、いくつかのライブラリ関数を別の関数(たとえば gdk_window_begin_draw_frame() など)に置き換えるようにコンパイラを介して警告されます

[ユーザー名@localhost ~]$ gcc gtktest.c -o object `pkg-config --cflags --libs gtk+-3.0`
gtktest.c: 関数 ‘kansuu’ 内:
gtktest.c:9:3: 警告: ‘gdk_cairo_create’ is deprecated: Use 'gdk_window_begin_draw_frame() and gdk_drawing_context_get_cairo_context()' instead [-Wdeprecated-declarations]
    9 |   handle = gdk_cairo_create(gtk_widget_get_window(widget1));
      |   ^~~~~~~
In file included from /usr/include/gtk-3.0/gdk/gdk.h:33,
                 from /usr/include/gtk-3.0/gtk/gtk.h:30,
                 from gtktest.c:1:
/usr/include/gtk-3.0/gdk/gdkcairo.h:35:12: 備考: ここで宣言されています
   35 | cairo_t  * gdk_cairo_create             (GdkWindow          *window);
      |            ^~~~~~~~~~~~~~~~

が、しかし、

これらの関数( gdk_window_begin_draw_frame() など)は Gnome開発者が使うものであるので(IRCでGnome開発者がそう解答した(2020年4月26日、日本時間で9時ごろ) )。なので 一般の GTKアプリケーション製作者は、この関数( gdk_window_begin_draw_frame() など)は用いないとの事である。

今後は、別の方法に置き換わっているとの、Gnomeの解答のこと。

gdk_cairo_create からの新方式への移行ガイドラインについては

https://developer.gnome.org/gtk3/stable/ch26s02.html#id-1.6.3.4.11

を参照せよ、との解答。


いまのところGTK開発元のGnomeコミュニティが、ロクに それらのライブラリ関数のマニュアルを整備してない状況です(形式的にリファレンス[1]


なお、上記コードは Fedora31 および31以降ではバグります。


もし将来的にcairoが使えなくなったら、フォークするか、でなければGTKでなくQtなどの別のデスクトップ環境に移行しましょう。どうせ組込系ではGTKよりもQtのほうが主流です。

この文章は要らないですね。マニュアルにちゃんと書かれてますよ。あと、基本ボランティア活動なんですから、文句があるなら、あなたも協力したらいかがですか?折角の素晴らしいチュートリアルなのに、この辺のせいですっかり台無しです。

ちなみに、上記のコード例の場合、このように警告されます。

‘gdk_cairo_create’ is deprecated (declared at /usr/include/gtk-3.0/gdk/gdkcairo.h:35): Use 'gdk_window_begin_draw_frame() and gdk_drawing_context_get_cairo_context()' instead [-Wdeprecated-declarations]
   handle = gdk_cairo_create(gtk_widget_get_window(window));
コードの解説

GTK3 では、まず main 関数側で、図形の描画をできるように設定を宣言する必要があります。


なお、ネットに転がってるコード例をみると、cairo の呼び出し方では、一般に任意の自作の関数(上記のコード例では kansuu)を介して cairo を呼び出します。


cairo による図形描画プログラムを作成するとき、windows APIプログラミングでいうところのハンドルのような物を宣言する必要があります。

上記コードでは

handle = gdk_cairo_create(gtk_widget_get_window(widget1));

で、ハンドル作成しています。

なお、関数宣言のさいの static gboolean kansuu(GtkWidget *widget1, cairo_t *handle, gpointer abcde) の際に、すでに引数として cairo_t *handle のように宣言されており、この時点ですでにハンドル作成などのための必要なメモリの確保を行っているものと考えられます。

cairo_t型とは、ハンドルのようなものを定義するための型です。そもそも一般的にC言語では、型の宣言とは、メモリの確保でもあります。

ともかく、cairo_t型の宣言のさいに既にメモリは確保されているので、あとは実際にハンドルの作成をすれば済むだけなので、 よって

handle = gdk_cairo_create(gtk_widget_get_window(widget1));

で、実際にハンドル作成を実行するわけです。

この画像描画の説明でいう「ハンドル」とは、たとえるなら絵を書くときのキャンバスのようなものです。

線を引いたりなど図形を描画するときは、初心者には わずらわしいですが、どのハンドル(キャンバス)に図形を描画するのかを、各関数で宣言する必要があります。


GTKでは図形の性質の定義と、実際に図形の描画を実行する関数とは、異なる関数になります。(Winodws APIと同様。)

cairo_stroke の関数で、実際に線分の描画を実行します。


そして、使用し終わったら、destroy でハンドルを破棄するのが一般的です。(メモリの圧迫を防ぐため。)(Winodws APIでも同様に、使い終わったハンドルは破棄を宣言する。)


この章を書くに当たり参考にした文献
素人の独学GTK+3.0 8章:タイムカウントとGDK3 2019年9月28日に閲覧


Cairoライブラリの描画関数
[編集]

GdkWindowに対して、gdk_cairo_create関数を使うことで、w:Cairoライブラリの描画コンテキストを得ることができます。ここで、Cairoは2D描画用のライブラリで、GTK+2が依存しているライブラリの1つです。gdk_cairo_create関数を使うと、GtkDrawingAreaウィジェットの中で、Cairoの描画関数を用いた図形の描画ができます。

Cairoライブラリには、GDKに存在しない描画機能があります。例えば、GDKの描画関数にはw:ベジエ曲線を描くための関数は存在しません。Cairoライブラリにはこれを描くための関数が用意されています。GtkDrawingArea内で、これらの機能を利用したいときにはCairoライブラリを使うとよいでしょう。

直線を用いた例
[編集]

Cairoライブラリを使う場合には、expose_cbは次のようになります。まず最初にGdkWindowからCairoコンテキストを作ります。

void expose_cb(GtkWidget *da){
  cairo_t *cr = gdk_cairo_create(da->window);

cairo_t*はCairoコンテキストで、Cairoの関数によって画面の描画を行える長方形です。gdk_cairo_create関数はGdkWindow*を引数に取り、GdkWindow*の全体からCairoコンテキストを作ります。Cairoコンテキストは描画が終わった時に、cairo_destroy関数で解放する必要があります。

Cairoの関数はCairoコンテキスト内に"パス"を作成します。例えば、ある点(x,y)から(a,b)に向けた直線を引く場合を考えます。この場合、Cairoコンテキストに対して、1つの直線のパスを作ります。

Cairoコンテキストには"現在の位置"という量があります。まず最初に、"現在の位置"をパスの先端に動かし、その後位置をパスの後端に動かします。パスの先端に動かすには、cairo_move_to関数を使います。

cairo_move_to(cr, x, y);

次に、cairo_line_to関数でCairoコンテキスト中にパスを作成します。

cairo_line_to(cr, a, b);

これによって(x,y)から(a,b)へのパスをひくことができます。実際にこの直線を描画するには、cairo_stroke関数を使います。

cairo_stroke(cr);

描画する際に色を変えることもできます。このためには、cairo_set_source_rgb(a)関数を使います。最後のaはw:アルファチャンネルを表すパラメータです。これらの関数は

cairo_set_source_rgb(cr, R, G, B);

または、

cairo_set_source_rgba(cr, R, G, B, A);

で表されます。ただし、R, G, Bは、0-255ですが、Aは0-1で表されます。

また、描画の色はパスに対して設定することはできないため、パスを定義した後 色を変えたとしても、cairo_strokeを実行する前なら、パスの色は変更した後の色で描画されます。

cairo_move_to, cairo_line_to以外に、cairo_rel_move_to, cairo_rel_line_toという関数もあります。これらの関数は対応する関数と同じ働きをしますが、移動の位置を"現在の位置"からの相対位置で決めます。

  • 注意

Cairoの関数は対応するw:PostScriptの関数と等しくなっています。例えば、三角形を描くPostScriptファイルは次のようになります。

20 20 moveto
50 230 lineto
230 50 lineto
20 20 lineto
stroke
showpage
実行例

moveto, lineto, strokeはそれぞれ対応するCairoの関数と似た働きをします。

他に、パスを作る関数として、cairo_rectangle, cairo_arc関数などがあります。これらの関数についてはCairoのリファレンス[2]を参照してください。

ベジエ曲線を用いた例
[編集]

w:ベジエ曲線は一般的な3次曲線で、w:ベクタ図形を記録するためによく用いられます。例えば、w:PostScriptw:SVGは曲線としてベジエ曲線を使っています。

ベジエ曲線を使う関数はcairo_curve_to, cairo_rel_curve_toの2つです。具体的には、cairo_curve_toは次のように使います。

cairo_curve_to(cairo_t*, x1, y1, x2, y2, x3, y3)

は"現在の位置"から(x3, y3)まで曲線をひきます。曲線の曲がり具合は、(x1, y1),(x2,y2)によって定めます。これらの点については、Cairoのリファレンスとw:ベジエ曲線を参照してください。

実行例

コンポーネント

[編集]
"メニューバー"オブジェクトについて
[編集]

ここからは、個々のオブジェクトについて詳しく調べていきます。gnome-panelの多くの設定で使用されているオブジェクトに、"メニューバー"があります。

このオブジェクトはアプリケーションメニューと場所メニュー、アクションメニューの3つのメニューから構成されています。それぞれのメニューをクリックすると種々のメニューが提供されます。アプリケーションメニューではシステムに存在するアプリケーションの起動が扱われ、アクションメニューでは"画面のロック"や"ログアウト"などのGNOMEデスクトップ全体に関わる事柄が扱われます。

メニューバーは大抵の設定でただ1つだけパネル内におかれているので、この部分は動かせないと思われがちです。しかし、実際にはこの部分は取り去ることが可能であり、またパネル内に複数置くことも可能です。

メニューバーのクラスはソースコード内では./gnome-panel/panel-menu-bar.hで定義されています。この中のPanelMenuBar構造体を確認すると分かる通り、このクラスはGtkMenuBarを継承しています。そのため、このクラスは基本的にGtkMenuBarと同じ動作をします。GtkMenuBarを使ったサンプルとして次のようなメニューの例があげられます。

 int main (int argc, char **argv){
   gtk_init(&argc, &argv);
   GtkWidget *window, *menubar, *menuitem, *menu;

   /* ウィンドウを作成 */
   window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

   /* メニューバーを登録 */
   menubar = gtk_menu_bar_new();
   gtk_container_add(GTK_CONTAINER(window), menubar);

   /* メニューバーの項目を登録 */
   menuitem = gtk_menu_item_new_with_label("mmm");
   gtk_menu_shell_prepend(GTK_MENU_SHELL(menubar), menuitem);

   /* 項目から派生するメニューを登録 */
   menu = gtk_menu_new();
   gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), menu);

   /* 派生した先に登録する項目を作成 */
   menuitem = gtk_menu_item_new_with_label("lll");
   gtk_menu_shell_prepend(GTK_MENU_SHELL(menu), menuitem);

   /* メニューの項目に対応するコールバック menu_cb を登録 */
   g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(menu_cb), NULL );

   gtk_widget_show_all(window);
   gtk_main();
   return 0;
 }
GtkMenuを使った例。この画像はできたGUIアプリケーションを見ながら、手で描いたものである。

GtkMenuBarはGtkMenuItemを書き込むことでメニューを作成することができるウィジェットです。 上の例はmmmという名のメニューを作成し、それをクリックしたときlllと書かれたメニューを表示し、更にその中のlllと書かれた部分をクリックすることで関数menu_cbを実行するというプログラムです。メニューの項目は増やせるので、コールバック関数をいろいろなアプリケーションの起動を行う関数とすることで、ランチャーの役目を果たすアプリケーションとすることができます。

ポップアップメニュー
[編集]

gnome-panelではパネルの各所を右クリックすることでポップアップメニューを得ることができます。この操作はGUIを使った操作としてはよく見られるもので、どのように実現されるかが気になる所です。実は、この操作はGTK+のクラスであるGtkMenuの操作として典型的なものです。ここではGtkMenuのポップアップの例を見るとともに、実際にこの操作がどのようにgnome-panel中で用いられているかを見て行きます。

GtkMenuのポップアップの例として、次のサンプルをあげます。

 int main (int argc, char **argv){
   gtk_init(&argc, &argv);
   GtkWidget *menuitem, *menu;

   /* メニューの例を参照 */ 
   menu = gtk_menu_new();
   menuitem = gtk_menu_item_new_with_label("lll");
   gtk_menu_shell_prepend(GTK_MENU_SHELL(menu), menuitem); 
   g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(menu_cb), NULL ); 
   gtk_widget_show_all(menu);

   /* ウィンドウから離れてメニューを表示*/
   gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, 0,
      gtk_get_current_event_time() );

   /*メインループ*/
   gtk_main();
   return 0;
 }

上の例では"lll"と書き込まれたメニューを作成した後、そのメニューをgtk_menu_popupによって表示します。メニューが表示される場所はgtk_menu_popupの引数によって変更できるのですが、上の例ではgtk_menu_popupが実行された時点でのマウスカーソルの場所になります。実行例を見るとわかるのですがこの例は何も無い部分に突然メニューが表示されるため、やや非直観的です。普通の例ではGtkWindow等のマウスイベントを設定し,マウスのボタンが押されたときにメニューが表示されるようにします。ただし、GtkWindowは通常ではマウスのボタンに対応するイベントを持たないため,その点を補う必要があります。 このためには、gtk_widget_add_eventsかGtkEventBoxを使う方法がありますが、ここではgtk_widget_add_eventsを用いる方法を述べます。GtkEventBoxについてはGTK+のリファレンス等を参照してください。以降の説明ではある程度Xプログラミングの経験があると理解が容易になります。

gtk_widget_add_eventsはウィジェットがGTK+の背後で動いているウィンドウシステムから、新しいイベントを得るように設定する関数です。背後のウィンドウシステムの代表例はw:X Window SystemですがUnix系のシステムでないなら他のものになることもあります。GTKではウィンドウシステムの値を直接使わなくてもすむよう、w:GDKというライブラリを用いています。GDKはGTK+とともに配布されるライブラリです。

gtk_widget_add_eventsでは引数として(GtkWidget *, GdkEventMask)を取ります。ここで、EventMask(イベントマスク)は対応するイベントをXなどから受け取るかを定めるビット列です。例えば,Xを用いてイベントを処理する場合にはXSelectInputなどを用いますが,この関数の引数としてイベントマスクが用いられます。詳しくはXプログラミングを参照してください。ここで扱うGdkEventMaskも同種の値です。

実際にマウスボタンのイベントを見るには,GdkEventMaskとしてGDK_BUTTON_PRESS_MASKを用います。結局GtkWindowを作った後,

gtk_widget_add_events(window, GDK_BUTTON_PRESS_MASK);

を実行すると,ウィンドウ内でマウスボタンの操作を見ることができるようになります。この関数の後には,GTKのイベントである"button_press_event"を用いてマウスのボタンを扱うことができます。

ここまでのことを用いて,メニューを作成してからメインループに至るまでの部分は次のようになります。


 GtkWindow *window;
 window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
 gtk_widget_add_events(window, GDK_BUTTON_PRESS_MASK);
 g_signal_connect(window, "button_press_event", G_CALLBACK(window_button_cb), menu);
 gtk_widget_show(window);

ただし、menuは上で作成したGtkMenuと同一です。ここで、window_button_cbは次のように与えます。


 void window_button_cb(GtkWidget *window, GdkEventButton *button, GtkWidget *menu){
   gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, 0,
       gtk_get_current_event_time() );
 }


アプリケーションの起動
[編集]

アプリケーションの起動を行うためには、 コールバック関数として定義された関数(上の例ではmenu_cbと与えられている)の中で、新たな"プロセス"を作る必要があります。"プロセス"はOSが複数のアプリケーションを同時に動かすときの単位で、それを作る方法はOSによって異なっています。Unix系のOSではプロセスを作る関数は大抵w:forkと呼ばれます。forkはプロセスを作成し、新たに作成されたプロセスのIDを返します。また、作成されたプロセスで実際にあるアプリケーションを起動する関数として、Unix系のOSではexec系の関数が与えられます。execは与えられる引数によっていくつかの似た関数が提供されます。

forkとexecを使った例

実際にそれぞれのメニューバーの項目に対応するコールバックを設定する操作は、./gnome-panel/panel-menu-item.c内の関数panel_menu_items_append_from_desktop内で行われています。この関数は第1引数にメニューの項目、第2引数に起動するアプリケーションの名称を取り、これを新たに作成したメニューの項目に与えています。

既にメニューの項目をクリックしたときの動作を与える方法として、

g_signal_connect(menuitem, "activate", menu_cb, NULL);

を使う方法を紹介しました。ここで、menuitemはここで内容を与えるメニュー項目であり、menu_cbは実際にこのメニューの項目がクリックされたときに実行させたい関数です。指定したアプリケーションを起動して、新たなプロセスを実行するためには、この関数はforkやexecを用いる関数である必要があります。

  • 注意

g_signal_connectはw:en:glibで定義された関数で、"シグナル"が定義されたGObjectクラス及びそれを継承したクラスに対して、コールバック関数を与える関数です。ここで、"シグナル"はおおよそコールバック関数と同じ意味で、Unixでいう"シグナル"(他のプロセスに影響を与える機構)とは無関係です。

panel_menu_item_append_from_desktop内でもこの関数が用いられており、コールバック関数として同じファイル内で定義された、関数panel_menu_item_activate_desktop_fileを取ります。この関数はクリックされたメニュー項目の情報に加えて、そのメニュー項目がクリックされたときに実行されるべきアプリケーションの名称を引数として受け取ります。ここで、この関数はアプリケーションの名称を引数として与えながら、関数panel_ditem_launchを呼びます。launchの名から分かる通り、この関数は実際にアプリケーションの起動を行います。launchは"起動する"、"発射する"などの意味を持つ英単語です。

ここまでも既に長い道のりでした。しかし、ここから実際にforkが呼ばれるまでに、更にいくつかのライブラリを見る必要があります。ある意味でlaunchと名の付いた関数を見付けた時点で、この関数がプログラムの実行を行う可能性は高いため、そこで探索を終える方法もあるでしょう。ここでは、一応最後まで関数の流れを追ってみます。

panel_ditem_launchは、./gnome-panel/panel-util.c内で定義されています。この関数はいくつかの準備を行った後、関数gnome_desktop_item_launch_on_screenを呼びます。実はこの関数はgnome-panel内の関数ではないため、ソースを読もうと試みる人は、この関数が定義されたソースを求めて方々を探す必要があります。実際にはw:googleなどを試してみるのがよいでしょう。

実際にこれを探すと、関数gnome_desktop_item_launch_on_screenは、gnome-desktopというライブラリ内の関数だとわかります。このライブラリもGNOMEのサイトから提供されているので、必要ならダウンロードしてください。実際には、この関数は、gnome-desktop-x.x.x/libgnome-desktop/gnome-desktop-item.cで定義されています。

この関数ではいくつかの引数のチェックを行った後、同じファイル内の関数ditem_executeを呼びます。この関数は関数の名前の最初に、gnome_desktop_...がついていません。このような関数は大抵staticをつけて宣言されており、そのファイル内だけで用いられる関数です。これは、staticをつけた関数は外部からは参照できないことを利用しています。他のファイルの関数名と重複することがないため、単純な名前でもよい訳です。

ditem_executeは様々なチェックなどを行った後、関数g_spawn_asyncを呼びます。この関数はg_から始まっていますが、GNOMEのアプリケーションでこの名称が出て来た場合、この関数は大抵w:en:glibの関数です。例えば、コールバックを与える関数であるg_signal_connectがglibの関数であることは既に述べました。

多くの関数をたどって来ましたが、g_spawn_asyncは事実上最後の関数です。この関数はglib-x.x.x/glib/gspawn.c内で定義されていますが、この関数はいくつかの関数を経て、関数fork_exec_with_pipesという関数を呼びます。この関数は名前の通りforkとexecを呼ぶ関数です。

ここまでで一応gnome-panelのメニュー項目がクリックされてから、実際に新たなプロセスが作成されるまでの道のりを見て来ました。もちろんただプロセスを作ることが目的なら、gnome-panel内で直接forkを呼ぶことも可能です。敢えてライブラリを使うのは、例えばw:Windowsを使うときにはこの方法が使えないことがあげられます。これは、Windows上でプロセスを作るときにはforkではなく別のw:Windows APIを使う必要があるからです。実際glibのgspawn.cがあるディレクトリ内には、gspawn-win32.cというWindows向けの関数も定義されており、glibライブラリをクロスプラットフォームライブラリにするよう試みているようです。

gconfを用いた設定

[編集]

ここまでで一応ランチャーとしての役目を果たすための機能を見て来ました。メニューを表示し、そのメニュー項目と対応するアプリケーションを起動することは、ランチャーの機能としては基本的です。ただし、ランチャーが扱えるアプリケーションは、プログラムのふるまいを外部から制御する機構がないのなら、ランチャーを作った人間がプログラム内に書き込んだアプリケーションに限られます。これでは新たなアプリケーションが加わった時にランチャーの振舞いを拡張することができないことになり、不便です。

ソフトウェアの動作を制御するためには、"設定ファイル"を使った方法がよく用いられます。例えば、XサーバやWebサーバ の動作を変更するために、これらの設定ファイルを書き直すことは特にホビーとしてのPC-Unixではよく行われます。

ただし、アプリケーションの動作を制御するために設定ファイルを使った方法を用いる場合には、その設定がアプリケーション内に取り込まれるタイミングが重要になります。例えば、設定ファイルを読む操作がアプリケーションの起動時にしか行われない場合、設定ファイルを変更した後アプリケーションの動作を変更するには、設定ファイルを書き直すたびに対応するアプリケーションを起動し直す必要があり、少し不便です。

より進んだ方法では、設定ファイルを書き直した後、そのことをアプリケーションに伝達する機構を用意しています。ここで、設定ファイルを書き直すプロセスは、一般には設定ファイルを利用するプロセスとは異なっています。このため、設定が変更されたことをその設定を使用しているアプリケーションに伝達するには、"プロセス間通信"の機構を用いる必要があります。

プロセス間通信は異なったプロセスの間で情報を伝達する機構です。この機構もプロセスの操作と同様OSによって提供される機構であり、異なったOSでは異なった動作をします。Unixにおける代表的なプロセス間通信には、w:ソケットを使った方法があげられます。ソケットは異なったプロセスからの情報を受け取るための一般的な機構ですが、これは異なったコンピュータ上にあるプロセスに対しても用いることができます。例えばLinuxでは、w:TCPの通信を行うためのソケットを提供していますが、この通信手法はw:インターネットのあらゆるサービスを提供するための手法として用いられています。

GNOMEでは、設定を扱うためにw:en:gconfと呼ばれるライブラリを利用します。これは、設定を扱うための1つのサーバ(gconfd)を導入し、そのサーバに、サーバ上の設定を参照しているgconfクライアントを記憶させておき、設定が変更された際に、そのことをgconfクライアントに伝える機構です。簡単な例では、gconfdが保持している設定は利用者のホームディレクトリ~の~/.gconf/以下に記録されます。

  • 注意

Unixの"シェル"では、利用者のホームディレクトリを~の記号で表します。設定によるのですが、このディレクトリは大抵/home/user_name/以下におかれます。ただし、user_nameはそのコンピュータに登録されている利用者の名前です。PC-Unixでは大抵利用者はそのパソコンの所有者1人だけなので、/home以下に直接設定ファイルをおけばよいようですが、多くの利用者が異なった設定でgconfを使う場面を想定してこのような作りになっています。

実際のシステムでは~/.gconfは次のようになります。

$ls
apps/  desktop/

これはfedora core 5での~/.gconf/内のファイルを表示した例です。ここには2つのディレクトリしかありませんが,apps/以下にはw:en:Eye of GNOME, w:geditなどの各種アプリケーションの設定が記録されています。これらはそれぞれ

eog/ gedit/

などの名前を与えられています。

gconfの動作を見るために、gconfが提供するツールを使った実験をしてみます。gconfはGConf-x.x.xというライブラリとしてGNOMEのサイトから配布されているのですが、その中には、GConf-x.x.x/gconf/gconftool.cで与えられるファイルが存在します。ここで、GConfのバージョンはGConf-2.14.0を使いました。このファイルは、gconfの設定内容を変更したり参照するための簡単なツールを提供します。このツールはgconftoolと呼ばれます。

ここでは、このツールを用いてgconfを使ってみます。既に~/.gconf/の中を見てみました。ここでは、gconftoolを使ってこのディレクトリ以下に新たな設定項目を作ります。もちろんこの項目は実際にアプリケーションで使われる項目ではないのですが、gconfの動作を見る上では便利です。具体的には、~/.gconf/以下に、/aaa/bbbという項目を作ります。ここで、/aaaは項目が配置されるディレクトリ名を表し、bbbが実際に記録される設定の名前です。もちろんこの階層はいくらでも深くすることができますが、ここではこの程度でよいでしょう。

また、gconfで設定される項目には"型"が必要になります。"型"にはint, bool, float型がなどがあります。これらのうち、intとfloatはC言語の対応する型と同じで、intは整数、floatは実数を表します。boolは例えばCPlusPlusなどでは導入されているのですが、"真"、"偽"の2つの値だけを持つ型です。C言語では1 を"真"、0を"偽"などとしてint型で代用することができます。ここでは、bbbの型はboolで、値を"真"、つまりtrueとします。

具体的に、/aaa以下にbool型の項目bbbを値trueで設定するには、

$gconftool-2 -s /aaa/bbb -t bool true

とします。ここで、-sは項目を指定するための引数であり、-tは項目の型を指定する引数です。

実際にこの操作を実行すると、~/.gconf/は次のようになります。

$ls
aaa/  apps/  desktop/

新たにaaa/というディレクトリが加わっている様子がわかります。aaa/内のファイルを表示すると,

$ls
%gconf.xml

が得られます。ここではgconfの設定はw:XMLファイルに記録されています。この中身は,

<?xml version="1.0"?>
<gconf>
        <entry name="bbb" mtime="1168623910" type="bool" value="true">
        </entry>
</gconf>

で与えられます。この中では3行目の"bbb", "bool", "true"から、上で扱った内容が記録されている様子がわかります。

gnome-panelでのgconf
[編集]

ここまででgconfの基本的な使い方を見て来ました。gnome-panelでもパネル内のどの位置にどのオブジェクトが配置されているかなどをを記録するために、gconfを用いています。ここではまず、gnome-panelがどのようにgconfdから設定を受け取り、設定への変更を取得しているかを見て行きます。

gnome-panelの起動
[編集]

ここではgnome-panelがどのように起動するかを見て行きます。通常のアプリケーションと同様、gnome-panelは起動時に設定を参照してどこにオブジェクトを配置するかなどを決めます。この設定は設定ファイルではなく、gconfを用いて行われるのですが、ここでは実際にアプリケーションの起動時にどのようにgconfが用いられているかを見て行きます。

大抵のGNOMEアプリケーションではアプリケーションの起動はmain関数から始まります。これは普通のCプログラムと同じです。一方、w:Windows APIを用いたWindowsのcプログラムでは、アプリケーションはWinMain関数から始まります。これはGUIアプリケーション一般の性質という訳ではないので注意してください。

そのため、アプリケーションの起動の様子を見るためには、このアプリケーションのmain関数を探す必要があります。gnome-panelでは、./gnome-panel/main.cに、アプリケーションのmain関数が定義されています。main関数では各種の初期化や設定が行われているのですが、その中でgconfとの相互作用を扱う関数として、関数panel_profile_loadが呼ばれています。この関数は、./gnome-panel/panel-profile.c内で定義された関数で、"いくつのパネルがあるか、それぞれのパネルにはどのようなオブジェクトやアプレットが配置されているか"などの各種情報をgconfdから受け取り、対応するウィジェットを作成しています。

実際には関数panel_profile_loadの最後で、関数panel_applet_load_queued_appletsが呼ばれています。 この関数は、./gnome-panel/applet.c内で定義されていますが、おおよそ関数panel_applet_load_idle_handlerを呼び出す関数です。panel_applet_load_idle_handlerもapplet.c内の関数ですが、この中では追加されたオブジェクトのタイプに対して、動作を変更するための、大きなswitch文が用いられています。switch文はC言語の制御構造の1つで、複数の条件があるときに、それらの条件に対して場合分けを行う文です。詳しくはC言語内の説明を参照してください。このswitch文はgconfが与えた内容に対して作成するウィジェットを変更するための場合分けで、これ以降各ウィジェットを作成する手順はウィジェットの種類によって様々です。 具体的には追加されたオブジェクトが"メニューバー"だった場合にはpanel_menu_bar_load_from_gconfを呼んでいます。この関数は設定に従って対応する"メニューバー"オブジェクトを作成する関数です。この関数の詳細を追うこともできますが、一応gconfの設定を読んだ後に、設定に応じて各種のオブジェクトを与える過程が得られたので、アプリケーションの起動に限った話はここまでとします。

各種設定の変更
[編集]

ここまでで、gnome-panelが起動するときに、gconfが与える設定に従って、各種ウィジェットを見分ける方法を見て来ました。実際にはgnome-panelはgnome-panelが動作している最中に設定が変更されても、それに対応してウィジェットを作成する機構を持っています。これはgconfの機能を活用した機構です。

具体的には、gnome-panelはパネルの自由な位置に利用者が指定したオブジェクトをgnome-panelを再起動すること無く導入するためのGUIを持っています。このGUIは、gnome-panelの設定の変更とそのことのgnome-panelへの伝達を同時に行っており、設定を変更するたびにgnome-panelを起動しなおす手間を省いています。 ここでは、このGUIを用いて、gnome-panelに各種オブジェクトを配置する方法を見て行きます。

まず、gnome-panelの設定を変更するためのGUIとして、gnome-panel/panel-addto.c内の関数が用いられています。このファイル内の関数はGtkDialogを用いて利用者からgnome-panel内で変更したい設定を取得しようとします。ここではこのウィジェットをAddToダイアログと呼びます。

ここで、GtkDialogはGtkWindowを継承したクラスで、GtkWindowに"OK", "キャンセル"などの各種ボタンを与えるクラスです。一般的なGtkDialogの例として、次のようなサンプルがあげられます。

 #include <gtk/gtk.h> 
 int main (int argc, char **argv){
     gtk_init(&argc, &argv); 
     GtkWidget *dialog, *label;

     /* ダイアログの作成 */
     dialog = gtk_dialog_new_with_buttons(NULL,
       NULL,
       0,
       "はい",
       1,
       "いいえ",
       2,
       NULL);
     label = gtk_label_new("あああ\n");
     gtk_container_add(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox), label);
     gtk_widget_show_all(dialog);

     /* 応対(response)の取得*/
     int response = 0;
     response = gtk_dialog_run(GTK_DIALOG(dialog));
     switch(response){
 	case 1:
 	    g_print("はい\n");
 	    break;
 	case 2:
 	    g_print("いいえ\n"); 
 	    break;
 	default:
 	    break;
 	}
     return 0;
 }
解説


AddToダイアログの動作はgconfの機能を使っています。具体的には、gconfの設定のうち、オブジェクトの種類や配置が記録されている部分にgconf_client_add_dirを使い、その値が変更されたときのコールバック関数を登録するために、gconf_client_notify_addを使っています。具体的には、これらの関数は既に見た関数panel_profile_load内及びそこから呼び出された関数内で用いられています。

関数panel_profile_loadは、途中でgconf_client_add_dir関数を呼び出しています。ここで指定するgconfの階層は、/apps/panel/generalです。この階層以下には、パネルの数やオブジェクトの位置などが記録されるため、オブジェクトの数を確認するためにはこの部分の変化を見ておく必要があります。

ここで実際のgnome-panelでの例を示します。実際にgconfの設定を見るためにgconftoolを用います。gconftoolで対応するディレクトリ以下の設定項目を見るには,

$gconftool-2 [-R|--recursive-list] ディレクトリ名

とします。

gnome-panelの設定を見るためには、あらかじめgnome-panelをできるだけ単純に設定しておくと後が楽になります。ここでは、パネルは1つだけを残して全て取り去り、残ったパネル内のオブジェクトも全て取り去りました。この操作は対応するパネルやオブジェクトを右クリックし、対応するメニューを用いることで行えます。また、最後のパネルを消そうとすると、メニューが表示されなくなるため、全てのパネルを消し去ることはできません。パネルを右クリックした場合新たなパネルを追加するメニューが表示されるため、それを用いて新たにパネルを加え、設定を元に戻すことができます。

実際にこの設定にした後,上のコマンドでディレクトリ名として/apps/panel/generalを用いると

object_id_list = []
applet_id_list = []
toplevel_id_list = [panel_1]

などの出力が得られます。ここで、object_id_list、applet_id_listはそれぞれパネルに含まれるオブジェクト、アプレットを表します。ここでは、オブジェクトを1つも用いていないので、これらは空欄となります。一方toplevel_id_listはパネルがいくつあるかを示すリストです。ここではただ1つのパネルを用いているため項目は1つだけとなります。後にわかるのですが、panelが複数になった時にはこの部分にpanel_2, panel_3, ... などの項目が追加されます。ここで、1, 2, 3などの数字はidと呼ばれます。この用語はtoplevel_id_list等の名前にも用いられていますが、以降のソース内の関数名にも何度か用いられます。

これで、オブジェクト、アプレット、パネルの数がどのように記録されているかがわかりました。更にこれらの設定の詳細については/apps/panel以下の各ディレクトリに記録されています。例えば,/apps/panel/toplevelsにはパネルの設定が記録され、/apps/panel/appletsにはアプレットの設定が記録されます。例えば/apps/panel/toplevels/panel_1の設定を見ることもできますが、設定項目の数が多いのでこれらの詳細には触れません。比較的意味が取りやすいものでは、

size = 24
orientation = top
auto_hide = false

などがあります。これらは、それぞれパネルの幅(単位はピクセル)、パネルを画面中でどの位置に置くか、カーソルが置かれていないときパネルを隠すかどうかに対応します。これらはどれもパネルの右クリックメニューから扱うことができる"プロパティ"によって設定できる項目です。

次に、パネルの数とアプレットの数を増やして同じ操作をしてみます。ここではパネルを2枚にし、メインメニュー、時計、通知スペース、ウィンドウの一覧、デスクトップの表示などの各種オブジェクトを追加しました。ただし、パネルの位置はそれぞれ上と下とし、オブジェクトのうち最初の3つは上のパネルに加え、後の2つを下のパネルに加えました。この場合/apps/panel/generalでの出力は

object_id_list = [object_1]
applet_id_list = [applet_0,applet_1,applet_2,applet_3]
toplevel_id_list = [panel_1,panel_2]

のようになります。ここで上では5つのオブジェクトを加えたのに、設定ではオブジェクトが1つでアプレットが4つとなっています。実際にはアプレットもオブジェクトの一種なのですが、アプレットは他のオブジェクトと比べてかなり動作が異なるので、アプレットは別に扱われます。

具体的にはアプレットはソース内ではOBJECT_BONOBOなどと呼ばれます。ここで、BONOBOはアプレットを扱う技術の名前なのですが、この技術は単純にはプロセス間通信を用いた技術です。実は上の時計などのアプレットの本体は、gnome-panelのプロセスとは別のプロセスとして存在します。例えば,時計が動いているgnome-panelが存在する時には,常にclock-appletというプログラムが動いています。具体的には、対応するpsコマンドの出力には、

3855 ?        S      0:00 /usr/libexec/notification-area-applet --oaf-activate- 
3859 ?        S      0:00 /usr/libexec/clock-applet --oaf-activate-iid=OAFIID:G
3874 ?        S      0:00 /usr/libexec/wnck-applet --oaf-activate-iid=OAFIID:GN 

などの出力が含まれます。ここで注目してほしいのは、中間の/usr/libexec/以下の各項目です。これらは上から順に通知スペース(notification-area)、時計(clock), ウィンドウの一覧に対応するプロセスです。BONOBOのライブラリであるlibbonoboについてはここでは深くは扱いません。w:en:bonobo (computing)[3]などを参照してください。

ここでは更に、/apps/panel/objectsやa/apps/panel/appletsの中身も見てみます。上で導入したオブジェクトの中でメインメニューはオブジェクト(アプレットでない)なので、/apps/panel/objects内に記述があるはずです。実際にこの項目を見ると、

/apps/panel/objects/object_1:
 toplevel_id = panel_1
 object_type = menu-bar
 position = 79

などの項目が与えられます。この中で、object_1はメインメニューに対応するオブジェクトのはずですが、menu-barの名前が3行目にあるので、確かにこのオブジェクトがメインメニューに対応することがわかります。他にpositionはメニューの位置を表し、toplevel_idはこのオブジェクトがどのパネルに含まれるかを表します。panel_1は上側のパネルなのでこれで正しいわけです。他にlockedという項目がありましたがこの項目はおそらくそのオブジェクトが"ロック"されているかを表します。"ロック"はオブジェクトの移動を禁止する機能で、右クリックメニューから選ぶことができます。

更にアプレットについては次のような項目が存在します。

/apps/panel/applets/applet_0:
 toplevel_id = panel_1
 bonobo_iid = OAFIID:GNOME_NotificationAreaApplet
 object_type = bonobo-applet
 position = 606 

ここで、object_typeはbonobo-appletとなっていますが、この項目はアプレット全般に対して用いられます。また、toplevel_idとpositionについては既に扱いました。最後にbonobo_iidですが、これはBONOBOの機構内でどのプロセスからの入力を扱うかを定める1つの文字列です。ここではNotificationAreaの文字があるので、このアプレットが"通知スペース"に対応することがわかります。

ここまででgnome-panelの設定に関する実例を見てきました。これらの設定の変更を扱うために、関数panel_profile_loadの中ではgconfの設定内のパネル、オブジェクト、アプレットそれぞれの設定に対して、panel_profile_load_listという関数が呼ばれています。関数panel_profile_load_listもpanel_profile_loadと同じファイル内で定義されているのですが、この関数は関数内でgconf_client_notify_addを呼び出しています。

gconf_client_notify_addの引数は、パネルの設定に対してこの関数が呼ばれた場合とオブジェクトやアプレットに対して呼ばれた場合で変化します。例えば、オブジェクトの設定を読んだ場合には関数panel_profile_object_id_list_notifyが引数として与えられます。この関数はどのオブジェクトが消えたり追加されたりしたのかを把握し、オブジェクトに対応するウィジェットを追加したり取り除くという作業を行う関数です。実際に関数panel_profile_object_id_list_notify内ではpanel_profile_load_added_idsとpanel_profile_delete_removed_idsの2つの関数が呼ばれていますが、これらの関数は名前の通りの動作をし、付け加えられた(added)オブジェクトを読み出したり(load)、取り除かれた(removed)オブジェクトを解放したり(delete)します。

実際にウィジェットを追加するのは関数panel_profile_object_id_list_notifyの最後で呼ばれている関数panel_applet_load_queued_appletsですが、この関数は既に見た関数で、gconfから受け取ったリストを用いて、対応するウィジェットを作成する関数です。ここでは、関数panel_profile_object_id_list_notify内でリストの変更が取り入れられているので、ウィジェトの追加や削除を行うことができるわけです。

ここまででgconfの設定をアプリケーションの動作中に反映するための機構を見て来ました。これらはアプリケーションの起動時にしか変更を反映できない方法と比べて優れた方法です。同様の方法は他のGNOMEアプリケーションでも用いられており、gconfがGNOMEのライブラリとして重要であることを示しています。

アプレットの動作に必要なライブラリ

[編集]

既に"アプレット"がlibbonoboを通じて実現されていることを述べました。ここで、libbonoboはプロセス間通信を行うための一般的な技術です。これは、アプレットを扱うプロセスとgnome-panelのプロセスの間の通信を行うために用いられます。

アプレットはlibbonoboに加えてlibbonobouiライブラリに含まれる技術も用いています。libbonobouiライブラリはlibbonoboを用いてあるプロセスで制御されるGTKウィジェットを他のプロセスのウィジェットに埋め込む一般的なライブラリです。

GtkPlug, GtkSocket
[編集]

まず最初に、GTKを用いたウィジェットの埋め込みを扱います。GTKの枠組みでウィジェットの埋め込みを行うには、GtkPlug, GtkSocketのウィジェットを使います。GtkSocketは、ウィジェットを埋め込まれる側のプロセスが作成するウィジェットで、GtkPlugが実際に埋め込まれるウィジェットに対応します。

X上で動くGTKのプロセス中では、GtkPlug, GtkSocketは埋め込まれるウィンドウを決めるために、WindowIDを用います。WindowIDはXのウィンドウに与えられる一意の数値で、型はXID(大抵unsigned long)で与えられます。

サンプルコード

X上でのWindowIDを知るには、xwininfoコマンドを使うのが簡単です。

$xwininfo

これは指定されたウィンドウのWindow IDやジオメトリ(位置と大きさ)などの情報を与えます。

GtkPlug, GtkSocketを用いて埋め込みを行うには、GtkSocketにGtkPlugのWindowIDを伝える必要があります。このためのlibbonoboui内のクラスとして、BonoboPlug, BonoboSocketの両クラスがあります。これらはそれぞれGtkPlug, GtkSocketを継承します。

実際に埋め込みを行うために、BonoboPlug, BonoboSocketはそれぞれBonoboControl, BonoboControlFrameを使います。ここで、BonoboControlはgetWindowIDという名の"メソッド"を持っており、他のプロセスで実行されるBonoboControlFrameにBonoboPlugのWindowIDを伝えます。ただし、libbonobo, libbonobouiのバージョンとして、2.16.0を用いました。

BonoboControlFrameはGtkWidgetを継承していないため、埋め込まれる側のウィジェットの配置に手間がかかります。この手間を省くため、BonoboControlFrameを"private"なメンバとして持ったクラスBonoboWidgetが存在します。

gnome-panel中でも、/gnome-panel/panel-applet-frame.c内でBonoboWidgetが用いられています。panel-applet-frameはBonoboWidgetを収納するGtkWidgetで、GtkEventBoxを継承します。GtkEventBoxはGtkBinクラスを継承したウィジェットで1つのウィジェットを収納します。

一方gnome-panelでは、埋め込むウィジェットを提供する機構としてPanelAppletクラスが提供されています。(./libpanel-applet/panel-applet.[ch]を参照)このクラスはBonoboControlへのポインタを所持します。各アプレットはこのクラスを継承し、PanelAppletFrameと相互作用します。

各種アプレットの動作

[編集]

既にgnome-panelの動作は"アプレット"によって拡張されることを見てきました。ここでアプレットはBonoboと呼ばれる技術を用いて作られており、これらはgnome-panelとは異なったプロセス内で動作しています。この時、なぜ単純にgnome-panelの新たなオブジェクトとして各機能を作成しなかったのかが疑問に思われます。

詳細は不明ですがこの方式の明らかな利点として、各アプレットを作成する言語として、C言語以外の言語を選べることがあげられます。実際gnome-applets内に含まれるアプレットでinvest-applet(gnome-applets-x.x.x/invest-applet)はw:Pythonを用いて作成されています。ここでgnome-appletsはGNOMEから配布されているファイルで、gnome-panelの各種アプレットを扱っています。この中には音量調節(gnome-applets-x.x.x/mixer)やごみ箱(gnome-applets-x.x.x/trashapplet)などのアプレットが含まれています。ただし、gnome-appletsのバージョンとしてはgnome-applets-2.16.2を用いました。

これに加えて既に登場した時計、通知スペースなどのアプレットがgnome-panel内に含まれています。(それぞれ./applets/clock, ./applets/notification_area内のファイル)ここでは、Bonoboの詳細には触れずに、各種"アプレット"の動作を見ていきます。これはBonoboを使う場合でもアプレット自体は通常のGTK+アプリケーションと同じように書くことができるからです。このことの詳細については[4]などを参照してください。

gnome-panel内のアプレット
[編集]
時計アプレット
[編集]

時計アプレットはその名の通りw:時計を表示するアプレットです。このアプレットはgnome-panelの./applets/clock以下に含まれています。時計アプレットの仕事はおおよそGTK+を使って時計を作成することです。簡単な時計の作り方については例えばXプログラミングを参照してください。

(赤線は筆者が導入した)基本的に時計アプレットの本体は時刻の数値をテキストとして書き込まれたGtkLabelです。GtkLabelは既にGtkFixedの中で用いたのでここでは説明しません。対応するGtkLabelは./applets/clock/clock.c内のcreate_clock_widget関数中で作られています。時刻の書き換えはclock_timeout_callback中で呼ばれるupdate_clockで行われます。この間数はgtk_label_set_textを用いてGtkLabel内の数値を変えた後、gtk_widget_queue_resize関数を呼びます。この関数はGtkWidget及びそれを継承したウィジェットに対してその変更を画面に反映するために呼ばれます。同種の関数にgtk_widget_queue_draw(_area)がありますが、gtk_widget_queue_resizeは変更によってウィジェットのサイズが変わる場合に呼ばれます[5]。一方、gtk_widget_queue_drawはウィジェットのサイズを変えません。

追加の機能として、時計アプレットは24時間表示と12時間表示を切り替えたり、カレンダーを表示したりといくつかの機能があります。前者はGtkLabelのフォーマットを変更するだけで書き換えられますが、後者は多くの操作が必要となります。実際にはカレンダーはGtkCalendarとしてGTK+のウィジェットが与えられているため、時計アプレット内ではそれが用いられています。GtkCalendarについてはGTK+のソースを参照してください。

また、時計アプレットはロケールを変更してgnome-panelを起動すると表示が変化します。次の例はロケールをen_USに変更した例です。ただし、シェルとしてw:bashを用いています。

$LANG=en_US gnome-panel

(赤線は筆者が導入した)

ロケールの変更による時刻のフォーマットの変更は、gettextライブラリによって行われます。gettextの詳細はOSS開発ツールを参照してください。実際のpoディレクトリは./po/以下で与えられます。この中では各国語でのフォーマットが文字列の形で定義されています。

wnckアプレット
[編集]

wnckアプレット(wnck-applet)は、"libwnck"を用いるアプレット群で、GNOMEデスクトップのウィンドウの管理を行います。このアプレット群は複数のアプレットを含んでおり、これらはそれぞれ"デスクトップの表示"(ShowDesktop), "ウィンドウ一覧"(WindowList), "ウィンドウセレクタ"(WindowMenu), "ワークスペース切替器"(WorkspaceSwitcher)が含まれます。既にアプレットの例でwnck-appletが起動されている場面を見ました。これらはここで与えられる複数のアプレットに対応するプロセスです。

ここで、それぞれのアプレットの機能を簡単に紹介します。"デスクトップの表示"は全てのウィンドウを最小化し、デスクトップを表示します。実はデスクトップは後に述べるw:Nautilusの画面なのですがここでは触れません。ウィンドウの大きさを変更する機能はウィンドウマネージャの機能であるため、対応するウィンドウマネージャがlibwnckの要求を受けない場合には、このアプレットは機能しません。実際twmを用いて"デスクトップの表示"を動かしたところ、エラーメッセージが表示されました。

次に"ウィンドウ一覧"と"ウィンドウセレクタ"はどちらもその時点で存在するウィンドウを選択するためのアプレットです。ただし、"ウィンドウセレクタ"はこれをWnckSelectorとして与え、"ウィンドウ一覧"はWnckTasklistとして与えます。

実行例

"ウィンドウ一覧"はよく用いられるアプレットで、既に意識せずに使っているかも知れません。こちらもtwmで管理されるウィンドウは表示されません。

"ワークスペース切替器"は何枚もの画面(ワークスペース)があるように見せ、それらを切り替えながら使うことで画面を広く使うアプレットです。こちらもtwmと同時には使えません。


通知スペース(NotificationArea)アプレット
[編集]
このページ「GTKプログラミング」は、まだ書きかけです。加筆・訂正など、協力いただける皆様の編集を心からお待ちしております。また、ご意見などがありましたら、お気軽にトークページへどうぞ。
gnome-applets内のアプレット
[編集]

gnome-appletsにもいくつかのアプレットが含まれています。ここでは比較的動作がわかりやすいアプレットを選んで紹介します。具体的には"CPU周波数"(cpufreq)アプレットと"音量調節"(mixer)アプレットを扱います。

cpufreqアプレットは使っているw:コンピュータw:クロック周波数を表示するアプレットです。クロック周波数はCPUの動作速度を表す指標で、基本的にはこの数値が大きい程速いCPUであるといえます。ただし、コンピュータを使う際の体感速度は、w:メモリの量などCPU以外の条件にもよるので、この数字だけでコンピュータの性能が決まるわけではありません。

Linux上では、使っているCPUの周波数は"ファイル"として利用者から利用できるようになっています。ただし、この値を変更してもハードウェアが変更されるわけではなく、この機能はシステムの状態を把握することを目的とした機能です。CPU周波数の情報は/proc/cpuinfo、もしくは/sys以下のディレクトリに記録されています。cpufreqアプレットはこれらの値を読み出して表示します。


関連項目

[編集]

OSS開発ツール/GUIツールキット


※ 統合作業用の見出し

[編集]

//////////////////////////

以降、『OSS開発ツール/GUIツールキット』 2019年9月28日 (土) 13:36‎ からの引用。統合作業中。

//////////////////////////

Gtk+

[編集]

GtkDrawingAreaの例

[編集]
GtkDrawingAreaとは
[編集]

ここでは、Gtkウィジェットの例として、GtkDrawingAreaウィジェットを扱います。GtkDrawingAreaは、内部にGdkWindowを持っており、利用者はその中に自由な描画を行えます。これは、w:WindowsでいうところのDevice Context(w:GDIを参照)と似た機能です。

GtkWindowを作り、GtkDrawingAreaを収納するサンプルは次のようになります。まず最初にGTKライブラリの初期化とウィジェットポインタの宣言を行います。

#include <gtk/gtk.h> 
int main(int argc, char **argv){
    gtk_init(&argc, &argv); 
    GtkWidget *win, *da;
    /* ここで、win, daはそれぞれGtkWindowとGtkDrawingAreaに対応します。GtkWindowとGtkDrawingAreaを実際に作るには、次のようにします。*/
    win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    da = gtk_drawing_area_new(); 
    /* これらの関数は対応する構造体のnew関数です。*/

    /* 更に、GtkWindow*winにGtkDrawingArea*daを収納します。このためには、gtk_container_addを使います。*/
    gtk_container_add(GTK_CONTAINER(win), da); 
    /* ここで、winに対してGtkContainer*へのキャストが行われていることに注意してください。GtkWindowはGtkContainerを継承するので、この操作は正しい操作です。しかし、GtkContainerを継承しない"クラス"に対してこの操作を行うと、プログラムの実行時に警告が出されます。*/

    /* 次に、実際に描画関数を呼ぶ処理を行います。ここで、実際の描画はコールバック関数の中で行います。コールバック関数の名称はexpose_cbとします。コールバック関数を登録するために、*/
    g_signal_connect(da, "expose_event", G_CALLBACK(expose_cb), NULL);    // を使います。ここでは、関数g_signal_connectについて解説します。

この関数の名前はg, signal, connectの3つに分かれます。

まず、最初のgは、この関数がw:en:glibに属するためにつけられています。これは、C言語に、w:名前空間の概念が無く、接頭詞g_を外すと、他のライブラリからの関数名と2重に関数が登録される危険があるためです。

次に、signalは、GSignalのことを指します。GSignalはおおよそコールバック関数へのポインタのことです。GSignalはw:en:glibで定義され、1つのコールバック関数と1つの文字列を対応付けます。上の例では、文字列"expose_event"で表されるシグナルが扱われます。GSignalはGObjectを継承した"クラス"に対して適用され、プログラムの実行時に"クラス"の初期化が行われる時、後に利用者のプログラム中で定義されるコールバックの置き場を与えます。

最後に、connectは、指定されたGSignalに対してあるコールバック関数を実際に与えることを指します。

結局、g_signal_connectでは、引数によってあるGObjectのGSignalを指定し、それに対して1つのコールバック関数を与える関数です。ここで、g_signal_connectの引数は

g_signal_connect(GObject *, gchar *, GCallback*, gpointer)

であり、第1、第2引数はそれぞれGSignalを指定するためのGObjectと、GSignalの名前を表します。第3、第4引数はそれぞれ、コールバック関数と関数に与える引数を表します。第4引数ではコールバック関数内で必要なデータを与えます。

また、"expose_event"は、w:X Window Systemなどから与えられる"イベント"の1つで、あるウィンドウ内の長方形を描画する必要があるときにXクライアントに対して与えられるイベントです。詳しくはXプログラミングを参照してください。ここでは、GtkWindowが他のウィンドウによって隠されたときや、一旦ウィンドウを最小化したときを扱うための手法であると述べるに留めます。

ここまでで、GtkDrawingAreaをGtkWindowに収納しました。これらを表示するためには次のようにします。

    gtk_widget_show_all(win); 
    gtk_main(); 
    return 0;
}

ここで、gtk_widget_show_allは指定されたウィジェットに収納されたウィジェット全てを"show"する関数です。

ここまででプログラムは終わりですが、expose_cbを空の関数としてこれを実行すると窓を開くだけの例と同じ結果になります。これは、GtkDrawingArea内に実際に描画を行っていないことによります。実際に描画を行うためには、expose_cb関数を書く必要があります。

GdkWindowの描画関数
[編集]

expose_cb関数は次の様に宣言されます。

void expose_cb(GtkWidget *da);

ここで、"expose_event"のコールバック関数は"expose_event"を受け取ったウィジェットを渡されます。ここでは、g_signal_connectで指定されたウィジェットがGtkDrawingAreaなので、コールバック関数にもGtkDrawingAreaが渡されます。

ここで、実際にGtkDrawingAreaに描画を行う方法について述べます。GtkDrawingArea構造体には、GdkWindowが含まれています。GdkWindowはXなどから与えられる長方形の領域で、この中の各ピクセルを扱う事で、図形を描画することができます。実際にGtkDrawingArea*内のGdkWindow*は次のように指定されます。

da->window

ここで、daは、GtkDrawingArea*を表します。

GdkWindowにはいくつかの描画用の関数があります。これらは基本的にw:X Window Systemの関数に対応しています。例えば、線をひくための関数であるgdk_draw_lineは、XDrawLine関数に対応しています。Xを扱う関数に関してはXプログラミングを参照してください。

ここでは、実際に線をひく関数を試してみます。まず、expose_cbの定義です。

void expose_cb(GtkWidget *da){

ここで、実際に図形の描画を行うためには、GdkWindowのGC(Graphic Context)を指定する必要があります。ここで、GCはXなどで扱われる描画要素で、図形の色や線の太さなどを表します。ここでは全ての値をデフォルトとしたGCを作るため、gdk_gc_new関数を使います。ただし、1度だけgcを作るため、staticで定義します。

static GdkGC *gc = NULL;
if (!gc) 
    gc = gdk_gc_new(da->window);

更にこのGdkGC*であるgcを用いてgdk_draw_lineは次のように書けます。

    gtk_draw_line(da->window, gc, x0, y0, x1, y1);
}

ここで、線は(x0, y0)から(x1, y1)までひかれます。

実行例

ここで、ウィンドウの最小化やウィンドウの重なりがうまく扱われていることに注意してください。

GDKの描画関数は他に、gdk_draw_polygon, gdk_draw_rectangle, gdk_draw_point(s), gdk_draw_arcなどがあります。これらについてはGDKのリファレンス[6]を参照してください。


Gtkのウィジェット

[編集]

ウィジェットは"ラベル"や"ボタン"などの様々なよく用いられるGUI要素のことを指します。ここからはこれらのGUI要素を用いたプログラムについて述べます。ウィジェットはw:Windows APIでは"(コモン)コントロール"と呼ばれるものです。

ボタンを利用した例
[編集]

ここでは、GtkButtonを利用した例を扱います。この例は公式のチュートリアル[7]でも扱われているので、簡単に済ませます。しかし、コールバックの話をするときに、再びこの例を使います。

GtkButtonは、gnomeのソフトウェアを利用するときに頻繁に利用されるボタンウィジェットです。

ボタンウィジェットは単独で使うことは出来ず、必ずGtkWindowの中で使う必要があります。あるウィジェットの中で別のウィジェットを使うことをウィジェットのパッキングといいます。ここではGtkButtonをGtkWidgetの中にパッキングします。

GtkWindowには1つのウィジェットしかパックできないため、複数のウィジェットを表示したい場合には複数のウィジェットを収納できるウィジェットをパックする必要があります。よく使われる例にGtkVBox, GtkHBox, GtkTableなどがあり、GtkButtonを複数収納する例はよく用いられます。しかし、複数のウィジェットを利用する場合にはlibgladeを利用した方が便利なので、ここでは1つのウィジェットを扱う例だけを扱います。

GtkButtonをGtkWindowに収納するには、上の例の3行目の次に、以下のプログラムを追加します。

1: GtkWidget *button = gtk_button_new();
2: gtk_widget_show(button);
3: gtk_container_add(GTK_CONTAINER(win), button);

ここでは順を追って追加されたプログラムを見ていきます。

1行目はGtkButtonのnew関数で、buttonを作成します。やはり、この関数もGtkWidgetポインタを作成します。

2行目はgtk_widget_show関数です。GtkWindow同様、GtkButtonにもgtk_widget_showを使わないとウィジェットが表示されません。

3行目はgtk_container_add関数を利用しています。gtk_container_add関数はGtkContainerクラスを継承したクラスを第1引数に取って利用され、第2引数のウィジェットを第1引数のコンテナに収納します。ここで、GTK_CONTAINERは、glibの例で見た通り、GTK_CONTAINERのインスタンスにキャストを行なうマクロです。第1引数はGtkContainer* なので、このキャストが必要になります。

  • スクリーンショット
glade
[編集]

ここまででウィジェットの性質を変えたり、ウィジェット間に収納関係を与える方法について見てきました。これらはどれも似たようなコードであり、作成するのが面倒になりがちです。例えば、ボタンを10個作らねばならないプログラムでは上で追加したのと同じ内容を10回繰りかえす必要があります。(実はforループを利用することも出来ます。)これは非常に大変な作業です。幸いにもこれらの作業を手軽に行なうプログラムがあるので、ここではそれを紹介します。

このようなプログラムはインターフェースビルダと呼ばれます。現在では類似の機能は例えば、Javaw:Swingに対して、w:Eclipseで提供されています。([8] )

まずgladeは、GUIを用いてgtk+のウィジェットを扱うプログラムです。

gladeのスクリーンショット

次に、libgladeはgladeを含む何らかの手法で作られたw:XMLファイルから、実際にウィジェットを作成するライブラリです。ここではまずlibgladeを使った場合に実際に書く必要があるコードを扱います。幸いにも複雑なウィジェット群を扱うときにもここで扱うプログラムはそれほど変化しません。この例は、gladeリファレンスマニュアル([9] )で扱われている例とほとんど同じものです。

libglade
[編集]

ウィジェットの情報が含まれるXMLファイルの拡張子は.gladeです。ここではXMLファイルの名前をsample.gladeとします。このときlibgladeを使ったプログラムは、次のようになります。

1: int main(int argc, char** argv){
2:   gtk_init(&argc, &argv);
3:   glade_xml_new("sample.glade", NULL, NULL);
4:   gtk_main();
5:   return 0;
6: }

単にウィジェットを表示することが目的なら、これだけで十分です。XMLファイルでコールバック関数を利用することが指定されているときには、3行目を

1: GladeXML *xml = glade_xml_new("sample.glade", NULL, NULL);
2: glade_xml_signal_autoconnect(xml);

に変更する必要があります。

gladeの使い方
[編集]

stub

複数のボタンを使った例
[編集]

ここではgladeを使ってより複雑なウィジェットを作成します。幸いにもほとんどの作業はGUIを利用して行なうことが出来ます。

ここでは次のようなウィジェットを作成します。

ウィジェットの関係は次のようになります。

GtkWindow - GtkVBox - GtkButton
                    - GtkButton

このウィジェットはWindow内に2つのボタンが入っているだけの簡単な例です。しかしそれでも、手作業で全てを作成するのは厄介な仕事です。

1. GtkWindow(window1)を作る。

空ウィンドウ.png

2. GtkVBox(vbox1)をwindow1に収納する。'サイズ'は2とする。

垂直ボックス.png

3. GtkButtonをvbox1の2つの空白に収納する。(button1, button2)

4. button1の'ラベル'をaaaとする。button2の'ラベル'をbbbとする。

gtk-demoの例
[編集]
Message Boxの例
[編集]

gladeを使ったより複雑な例として、gtk-demoからの例をあげます。ただし、コールバック関数は提供せず、インターフェースだけとします。gtk-demoは、gtk+とともに配布されているデモで、ソースはgtk+-x.x.x/demos以下に含まれています。gtk-demoは、gtk+とともにインストールされているので、

$gtk-demo

のコマンドで利用することができます。ここでは、Dialog and Message Boxes という例を取り上げます。(demos/gtk-demo/dialog.c)

dialog.cはいくつかのウィジェットを利用して書かれています。多くはコンテナウィジェットであり、コンテナの使い方を見る上でよい題材です。

まず、それぞれのウィジェットを紹介します。gladeを使わずにこれらのウィジェットを扱う方法は、dialog.cのソースを参照してください。

Dialogsと書き込まれた外枠は、gtkframeを利用します。gtkframeは、内側に1つのウィジェットを収納できるコンテナです。ここには、gtkvboxを収納します。gtkvboxは複数のウィジェットを収納することができ、収納したウィジェットを縦に並べます。gtkvboxに収納するウィジェットは上から順にgtkhbox, gtkhseparator, gtkhboxです。gtkhboxはgtkvboxと似た性質を持ちますが、縦ではなく横にウィジェットを配置します。hseparatorは、スクリーンショット内の横棒で、ウィジェット間に距離をとる働きをします。

次に、2つのgtkhbox内のウィジェットについて順に述べていきます。上のウィジェットにはgtkbuttonを収納します。gtkbuttonにはlabelというパラメータがあり、この部分は_Message Dialogとします。label中のアンダースコアは、gtkbuttonのパラメータを書き換えることで、下線として表示させることができます。(設定次第でそのままアンダースコアとして表示させることもできます。)

下のgtkhboxには、左から順にgtkvbox, gtktableを配置します。gtkvboxには、_Interactive Dialogと書かれたgtkbuttonを収納します。下線の扱いは先程の_Message Dialogと同じです。gtktableは、碁盤目状の枠を作り、それぞれのマスにウィジェットを収納するコンテナです。ここでは、幅2、 高さ2のgtktableを作成します。gtktableには、左上から順にgtklabel, gtkentry, gtklabel, gtkentryを収納します。(左上、右上、左下、右下の順)gtklabelには、それぞれ_Entry 1または、E_ntry 2と書き込みます。

ここからはここまでの手順をgladeを利用して作成していきます。 1. gtkwindowを導入する。 2. gtkframeを加える。影の種類を'Etched In'にする。'境界線の幅'を、5程度にしておく。

3. gtkvboxを収納する。

4. gtkhboxをgtkvboxに収納し、gtkbuttonをgtkhboxに収納する。

5. gtkhseparatorをgtkvboxに収納する。

6. gtkvboxにgtkhboxを収納する。列の数は2とする。 7. gtkhboxにgtkbuttonとgtktableを加える。

8. gtktableに、gtklabel, gtkentryを加える。

9. gtkbutton, gtklabelの'ラベル'を_Message Dialog などの内容に書き換える。詳しい内容は前述した。gtklabelでは、'下線付き'を'はい'にしておく。

10. .gladeファイルとして保存し、先程のプログラムを使って表示する。

Expanderの例
[編集]

次に、gtk-demoの'Expander'の例を扱います。GtkExpanderは閉じた状態と開いた状態を持つウィジェットです。

GtkExpanderはコンテナウィジェットでもあり、GtkExpanderを開いた時に現れるウィジェットはGtkExpanderに収納されているウィジェットです。ここでは上の例を順に作成していきます。

ウィジェットの関係は次のようになります。

GtkDialog - GtkVBox - GtkLabel
                    - GtkExpander -GtkLabel

ここでGtkExpander自身も"Details"と書かれたラベルを持っていますが、これはGtkExpanderの一部ということで上の関係図には入れていません。

新たなウィジェットとして、GtkDialogが登場しています。GtkDialogは主となっているウィンドウではない新しいウィンドウを使いたいときによく用いられます。特に、設定を変更するときに用いられる'OK', 'キャンセル'などのボタンを含んだウィンドウはおそらくGtkDialogが用いられています。上の例では、'閉じる'のボタンが使われていますが、これもGtkDialogの標準のボタンの1つです。上の例の内で、'閉じる'だけが日本語で後は英語であるのもそれが原因です。標準の文章なのでボタンに書かれた文章だけは翻訳が進んでいます。

(Gtk+では、各国語に翻訳を行うために、gettextを用いています。gettextはコンパイル時ではなく実行時に文章を変更することができるため、gtk-demoをw:ロケールを変更して実行することでボタンの文章を変更することができます。例えば、

$LC_ALL=en_US gtk-demo

とすると、ボタンの文章が英語に変わります。

こちらはドイツ語の例です。

$LC_ALL=de gtk-demo

)

ここからは実際にウィジェットを作成します。手順は次のようになります。

1: GtkDialogを開く。

2: GtkDialogにGtkVBoxを収納する。GtkVBoxの収納数は2とする。

3: GtkVBoxの上部に、GtkLabelを収納する。

4: GtkVBoxの下部にGtkExpanderを収納する。

5: GtkExpanderにGtkLabelを収納する。 GtkExpanderを開いた状態です。

GtkExpanderにGtkLabelを追加した状態です。

6: 2つのGtkLabelと、GtkExpanderの文章を書き換える。

7: 先程のプログラムを作成し.gladeファイルを表示する。

より進んだ例

[編集]

上の例以外でもgtk-demoの中には比較的簡単に作成できそうな物が含まれています。これらを作成してみるとよいかもしれません。gtk+を使ったプログラムは数多いので、これらを探して読んでみるのもよいでしょう。

参考文献

[編集]

w:Gtk+,w:Qt,w:SDL,gcc-3.4.4 man及びinfo

gtk4

[編集]

2021年現在のFedoraの最新版が搭載しているのはgtk4です。gtk4の開発環境を入れるコマンドは、ターミナルで

sudo dnf install gtk4-devel

でそのまま入る。ただし、gtk3とgtk4は文法が多少違っているので、gtk3のコードは使い回しできません。

下記のgtk4コードはgtk4公式マニュアル からの引用をしたものに、本wiki側で説明用にコメントを加えたり、説明しやすいようにコードの数値や文字列などを少々変更しています。

#include <gtk/gtk.h>

// main関数から呼び出されるコールバック関数
static void activate(GtkApplication *app, gpointer user_data) {
  GtkWidget *window = gtk_application_window_new(app);
  gtk_window_set_title(GTK_WINDOW(window), "Window 題名"); // ウィンドウタイトルの設定
  gtk_window_set_default_size(GTK_WINDOW(window), 300, 200); // ウィンドウサイズの設定
  gtk_widget_show(window); // ウィンドウの表示:
  // ↑ gtk_widget_showの呼び出しを消すと、プロセスを殺さない限り終了できなくなる。このコードは残せ!
}

int main(int argc, char **argv) {
  GtkApplication *app = gtk_application_new("org.gtk.example", G_APPLICATION_FLAGS_NONE);
 /* g_signal_connectの第2引数の"active"はイベントのニーモニックなので、
  * 変更するとコンパイルエラーにならず、実行時にイベントを捕捉できなくなる */
  g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); 
  int status = g_application_run(G_APPLICATION(app), argc, argv); // GtkApplication のイベントプール
  g_object_unref(app); // すぐ終了するので、この行を消しても実行できるが、公式サイトにあるので残そう

  return status;
}

これはもう、このまま使ってください。 一応、いくつかの変数名を変更しても使用できますが。

ファイル名は、公式サイトのサンプルファイル名に合わせて「example-0」にしましょう。


上記コードをコンパイルするには、ターミナルでコマンド

gcc $( pkg-config --cflags gtk4 ) -o example-0 example-0.c $( pkg-config --libs gtk4 )

です。 これでオブジェクトファイルが生成されているので、あとはそのオブジェクトファイルの実行コマンドをターミナルで入力すればいいだけです。たとえばオブジェクトファイルの存在場所がユーザープロファイルなら

./example-0

で実行できるはずです。


上記コードの大まかな仕組みを言うと、main関数ではどのウィンドウを呼び出すかだけを関数名を用いて設定しており、ウィンドウのサイズなどの設定は個々のユーザ定義関数で行う仕組みになっています。もしかしたらユーザ定義関数を使わなくてもウィンドウ表示できるのかもしれませんが、しかしGTK公式サイトがそのような方法を紹介しないという事はおそらく、そのような方法を推奨していないのでしょう。つまり、ユーザ定義関数を用いてウィンドウ設定などはmain関数から分けてから呼び出して欲しいようです。


次に、ボタンをウィンドウに追加してみましょう。下記コードも公式サイトにあるコードを元にしましたが、本wiki側で説明用に若干のコード書き換えをしています。

#include <gtk/gtk.h>

// コールバックのユーザ定義関数. ※ ボタン設定ではない
static void print_hello (GtkWidget *widget,
             gpointer   data)
{
  g_print ("ターミナルでhello\n"); // ボタンではなくターミナルに表示される文
}

// main関数から呼び出されるコールバックのユーザ定義関数. ボタン設定はここに追加されている
static void activate (GtkApplication* app,
          gpointer        user_data)
{
  GtkWidget *window;

  window = gtk_application_window_new (app);
  gtk_window_set_title (GTK_WINDOW (window), "Window 題名"); // ウィンドウ名が「Window題名」になる
  gtk_window_set_default_size (GTK_WINDOW (window), 300, 200); // ウィンドウサイズの設定、横300、縦200
  
  
  // ボタン関連 
  GtkWidget *button;  
  button = gtk_button_new_with_label ("Hello World"); // ボタンに表示される文章をラベル命令で決定
  gtk_widget_set_halign (button, GTK_ALIGN_CENTER); 
  gtk_widget_set_valign (button, GTK_ALIGN_CENTER);

  g_signal_connect (button, "clicked", G_CALLBACK (print_hello), NULL); // クリック時にターミナル文字表示の冒頭関数を実行
  g_signal_connect_swapped (button, "clicked", G_CALLBACK (gtk_window_destroy), window); // クリック時にアプリ終了

  gtk_window_set_child (GTK_WINDOW (window), button); // これ消すとボタンが表示されない. ボタンをウィンドウに貼り付ける命令だと思われる.
  // 以上、ボタン関連の追加コード
    
  gtk_widget_show (window);  // ウィンドウの表示 。この命令を消すと終了できなくなるのでコードに残せ
}

int main (int    argc,
      char **argv)
{
  GtkApplication *app;
  int status;

  app = gtk_application_new ("org.gtk.example", G_APPLICATION_FLAGS_NONE);
  g_signal_connect (app, "activate", G_CALLBACK (activate), NULL); // 第2引数の"active"は(冒頭のコールバック関数ではなく)コマンド名なので変更するとエラー
  status = g_application_run (G_APPLICATION (app), argc, argv); // ウィンドウ描画の実行をしていると思われる
  //g_object_unref (app); // この行を消しても実行できるが、公式サイトにあるので残そう

  return status;
}

コンパイルは上記と同様にできます。もしファイル名を変えなければ、上記コンパイルコマンドでそのままコンパイルできます。

g_print はターミナルで文字表示する命令ですので、ここの表示文をいくら弄って、ボタンにラベル表示されている文章は変わりません。混同しないように注意しましょう。

参考としたプログラム

[編集]

GNOME