2007年10月19日

[012]原始的な描画プログラム〜その3〜

今回もまた「原始的な描画プログラム」です。ラバーバンドも描画できるようになり、少しだけドローソフトっぽくなってきた「原始的な描画プログラム」ですが、まだ致命的な欠陥があります。

試しに前回のプログラムを起動し、何か適当に描画してみてください。描画したら、ウィンドウの右上の最小化ボタンを押して最小化してみます。その後再びウィンドウを表示してみると、せっかく描画したものがきれいさっぱりなくなっている筈です。
ウィンドウのサイズを変えたときや、他のウィンドウに覆い隠された場合も同様の事が起こります。

この問題を回避するにはexposeイベント(再描画が必要なときに起こる)を処理するハンドラの中で今まで描画したものをもう一度描画させる必要があります。

そのためには「画面のどこをクリックしてどこに線を引いた」といった描画情報を保存しておき、exposeイベントが起きたときに保存した情報を元に描画を行う必要があります。

この場合大きく分けて次の二つの方法が考えられます。
  1. 始点と終点の情報を配列などに保存しておき、exposeイベントのハンドラの中で一本一本の線をdrawingareaに描画する
  2. drawingareaと同じ大きさの画像(バッキング・ピックスマップ)を用意しておき、一本一本の線の描画はバッキング・ピックスマップに対しておこなう。exposeイベントが起きたときにバッキング・ピックスマップをdrawingareaに描画する

今回は2番目のバッキング・ピックスマップを使う方法を試しています。この方法はダブルバッファリングとも呼ばれていて、アニメーションなどを行う場合に「ちらつき」を防止する方法として利用されている技術でもあります(*註1)


ソースです→LineDrawing3.tar.gz

いつものように解凍してできた、LineDrawing3というディレクトリの中のLineDrawing3.prjというファイルをanjutaで開けば好きなように弄りまわせる筈です。

各ウィジットのプロパティ

[window1]
タイトル→「直線描画パート3」
シグナル→「destroy」
ハンドラ→「gtk_main_quit」


[drawingarea]
名前→「drawingarea」
イベント→「00000000000100000100」(GDK_POINTER_MOTION_MASK,GDK_BUTTON_PRESS_MASK)
シグナル→「realize」
ハンドラ→「on_drawingarea_realize」
シグナル→「configure_event」
ハンドラ→「on_drawingarea_configure_event」
シグナル→「expose_event」
ハンドラ→「on_drawingarea_expose_event」
シグナル→「button_press_event」
ハンドラ→「on_drawingarea_button_press_event」
シグナル→「motion_notify_event」
ハンドラ→「on_drawingarea_motion_notify_event」



イベント処理
callbacks.c
enum
{
	FIRST_CLICK,	/* 1回目のクリック */
	SECOND_CLICK	/* 2回目のクリック */
};

/* クリックカウント用(1ビットカウンタ)*/
gint click_count	=	FIRST_CLICK;
	
/* 直線の始点の座標 */
gint	x0=0,y0=0;

/* ラバーバンドの終点座標(消去用) */
gint	x1=0,y1=0;

/* ラバーバンド描画用GC */
GdkGC	*rubber_gc=NULL;

/* drawingareaに対するバッキング・ピックスマップ */
GdkPixmap	*pixmap=NULL;

/* drawingareaが生成されたとき */
void
on_drawingarea_realize                 (GtkWidget       *widget,
                                        gpointer         user_data)
{
	/* マウスモーションイベントをブロック */
	g_signal_handlers_block_by_func(G_OBJECT(widget),
				G_CALLBACK(on_drawingarea_motion_notify_event),NULL);
	
	/* drawingareaの背景色を取得 */
	GtkStyle *style = gtk_widget_get_style(widget);
	GdkColor	bgcolor=style->bg[0];

	/* ラバーバンド用グラフィックコンテキストを生成 */
	rubber_gc = gdk_gc_new(widget->window);
	
	/* ラバーバンドの描画色(水色) */
	GdkColor rubber_color;
	rubber_color.red = 0;
	rubber_color.green = 0xffff;
	rubber_color.blue = 0xffff;
	
	/* ラバーバンド用前景色の設定 */
	GdkColor fgcolor;
	fgcolor.red = (rubber_color.red)^(bgcolor.red);
	fgcolor.green = (rubber_color.green)^(bgcolor.green);
	fgcolor.blue = (rubber_color.blue)^(bgcolor.blue);
	gdk_color_alloc(gdk_colormap_get_system(), &fgcolor);
	gdk_gc_set_foreground(rubber_gc, &fgcolor);
	
	/* XOR描画モードに設定する */
	gdk_gc_set_function(rubber_gc, GDK_XOR);
}

