シグナルハンドラの実装 〜直線の描画〜


直線描画の設計概要

以下のように直線描画部分を実装します。

マウスにより、直線の入力を受け付けたら、新しい直線の始点と終点を記録する領域を確保して、 リストに接続します。そして、"expose_event"を発行して、直線の描画を行います。 ユーザからの入力インタフェースとしては、ドラッグ&ドロップで直線が描画できるようにしようと思います。 ドラッグ開始から線を描画しはじめて、ドロップの瞬間で直線を確定させるようなインタフェースです。


マウス関連のイベント

マウスのボタンを押した点を直線の起点として、 ボタンを離した点を直線の終点とするインタフェースを実装します。 マウス関連のイベント("button_press_event"、"button_release_event"、"motion_notify_event")は、 gladeでハンドラを追加しただけでは、これらのハンドラは起動しません。 gtk_widget_add_events()を用いて、 これらのイベントをアプリケーションに通知するようにする必要があります。 そこで、main.cに以下のコードを追加します。

main.c 40行目付近

  ...
  window1 = create_window1 ();
  gtk_widget_show (window1);

  /* 以下のコードを追加 */
  drawingarea1 = lookup_widget(GTK_WIDGET(window1), "drawingarea1");
  if(drawingarea1){
      gtk_widget_add_events(GTK_WIDGET(drawingarea1),
                            gtk_widget_get_events(drawingarea1) | GDK_BUTTON_PRESS_MASK);
      gtk_widget_add_events(GTK_WIDGET(drawingarea1),
                            gtk_widget_get_events(drawingarea1) | GDK_BUTTON_RELEASE_MASK);
      gtk_widget_add_events(drawingarea1,
                            gtk_widget_get_events(drawingarea1) | GDK_POINTER_MOTION_MASK);
 
  }
  /* ここまで */
  ...
lookup_widget()関数は、support.cの中に実装されており、gladeが自動的に生成してくれます。 この関数を用いると、ウィジェットの名前からウィジェットへのポインタを取得することができます。 これにより、描画領域ウィジェットへのポインタを取得し、それぞれイベントを通知するように設定します。


図形データの管理

直線データをリスト構造を用いて管理することにします。 直線データには、始点の座標と終点の座標が含まれます。 また、GDKは、座標を保持する構造体として、GdkPointを定義しています。 GdkPointは、以下のように定義されています(gdktypes.h)。

...
/* Type definitions for the basic structures.
 */
typedef struct _GdkPoint	      GdkPoint;
...
struct _GdkPoint
{
  gint x;
  gint y;
};
...
そこで、data_struct.hというファイルを作成して、以下のような構造体を定義します。
#include 

typedef struct line {
    GdkPoint sp;
    GdkPoint ep;
} line_t;

typedef struct line_list{
    line_t line;
    struct line_list *next;
} line_list_t;
ちなみに、GdkPointは、gdktypes.hで定義されていますが、gtk.hをインクルードすると gdktypes.hもインクルードされるので、gtk.hをインクルードします。


"button_press_event"イベントハンドラの実装

マウスのボタンが押された時のイベントハンドラを実装します。 以下のようにイベントハンドラを実装しました(callbacks.c)。

...
#include "data_struct.h"

line_list_t *line_list_head = NULL;
line_list_t *line_list_tail = NULL;

...
gboolean
on_drawingarea1_button_press_event     (GtkWidget       *widget,
                                        GdkEventButton  *event,
                                        gpointer         user_data)
{
     gint cur_x,cur_y;
     line_list_t *newitem;

     cur_x = (gint)event->x;
     cur_y = (gint)event->y;

     if((newitem = malloc(sizeof(line_list_t)))==NULL){
         g_print("malloc failed\n");
         return FALSE;
     }

     newitem->line.sp.x = cur_x; newitem->line.sp.y = cur_y;
     newitem->line.ep.x = cur_x; newitem->line.ep.y = cur_y;
     newitem->next = NULL;
     if(line_list_head == NULL){
         line_list_head = newitem;
     }else{
         line_list_tail->next = newitem;
     }
     line_list_tail = newitem;

     gtk_widget_queue_draw(widget);

     return TRUE;
}
まず、直線データを管理するリストの先頭のポインタをグローバル変数として確保します。 新しく確保した直線データはリストの末尾に追加するため、リストの末尾を指すポインタも確保します。

on_drawingarea1_button_press_event()関数は、GdkEventButton構造体へのポインタを引数に持ちますが、 この引数のうち、GdkEventButton構造体の中に、マウスのボタンが押された時のポインタの座標やボタンの種類(左ボタン、中ボタン、右ボタン等)が格納されています。 そこで、まず、ポインタの座標を取得します。GdkEventButton構造体では、ポインタの座標をgdouble型で保持しているので、gint型にキャストしています。

次に、直線情報を格納する領域をmalloc()で確保します。 マウスボタンが押された時点では、始点と終点は同一なので、取得したポインタの値を 始点と終点にそれぞれ設定します。 生成したデータはリストの末尾なので、nextには、NULLを設定します。

