[GTK+] 線を描画する (解説編)

さて昨日の線描画プログラムの解説をば。長いよ。w

 28     GtkWidget *window, *area;
...
 38     area = gtk_drawing_area_new();

GtkDrawingArea ウィジェットを生成する。
GtkDrawingArea というのは GtkWidget の派生オブジェクトなのだが、他の派生オブジェクト(ボタンやラベルなど)と違い、特に用途が決められているわけではない。このことは、 GtkDrawingArea が提供する関数がただ一つ、このオブジェクトを生成するための gtk_drawing_area_new() だけだということからも伺い知れる。リファレンスマニュアルの GtkDrawingArea に対する説明には「カスタムユーザインタフェースを生成するために使われる」とある。
今回はこのGtkDrawingArea ウィジェットをウィンドウの上に貼り付けて、その上に描画することにしたい。

 39     g_signal_connect(G_OBJECT(area), "expose_event",
 40                      G_CALLBACK(expose_event), NULL);

つい先ほど生成した GtkDrawingArea ウィジェット(=area)が expose_event シグナルを受け取ったときに、コールバック関数 expose_event (15-24行目) をコールするようにする。

expose_event シグナルとは何ぞや、ということになるが、その前にまず GTK+の描画モデルについて理解する必要があると思うので、はじめにそちらについて説明したい。


例えばウィンドウのサイズを変更したり、背面にあったウィンドウを最前面に持ってきたりしたときのことを考えてみよう。ウィンドウサイズを拡大した場合、その拡げられた部分にはこれまで何も描画されていないため、新たに描画を行う必要がある。また背面にあったウィンドウを前面に持ってくるケースでは、背面に隠れていた部分を新たに描画する必要がある。
これら新たに描画される部分を含めて、ウィンドウシステム側であらかじめ描画内容をメモリに格納しておき、必要なときに必要な箇所を自動的に再描画してくれたりしたら、プログラマ的には楽だろう。
が、実際にはそうなっていない。おそらく「どう再描画するか」については、アプリケーション制作者に委ねられるべきという考え方からだと思う。そこでウィンドウシステム側は、自動的に再描画はせずに、再描画が必要なウィジェットに対して expose_event というシグナルを送信し、再描画の必要があることをアプリケーション側に知らせるようになっている。

繰り返しになるが、上記の g_signal_connect では、ウィジェット area が expose_event シグナルを受け取ったときに実行される関数を登録している。

 42     gtk_container_add(GTK_CONTAINER(window), area);

そして、その area を、トップレベルウィンドウ(window)に追加している。


次に、このコールバック関数 expose_event の内容をば。

 15 static gboolean
 16 expose_event(GtkWidget *area, 
                                  GdkEventExpose *event, 
                                  gpointer data)
 17 {
 18     gint x;
 19 
 20     for (x = 0; x < 200; x++)
 21         draw_point(area, x, x);
 22 
 23     return TRUE;
 24 }

expose_event シグナルを受け取ったときのコールバック関数は以下の形式となる。(GTK+ Reference Manual の GtkWidget の Signals 参照)

gboolean user_function (GtkWidget *widget, 
                                               GdkEventExpose *event, 
                                               gpointer user_data)

で、ここでやっているのは、

  • (0, 0) から (200, 200) に向かって斜めの線を引く

これだけである。
ちなみに draw_point というのは以下のとおりで、

  8 void draw_point(GtkWidget *area, gint x, gint y)
  9 {
 10     gdk_draw_point(area->window,
 11                    area->style->fg_gc[ GTK_WIDGET_STATE(area) ],
 12                    x, y);
 13 }

gdk_draw_point という関数を使って、指定された座標に点を打っている。
gdk_draw_point の関数仕様は以下のようになっている。

void gdk_draw_point(GdkDrawable *drawable, 
                                        GdkGC *gc, 
                                        gint x, 
                                        gint y)


注:以下の説明文で GdkWindow と GtkWindow という非常に良く似ているが全く別物の名前が出てきますw


第1引数で指定するのは描画対象。ただし、ウィジェットそのものを指定するのではなく、GdkDrawable * 型のものを指定する。描画対象を GtkWidget 型で持っている場合は、その中に window というプロパティがある(型は GdkWindow*)ので、それを使って

widget->window

のように指定すればよい。

ちなみに window という言葉の意味がややこしいのだが、ここで言う window は GdkWindow *型であり、単に「画面上に存在する矩形の領域」を意味する。GtkWindow、つまり複数ウィジェットをまとめる最上位のウィジェットという意味のウィンドウとは別物だ。(一文字違いだから余計ややこしいw)

window はGtkWidget のプロパティの一つとして含まれているので、GtkWidget を継承したオブジェクト、たとえば GtkButton や GtkEntry などもすべてこの window を持っていることになる。
で、GdkWindow は GdkDrawable を継承したオブジェクトなので、gdk_draw_point の第1引数の描画対象として指定することが可能だ。


次に第2引数の描画スタイルの指定部分について。

 11                    area->style->fg_gc[ GTK_WIDGET_STATE(area) ],

内側の GTK_WIDGET_STATE(area) から見ていく。
GTK_WIDGET_STATE() はマクロで、以下のように定義されている。

#define GTK_WIDGET_STATE(wid)		  (GTK_WIDGET (wid)->state)

state というのは何かというと、特定ウィジェットの「状態」を表す値であって、GtkStateType で定義された5つの値のいずれかをとる。

typedef enum
{
  GTK_STATE_NORMAL,
  GTK_STATE_ACTIVE,
  GTK_STATE_PRELIGHT,
  GTK_STATE_SELECTED,
  GTK_STATE_INSENSITIVE
} GtkStateType;

これらの意味するところは Reference Manual を参照していただくとしてここでは割愛。ただ、これが GTK_WIDGET(area)->state の値であり、配列 fg_gc[] の添字である。

 11                    area->style->fg_gc[ GTK_WIDGET_STATE(area) ],

さて今度は area->style の意味を。
GtkWidget のプロパティの中に style という GtkStyle * 型の値がある。この GtkStyle 構造体は、ウィジェットの見せ方に関するさまざまな情報を格納している(色など)。

さらに GtkStyle の中を見ていく。

typedef struct {
  ...(中略)
  GdkGC *fg_gc[5];
  ...(中略)
} GtkStyle;

かなり前後を略してあるが、この中に fg_gc というGdkGC * 型の配列が定義されている。

GdkGC というのは GDK で使用される Graphic Context(GC)。描画を行うさいのさまざまな情報、たとえば色や線の太さなどを保持している。
で、fg_gc は、GdkGC へのポインタを5つ保持している。5つというのは、先に出てきた GtkStateType の数と同じであり、つまり、各 State における前景描画時の情報をそれぞれ保持しているということになる。


かなり話がややこしくなってきたが、結局

 11                    area->style->fg_gc[ GTK_WIDGET_STATE(area) ],

これは、

ウィジェットに定義されている、現在のウィジェットの状況における描画コンテクスト

を得ることになる。そして、

 10     gdk_draw_point(area->window,
 11                    area->style->fg_gc[ GTK_WIDGET_STATE(area) ],
 12                    x, y);

これは「ウィジェット area の window 領域に対して、現在の状況における描画コンテクストを使って、座標(x, y)に点を打つ」ということになる。


ふぅ、ずいぶん長くなってしまったww

(参考)