/* drawingareaのサイズが設定・変更されたとき */
gboolean
on_drawingarea_configure_event         (GtkWidget       *widget,
                                        GdkEventConfigure *event,
                                        gpointer         user_data)
{
	/* drawingareaの幅と高さ */
	int		width  =	widget->allocation.width;
	int		height =	widget->allocation.height;

	/* 現在のpixmapのバックアップ用 */
	GdkPixmap	*oldmap = NULL;

	/* 背景色描画用 */
	GdkGC	*bg_gc
		= widget->style->bg_gc[GTK_WIDGET_STATE(widget)];

	/* 新しく生成したpixmap上に
	   古いpixmapを描画するときに使うGC */
	GdkGC	*normal_gc
				= widget->style->fg_gc[GTK_STATE_NORMAL];
	
	/* 現在のpixmapをバックアップ */
	if(pixmap!=NULL)	oldmap = pixmap;

	/* 新しいpixmapを生成 */
	pixmap = gdk_pixmap_new(widget->window,width,height,-1);

	/* 生成したpixmapを背景色で塗りつぶす */
	gdk_draw_rectangle(pixmap,bg_gc,TRUE,0,0,width,height);
	
	/* 古いpixmapがあればそれを新しいpixmapに描画 */
	if(oldmap!=NULL)
	{
		gdk_draw_drawable(pixmap,normal_gc,oldmap,0,0,0,0,-1,-1);
		g_object_unref(oldmap);
	}

	return FALSE;
}

/* drawingareaが表示されるとき */
gboolean
on_drawingarea_expose_event            (GtkWidget       *widget,
                                        GdkEventExpose  *event,
                                        gpointer         user_data)
{
	/* 描画用GC */
	GdkGC *gc = widget->style->fg_gc[GTK_WIDGET_STATE(widget)];
	
	/* バッキング・ピクスマップを描画 */
	gdk_draw_drawable(widget->window,gc,pixmap,0,0,0,0,-1,-1);
	return FALSE;
}

/* drawingarea内でマウスのボタンが押されたとき */
gboolean
on_drawingarea_button_press_event      (GtkWidget       *widget,
                                        GdkEventButton  *event,
                                        gpointer         user_data)
{
	/* グラフィックコンテキスト(黒) */
	GdkGC		*gc = widget->style->black_gc;
	
	/* クリック時のマウスカーソルの座標 */
	gint	x = (gint)event->x;
	gint	y	=	(gint)event->y;
	
	switch(click_count)
	{
		/* 1回目のクリックの場合は始点を設定 */
		case	FIRST_CLICK:
			x0 = x;	y0=y;		/* 直線の始点を設定 */
			x1=x0;	y1=y0;	/* ラバーバンドの終点を初期化 */
		
			/* マウスモーションイベントのブロック解除 */
			g_signal_handlers_unblock_by_func(G_OBJECT(widget),
				G_CALLBACK(on_drawingarea_motion_notify_event),NULL);
			break;
		
		/* 2回目のクリックの場合は直線を描画 */
		case	SECOND_CLICK:
			gdk_draw_line(pixmap,gc,x0,y0,x,y); /* pixmapに描画 */
			gtk_widget_queue_draw(widget); /* drawingareaを再描画 */
		
			/* マウスモーションイベントをブロック */
			g_signal_handlers_block_by_func(G_OBJECT(widget),
				G_CALLBACK(on_drawingarea_motion_notify_event),NULL);
			break;
	}
	
	/* クリック・カウンタをカウントアップ */
	click_count++;
	click_count=click_count%2;
	
  return FALSE;
}