そして、生成した直線情報をリストに接続します。 生成した直線情報が最初のものである場合(line_list_head==NULLの場合)は、line_list_headに 接続します。そうでない場合は、末尾のリストの次に接続します。 接続した直線情報は末尾になるので、末尾を示すポインタを更新します。 最後に、"expose_event"を通知するため、gtk_widget_queue_draw()を呼びます。


"button_release_event"イベントハンドラの実装

マウスのボタンが離された時のイベントハンドラを実装します。 以下のようにイベントハンドラを実装しました(callbacks.c)。

gboolean
on_drawingarea1_button_release_event   (GtkWidget       *widget,
                                        GdkEventButton  *event,
                                        gpointer         user_data)
{
    gint cur_x,cur_y;

    cur_x = (gint)event->x;
    cur_y = (gint)event->y;
    line_list_tail->line.ep.x = cur_x;
    line_list_tail->line.ep.y = cur_y;

    gtk_widget_queue_draw(widget);

    return TRUE;
}
ボタンが離された時の座標を取得し、ボタンを押した時に確保した直線情報に終点の情報を設定します。


"expose_event"イベントハンドラの実装

マウスの入力によって生成された直線データの描画を行います。 以下のようにイベントハンドラを実装しました(callbacks.c)。 直線情報のリストを先頭からたどって、一つずつ描画していきます。

gboolean
on_drawingarea1_expose_event           (GtkWidget       *widget,
                                        GdkEventExpose  *event,
                                        gpointer         user_data)
{
    line_list_t *cur;

    for(cur = line_list_head; cur ; cur = cur->next){
        gdk_draw_line(widget->window,widget->style->fg_gc[widget->state],
                      cur->line.sp.x, cur->line.sp.y,
                      cur->line.ep.x, cur->line.ep.y);
    }

    return TRUE;
}
ここまでを実装して、コンパイルするとボタンを押した地点から離した地点までの間の直線を引くことができることが確認できます。 但し、ボタンを押してから離すまでの間、直線が描画されないので、少々直線が引きにくいと思います。 ボタンを押してから離すまでの間、直線の始点からマウスポインタまで直線が引かれていたらずっと入力しやすいはずです。 マウスポインタが動作する毎に、直線を描画するように、"motion_notify_event"ハンドラを実装します。


"motion_notify_event"イベントハンドラの実装

マウスポインタが動作する毎に呼び出されるハンドラです。 マウスポインタが動作する度に終点の座標を更新し再描画すれば、ボタンを押してから離すまでの間、始点からポインタまでの直線が描画できます。 尚、終点の座標更新と再描画処理は、マウスポインタが押されてから離されるまでです。 この状態を識別するためのフラグ(drawing_flag)をグローバル変数として確保します。 そして、ボタンが押された時と離された時にこのフラグの設定を行います。 これらの変更を、on_drawingarea1_button_press_event()とon_drawingarea1_button_release_event()に加え、 以下のようにon_drawingarea1_motion_notify_event()を実装します。

on_drawingarea1_button_press_event()

...
#include "data_struct.h"

line_list_t *line_list_head = NULL;
line_list_t *line_list_tail = NULL;
unsigned drawing_flag = 0;
...
gboolean
on_drawingarea1_button_press_event     (GtkWidget       *widget,
                                        GdkEventButton  *event,
                                        gpointer         user_data)
{
     gint cur_x,cur_y;
     line_list_t *newitem;

     drawing_flag = 1;  /* ここを追加 */

     cur_x = (gint)event->x;
     cur_y = (gint)event->y;
...
on_drawingarea1_button_release_event()
gboolean
on_drawingarea1_button_release_event   (GtkWidget       *widget,
                                        GdkEventButton  *event,
                                        gpointer         user_data)
{
    drawing_flag = 0; /* ここを追加 */

    /* 終点の設定処理は motion_notify_eventで行うので削除  */

    return TRUE;
}
on_drawingarea1_motion_notify_event()
gboolean
on_drawingarea1_motion_notify_event    (GtkWidget       *widget,
                                        GdkEventMotion  *event,
                                        gpointer         user_data)
{
    gint cur_x,cur_y;

    if(drawing_flag){
        cur_x = (gint)event->x;
        cur_y = (gint)event->y;
        line_list_tail->line.ep.x = cur_x;
        line_list_tail->line.ep.y = cur_y;

        gtk_widget_queue_draw(widget);
    }

    return TRUE;
 }
このように実装すると、ボタンを押した地点から直線が引かれ、ポインタを動かすとポインタのある場所まで直線が引かれるようになります。
ここまで実装して、直線で絵を描いた様子を図18に示します。
図18 直線で描いた絵
直線で描いた絵

ここまで実装したソースは、ここからダウンロードできます。
シグナルハンドラの実装 〜長方形と楕円の描画〜 (工事中)
GTK/GDK/gladeによるプログラムメモ Topへ
Copyright (C) 2004 Kohta NAKASHIMA
All Rights Reserved.