/* drawingarea内でマウスが動いたとき */
gboolean
on_drawingarea_motion_notify_event     (GtkWidget       *widget,
                                        GdkEventMotion  *event,
                                        gpointer         user_data)
{
	/* マウスカーソルの座標 */
	gint	x = (gint)event->x;
	gint	y	=	(gint)event->y;
	
	/* 前回の線を消去 */
	gdk_draw_line(widget->window,rubber_gc,x0,y0,x1,y1);
	
	/* ラバーバンドを描画 */
	gdk_draw_line(widget->window,rubber_gc,x0,y0,x,y);
	
	/* 現在のマウスポインタの位置を保存 */
	x1=(gint)event->x;
	y1=(gint)event->y;
	
  return FALSE;
}

赤く示した部分は今回追加または変更を行った部分です。

参考情報

http://www.gnome.gr.jp/docs/gtk+-2.0.x-tut/sec-thedrawingareawidget.html
が非常に参考になります。しかし、gdk_draw_pixmap()に関しては、Devhelpで調べると「gdk_draw_pixmap is deprecated and should not be used in newly-written code. Use gdk_draw_drawable() instead.」と書いています。gdk_draw_drawable()を使った方が良いようです。


GdkPixmapの生成と初期化

GdkPixmapの生成にはgdk_pixmap_new()を使います。
http://www.gnome.gr.jp/docs/gtk+-1.2.x-refs/gdk/gdk-bitmaps-and-pixmaps.html#GDK-PIXMAP-NEW
には次のように説明されています。
gdk_pixmap_new ()

GdkPixmap* gdk_pixmap_new(GdkWindow *window,
gint width,
gint height,
gint depth);

指定したサイズと深さ (depth) のピックスマップを新規に生成します。
window:GdkWindow で、新しいピックスマップの初期値を決定するのに使用される。depth が指定された場合は NULL にすることも可能
width:新しいピックスマップの幅 (ピクセル単位)
height:新しいピックスマップの高さ (ピクセル単位)
depth:新しいピックスマップの深さ (ビット数/ピクセル)。depth = -1 かつ window != NULL の場合は window の深さと同じになる。
返り値:GdkPixmap

今回の例では
/* 新しいpixmapを生成 */
	pixmap = 
gdk_pixmap_new(widget->window,width,height,-1);
のように使っています。

また、今回の例では以下のようにして生成したピックスマップの色をDrawingAreaの背景色と同じ色にしています。
/* 背景色描画用 */
	GdkGC	*bg_gc
		= widget->style->bg_gc[GTK_WIDGET_STATE(widget)];

/* 生成したpixmapを背景色で塗りつぶす */
	gdk_draw_rectangle(pixmap,bg_gc,TRUE,0,0,width,height);


ピックスマップの描画について

GdkPixmapを描画するには、gdk_draw_drawable()を使います。
void gdk_draw_drawable(GdkDrawable *drawable,
GdkGC *gc,
GdkDrawable *src,
gint xsrc,
gint ysrc,
gint xdest,
gint ydest,
gint width,
gint height);

gdk_draw_drawable.png

widthとheightについては-1が指定された場合はピックスマップのオリジナルの幅または高さが使われます。

今回の例では次のようにしてピックスマップを描画しています。
gdk_draw_drawable(widget->window,gc,pixmap,0,0,0,0,-1,-1);



再描画について

gtk_widget_queue_draw(GtkWidget *widget)

を使うことで強制的にexposeイベントを起こし、ウィジットを再描画させることができます。今回の例では直線の終点がクリックされバッキング・ピックスマップに直線が描画されたあとgtk_widget_queue_draw()を使ってdrawingareaを再描画しています。



(*註1)


http://www.gnome.gr.jp/docs/porting-apps-GNOME-2.0/ar01s18.html
には
「GdkWindow には二つの表示用バッファ (バッキングストア) が組み込まれています。」
と書いてあります。したがってGtkの場合、ちらつき防止の目的でバッキング・ピックスマップを使う必要は無いのかもしれません。
posted by knyakki at 16:21| Comment(1) | TrackBack(0) | プログラミング | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
失礼いたします
Posted by エロ at 2008年01月25日 23:49
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。
この記事へのトラックバックURL
http://blog.seesaa.jp/tb/61421171

この記事へのトラックバック
×

この広告は1年以上新しい記事の投稿がないブログに表示されております。