用 Guile 来为 GTK+ 应用添加扩展语言 Scheme
简介
我们这里说的扩展语言又称嵌入式脚本语言,在开源社区,有一种工具可以为你开发的应用添加这种嵌入式脚本语言功能,这种工具就是扩展语言开发库,我们只要在应用中连接这一扩展语言开发库,经过简单的处理,就可以为我们的应用增加嵌入式脚本语言功能,也就是说可以为我们的应用编写脚本语言代码,达到相应的功能。本文用到的与这些相关的开发工具有三种:Scheme 语言、Guile 和 GTK+。
Scheme 语言是一种函数编程语言,是古老的 LISP 语言的分支,它的特点是既简单易用,又富有灵活的技巧,同时它还具有挑战性,因为它具有目前许多新生的编程语言还不具备的特色。目前 Scheme 语言已经成为初学编程者首选的语言,也是值得有经验的编程者去研究的语言。它也是扩展语言较好的选择之一。
Guile是GNU工程的扩展语言库(Project GNU's extension language library),它是一个Scheme语言的实现版本,打包成为一个库后,你可以把这个库链接到你的应用中,这样你就为你的应用增加了一种扩展语言,这种语言就是Scheme语言。关于Guile的编译安装,见《Scheme语言概要(上)》。
GTK+是一种跨平台的图形界面开发工具,采用C语言来开发,在Linux平台,它是基于XWindow的GNOME桌面环境的基础开发工具,应用它可以在轻松的开发出图形界面应用程序来,本文就选用它来做GUI开发工具。
关于Scheme语言、Guile和GTK+的更多信息见参考资料。
目标
我们的目标很简单,就是用GTK+开发一个简单的应用,结合Guile来为这个简单的应用增添一种扩展语言--Scheme语言,从而使我们的这个简单应用具有灵活的扩展功能。
为什么加扩展语言?加什么样的扩展语言?加扩展语言能达到什么样的功能和效果?如何才能加上扩展语言? 扩展语言会让您的简单应用的功能并不简单。
像编辑软件、图像处理软件、游戏、专家系统、办公套件等大型工具或软件开发项目拥有自己的扩展语言,扩展语言使它们多了一双无形的翅膀,灵活使它们更强大。
目前在不同的应用领域有三个代表性的工具软件都不约而同的具有这样的扩展语言,首先是编辑工具软件GNUEmacs,它的扩展语言称为Elisp;其次是著名的CAD工具软件AutoCAD,它的扩展语言称为AutoLisp;最后是开源的图像处理工具软件GIMP,它的扩展语言称为Script-Fu,用它编写的工具可以在GIMP中轻松创建下面这样有特色的图像。这些工具软件的扩展语言和Scheme语言一样,都是LISP语言的变体。而Guile做为GNU扩展语言库则正好为我们提供了达到这一功能的有效途径。


(这是用GIMP1.3.x做的图像,可以在菜单中选择相应的创建功能,只需输入一串字符和更改几个参数就可以轻松创建,实现这些功能的脚本语言称为Script-Fu,它们的代码可以在/usr/share/gimp/scripts目录中找到,是Scheme语言代码)
我们做一个绘图软件,简单得很,以至于我们只能看到画板,而不能用鼠标来画图,(因为实现鼠标绘图要加很长一段令人费解的C代码)。用什么画图呢?用扩展语言代码。经过一翻简单的处理,我们可以用扩展语言代码在绘图板上画点、线、矩形、弧线、圆形,添充矩形、扇形、圆,更改颜色、输出字符串等功能,要画什么样的图形,只需要编写代码就可以了。
扩展结构原理
怎样才能为应用加上扩展语言呢?首先得做出绘图板来,我们用GTK+来做应用程序的主体,GTK+中的GtkDrawarea控件的主要用途就是绘图的,用它可以轻松的做一个绘图板。画板是做出来了,怎样画图呢?在GTK+的底层绘图工具GDK中提供了一系列原始的基本绘图函数gdk_draw_*(见下面介绍),用这些函数就可以在画板的GdkWindow上绘图了。如果用这些函数来编码绘图是一件非常繁琐的事情;事实上,应用程序加上Guile库后,只要将这些绘图函数转换为Scheme语言过程,就可以用Scheme语言代码来轻松绘图了。
如果您还没弄懂的话,看一看下面我们这个应用的结构原理图:

从图中可以看出,我们系统中必需安装有GTK+2.0开发库和Guile1.6.4开发库,否则我们将无法继续下面的构建应用和测试。
下面是我们应用执行时的效果图(在Fedora Core 2.0下运行),白色有图形的地方就是GtkDrawarea控件,即绘图板。在标明绘图文件的单行输入框里输入绘图文件名(如draw.scm),然后点击执行按钮,即在绘图板上绘制您想要的图形。在标明图像的单行输入框里输入图像名(默认为test),然后点击保存按钮,即在将绘图板上的图像以PNG格式保存在当前目录中(默认即保存为test.png文件)。

应用的创建
如果您熟悉GTK+开发的话,创建上面的图形窗口是一件轻松的事。关键是创建Drawarea控件,定义指针area,则area->window即指向area控件底层的GdkWindow,我们应用GDK中的gdk_draw_*系列函数就可以在此控件的GdkWindow上绘图并能正常显示出来。
同时我们还应当将此area复制一个pixmap指针用于保存文件,也就是说在向屏幕绘制图形的同时还要向pixmap绘图(不可见),保存时只保存此pixmap中的图像就可以了,这样做虽然很笨拙,但可以实现,其它方法(如直接保存area的内容为图像)笔者尚未实现。GTK+中的GDK-PIXBUF库提供了保存图像功能,我们这里简单的利用了一下,用gdk_pixbuf_get_from_drawable函数取得pixmap的pixbuf,然后用gdk_pixbuf_save函数将取得的pixbuf保存为文件,可以选择多种图像格式,为了简便我们只用PNG格式保存,这样我们的应用就更具实用价值了。
我们设定画板的默认尺寸为600x400,在绘图区上自动形成一个以左上角为原点(0,0)高为 400 像素宽为 600 像素的平面直角坐标系,以此为基点,我们就可以用 gdk_draw_* 系列函数在画板上绘图了。
以下为 gdk_draw_* 系列函数以及此应用涉及的其它相关函数的简单介绍(更详细的信息见参考资料):
//画点函数
void gdk_draw_point (GdkDrawable *drawable, GdkGC *gc, gint x, gint y);
//画线函数
void gdk_draw_line (GdkDrawable *drawable, GdkGC *gc,
gint x1_, gint y1_, gint x2_, gint y2_);
//画矩形函数
void gdk_draw_rectangle (GdkDrawable *drawable, GdkGC *gc, gboolean filled,
gint x, gint y, gint width, gint height);
//画弧线函数
void gdk_draw_arc (GdkDrawable *drawable, GdkGC *gc, gboolean filled,
gint x, gint y, gint width, gint height, gint angle1, gint angle2);
//绘制字符串函数
void gdk_draw_layout (GdkDrawable *drawable, GdkGC *gc,
gint x, gint y, PangoLayout *layout);
//画多边形函数
void gdk_draw_polygon (GdkDrawable *drawable, GdkGC *gc, gboolean filled,
GdkPoint *points, gint npoints);这些函数中都包括GdkDrawable *drawable和GdkGC *gc这两个参数,它们分别代表我们的绘图板和绘图的前景颜色;
在gdk_draw_rectangle、gdk_draw_arc和gdk_draw_polygon函数中还有一个gboolean filled参数,用来指定是否添充颜色,如为TRUE则添充颜色,如为FALSE则不添充颜色。
需要说明的是在gdk_draw_arc函数的最后是两个关于角度的参数,其中angle1表示此弧线旋转的角度,参数angle2表示此弧线的角度,此函数将圆周分为360度后,又将其中的1度64等分,也就是说,angle2为1的话,表示此弧线只有六十四分之一度;而angle1为1的话,则表示将此弧线旋转六十四分之一度。
gdk_draw_polygon函数中的GdkPoint结构如下所示:
struct GdkPoint {
gint x;
gint y;
};正好用Scheme语言中的点对来处理,car对应x,cdr对应y,而gdk_draw_polygon需要的GdkPoint数组指针正好用列表来实现。
gdk_draw_layout来向绘图板输出字符串,其中的PangoLayout *layout参数表示要输出的字符串,可以用gtk_widget_create_pango_layout函数创建。
另外我们还用到void gdk_gc_set_rgb_fg_color(GdkGc *gc, GdkColor *color);函数,用来设置绘画板的前景颜色,这个函数用到了GdkColor结构,此结构中用red, green, blue三个整型值来表示RGB颜色值,如此我们可以用RGB规则轻松设定颜色。
还有很多其它绘图及相关函数,此应用并未涉及,有兴趣的读者可以试一试。关于应用中控件的构建和组织可以见参考资料中的文章,此处不做更多的说明。
Guile中的C API
当了解了上面的绘图功能后,下一个课题就是Guile了。在Guile中,Scheme语言的每个R5RS中定义的基础过程几乎都有一个C函数与之相对应,也就是说,每一条Scheme语句都是在执行一个或多个C函数。如 Scheme 语言过程 string? 事实上就是在运行scm_string_p 函数,而 string->list 过程则是在运行 scm_string_to_list 函数。要调用 Guile 开发库的 C API,只需要在我们的代码中加入 #include <libguile.h> 就可以了。另一个重要的功能是初始化Guile的动态链接库以调用Guile的各种功能,只要在代码中执行scm_init_guile函数即可实现。
下面我们编写C代码来调用Guile中的C API,以此达到运行简单的Scheme语句的目的,代码如下:
//tt.c
#include <libguile.h>
int main(int argc, char *argv[])
{
char *ver = "(display \"Guile version \")(display (version))";
char *newline = "(newline)";
char *lambda = "(define newdisplay (lambda (n) (display n) (display \"\t\")))";
char *call = "(for-each newdisplay '(Tom Bob Peter Jim Lucy))";
scm_init_guile();
scm_c_eval_string(ver);
scm_c_eval_string(newline);
scm_c_eval_string(lambda);
scm_c_eval_string(call);
scm_c_eval_string(newline);
return 0;
}
//the end再做一个简单的Makefile来实现make编译,内容如下:
CC = gcc
CFLAGS = `guile-config compile`
LIBS = `guile-config link`
all:
$(CC) $(CFLAGS) -c tt.c
$(CC) $(LIBS) tt.o -o tt编译后,执行./tt程序,结果如下所示:
Guile version 1.6.4 Tom Bob Peter Jim Lucy
可见我们完全可以用C语言来实现自己的Scheme语言代码的运行,其中包括Scheme语言中的变量、过程的定义和调用。此处只用到了scm_c_eval_string函数,实事上几乎所有的Guile的C API可以像它这样来调用。
如果我们逆向思考,我们就可以自己定义一个C函数,通过某种功能将其转换为新的Scheme过程,再运行这个新定义的Scheme过程,就是执行我们自己定义的新的C函数。事实的确如此!
由Scheme到C
下一步就是如何为Guile定义C函数,使此C函数能转换为Scheme过程。这样我们就不得不提到一个Guile的C API中经常用到的自定义结构体类型--SCM,几乎Guile中所有的C API都要用到这一结构类型,它是Guile中的数据类型的通用体,也就是说,Guile在解释Scheme语言代码时,将所有的Scheme语言中的数据类型变量都做为一个SCM结构体类型来处理。Guile提供了一系列函数来将SCM结构转换为C语言其它类型,如整型、浮点型、字符指针类型等;同样,我们也可以将C语言中的整型、浮点型、字符指针类型变量转换为SCM结构,这样Guile中的C API就可以正常处理这一变量了。
如果我们正常定义一个C函数int add(int x, int y) ,转换为Scheme语言过程则应当是(add x y),而我们定义的C函数并不能为Guile正确执行,Guile所执行的应当是SCM add (SCM x, SCM y) 这样的函数,因为在Guile在执行(add x y)过程时,将x和y做为一个SCM结构来处理,返回值也应当是一个SCM,所以我们应当设计SCM add(SCM x, SCM y)这样的C函数。
如此,则我们将函数 void gdk_draw_point(GdkWindow *window, GdkColor gc, int x, int y) ,应当简单的封装为 SCM draw_point(SCM x, SCM y) 以便于Guile解释执行,而其中的GdkWindow *area和GdkColor gc_front则应首先定义为全局变量,直接在函数中调用,函数代码应当是:
static SCM draw_point(SCM x, SCM y)
{
gdk_draw_point(area, gc_front,
SCM_INUM(x), SCM_INUM(y));
gdk_draw_point(map, gc_front,
SCM_INUM(x), SCM_INUM(y));
}其中的area和gc_front分别代表绘图区和前景颜色,map则代表用在保存时中用到的pixmap,在构建绘图控件时定义。由于其返回类型是空值,所以可以不做返回操作。还有就是这些将转换并封装为Scheme语言过程的函数都应当定义为静态的。
其中的SCM_INUM宏是Guile库内部定义的,可以将SCM结构转换为一个整型数;这样的宏还有SCM_STRING_CHARS和SCM_SYMBOL_CHARS,其中的SCM_STRING_CHARS宏将Scheme语言中的字符串型SCM结构转换为字符型指针;而SCM_SYMBOL_CHARS宏则将符号型SCM结构转换为字符型指针,我们在绘制输出字符串时就用到SCM_STRING_CHARS宏,这是因为我们设计的输出字符串的Scheme过程为(_string x y "some string here")。输出字符串的函数代码定义如下:
//定义绘制输出字符串函数
static SCM draw_string(SCM x, SCM y, SCM str)
{
PangoLayout *layout =
gtk_widget_create_pango_layout(area, SCM_STRING_CHARS(str));
gdk_draw_layout(area->window, gc_front,
SCM_INUM(x), SCM_INUM(y), layout);
gdk_draw_layout(map, gc_front,
SCM_INUM(x), SCM_INUM(y), layout);
}这其中最复杂的就是绘制多边形函数,我设计导出的Scheme过程为(_polygon point-list, point-number filled),第一个参数为点列表,第二个参数是点数,第三个参数为是否添充。用SCM_NFALSEP宏可以将Scheme语言中的#f转换为0,#t转换为1,这样是否添充这个问题就解决了;而gdk_draw_polygon函数需要的GdkPoint指针数组需要分配内存来实现,我们设计的参数是一个列表,可以用scm_list_ref来取值,关键是这个函数的第二个参数并不是一个简单的整数,而是一个SCM结构,可以用scm_int2num函数将一个C整数转换为SCM结构,再用SCM_CAR和SCM_CDR分别取点对的左侧和右侧分别赋给GdkPoint结构中的x和y,绘图后释放内存,达到目的。
//绘制多边形函数,参数为点数组,点数,是否添充
static SCM draw_polygon(SCM spoints, SCM number, SCM filled)
{
int i ;
GdkPoint *points = g_new(GdkPoint, SCM_INUM(number)) ; //为点分配内存
for(i=0; i<SCM_INUM(number); i++)
{
points[i].x = SCM_INUM(SCM_CAR(scm_list_ref(spoints, scm_int2num(i))));
points[i].y = SCM_INUM(SCM_CDR(scm_list_ref(spoints, scm_int2num(i))));
//循环赋值,关键是将C中的int转换为SCM的 scm_int2num 函数
}
gdk_draw_polygon(area->window, gc_front, SCM_NFALSEP(filled), points, SCM_INUM(number));
g_free(points); //释放内存
}更多函数定义请看此应用的C源码,可以到参考资料中下载。
将C函数导出为Scheme过程
我们可以将上面定义好的C函数导出Scheme过程,这一功能由Guile中的C函数scm_c_define_gsubr实现,这个函数需要5个参数,第一个参数是要转换为成的Scheme过程名,第2,3,4三个参数与要转换的Scheme过程的参数数量有关,第2个参数表示必需的参数的数量,第3个参数表示可选的参数的数量,第4个参数表示参数的数量是否可变,如果此参数置0则参数的总量为必需参数和可选参数的和,如果此参数非0,则参数的总量为必需参数与可选参数的和再加1;最后一个参数是要转换的C函数名指针,这一函数名必须按上面的要求,即返回类型和参数均为SCM结构类型。
我们的应用最后导出六个原始的绘图过程,分别是:
1、_point,画点,两个参数 x, y ;
2、_line,画线,四个参数 x1, y1, x2, y2 ;
3、_rectangle,画矩形,五个参数 x, y, width, height, filled ;
4、_arc,画弧线,七个参数 x, y, width, height, angle1, angle2, filled ;
5、_polygon,画多边形,三个参数 point-list, point-number, filled ;
6、_string,绘制字符串文本,三个参数 x, y, string;
7、_color,设定绘图颜色,三个参数 red, green, blue 。
导出代码如下:
//编码绘图初始化过程
void draw_init(void)
{
scm_init_guile();
//初始化guile
scm_c_define_gsubr("_rectangle", 5, 0, 0, draw_rect);
scm_c_define_gsubr("_point", 2, 0, 0, draw_point);
scm_c_define_gsubr("_line", 4, 0, 0, draw_line);
scm_c_define_gsubr("_arc", 7, 0, 0, draw_arc);
scm_c_define_gsubr("_polygon", 3, 0, 0, draw_polygon);
scm_c_define_gsubr("_string", 3, 0, 0, draw_string);
scm_c_define_gsubr("_color", 3, 0, 0, set_front_color);
//定义六个原始绘图过程
}当我们将上面的C函数转换为Scheme语言过程后,我们就可以在我们的Scheme语言代码中加入这些过程了,但要达到绘图效果还得有必须的一步,就是让我们的应用执行这些Scheme语言代码,以达到绘图效果。这样我们就必需在运行按钮点击信号的回调函数中加入执行Scheme语言代码的功能,这就用到了Guile的C API中的scm_c_primitive_load函数,它的功能就是打开并运行一个Scheme语言代码文件,也就是我们的绘图文件。如此我们就真正达到了用代码画图形的功能了。(关于图像的保存读者可自行阅读代码)
编译测试
我们可以根据上面的Makefile文件再写一个用到GTK+库的Makefile来编译我们的应用,这个Makefile的内容如下:
CC = gcc GTK2_CFLAGS = `pkg-config --cflags gtk+-2.0` GTK2_LIBS = `pkg-config --libs gtk+-2.0` GUILE_CFLAGS = `guile-config compile` GUILE_LIBS = `guile-config link` all: $(CC) $(GTK2_CFLAGS) $(GUILE_CFLAGS) -c brush.c $(CC) $(GTK2_LIBS) $(GUILE_LIBS) -o brush brush.o clean: rm *.o brush test.png *~ -fr
当我们编译成功后,就可以编写一个简单的绘图文件来测试一下了,如我们在绘图文件test.scm中只写一行:(_line 100 100 300 300),保存后,在我们的应用中运行一下,应该在绘图板上出现一条斜线,OK,成功了。
下面代码测试了一下弧线、弧线的添充和旋转:
;;;设定前景为蓝色 (_color 0 0 65535) ;;; 宽高均为 100 的 270 度的弧,旋转0度 (_arc 80 100 100 100 0 (* 64 270) #f) ;;; 同上,添充效果 (_arc 200 100 100 100 0 (* 64 270) #t) ;;; 宽高均为 100 的270度的弧,旋转90度 (_arc 320 100 100 100 (* 64 90) (* 64 270) #f) ;;; 同上,添充效果 (_arc 440 100 100 100 (* 64 90) (* 64 270) #t)
测试效果如下图所示:

还可以用(_polygon (list (cons 100 150) (cons 100 200) (cons 200 200)) 3 #f)来画一个三角形,自己试一试吧!
封装与扩展
我们定义的六个原始过程用起来比较繁琐,但只要对它们进行简单的封装,就可以实现新的过程,来提高它们的易用性。
1、对颜色的封装
我们的应用中提供了简单的RGB前景颜色设定过程_color,此过程需要三个参数,分别代表RGB的三个颜色值,下面代码对这个过程进行了简单的封装:
;;;set the front color
(define set-color
(lambda (color)
(case color
((white) (_color 65535 65535 65535))
((black) (_color 0 0 0))
((red) (_color 65535 0 0))
((green) (_color 0 65535 0))
((blue) (_color 0 0 65535))
;;;如果你知道更多的RGB颜色值请在此处按上面规则扩展
(else
(display "Error : color not defined!\n")))))如此,我们只要用set-color过程中加一个颜色名做参数就可以了,如(set-color 'black)。
2、设定背景颜色
我们的应用中只提供了设定前景颜色的过程,由于背景和前景是相对的,完全可以用设定前景颜色功能来实现设定背景颜色过程,就是设定前景颜色后,用添充矩形过程来实现,如果此矩形的宽高为画板的默认的宽高,就实现了整个画板的背景颜色设定。如下面代码所示(注意,这样处理后前景和背景颜色都一样了,再绘图时一定要先改一下前景颜色):
(define set-back-color (lambda (red green blue) (begin (_color red green blue) (_rectangle 0 0 600 400 #t))))
3、丰富画点功能
下面代码是对画点过程的一系列封装,实现了point,draw-point,draw-dot-hline,draw-color-dot-hline四种功能,相信读者会封装出更优秀的功能来。
;;;画点 100 100
(_point 100 100)
;;;画点 150, 150
(define point _point)
(point 150 150)
;;;画点 125, 125
(define draw-point _point)
(draw-point 125 125)
;;;画点状横线,从x,y开始,宽度为width,每隔5点画一点
(define draw-dot-hline
(lambda (x y width)
(let ((a x) (b (+ x width)))
(while (< a b)
(_point a y)
(set! a (+ a 5))))))
;;;测试
(draw-dot-hline 30 30 150)
;;;画带颜色的虚线,颜色为color,从x,y开始,宽度为width
(define draw-color-dot-hline
(lambda (color x y width)
(begin
(set-color color)
(draw-dot-hline x y width)
(set-color 'black))))
;;;测试
(draw-color-dot-hline 'blue 50 50 200)4、画圆形和添充圆形过程
画圆形过程可以用画弧线过程来实现,毕竟圆形是一条封闭的弧线。只要设定弧线的宽高相等,不旋转(旋转也没有效果),并且度数为360度(应该是64x360,因为这里的单位是64分之一度),就是一个圆了。代码如下:
;;;draw a circle (define draw-circle (lambda (x y d) (_arc x y d d 0 (* 64 360) #f))) ;;;fill a circle (define fill-circle (lambda (x y d) (_arc x y d d 0 (* 64 360) #t)))
5、实现项目列表
项目列表,即先画一个圆形或矩形,然后再输出一串字符串,同时还可以设定项目标号的颜色,这一功能可由下面的代码实现:
(define item
(lambda (x y color str)
(begin
(set-color color)
(fill-rect x y 10 10)
(set-color 'black)
(draw-string (+ x 12) y str))))
;;;test
(item 250 300 'black "A.com")以上几项只是基础功能的封装,还有画正方形,正三角形,菱形等等常用图形功能尚未实现。我们可以将这些函数统一做一个文件,在起动此应用时就调用这一文件,这样我们就可以在新的绘图文件中调用这些功能了。(详细内容见源码包中的src/brush.scm文件,如果你想实现上面的功能,请将它写入这个文件中去)
事实上我们的结构原理示意图就是用绘图代码做出来的。你发现用Guile为GTK+应用做扩展语言的功能强大了吧:-),由于笔者条件所限,没能将此软件在开源网站上发布(这正是笔者所想要的),如果那位朋友愿意帮这个忙的话,将不胜感激,当然,还望对图形或CAD感兴趣的朋友多加指点。
两个示例
在绘图领域,笔者是一个外行,下面两个示例,供大家参考,第一个是画条形图(ta.scm),第二个是折线形图(tb.scm),效果如下所示:


几个应注意的BUG
如果输入错误的绘图文件名,点击运行按钮时会崩溃;绘图文件中有代码错误时,应用同样也会崩溃;所以一定要保证绘图代码正确无误。
同时还要保证Guile的完整安装(头文件、链接库和可执行文件),否则编译时会出问题。如果完整安装在执行configure脚本时还出问题,可以将guile.m4复制到/usr/share/aclocal目录下,以保证configure脚本的正常运行。
我们的应用还不具有重绘功能,也就是说当应用的窗口被其它窗口遮蔽或最小化后画板上的图形会被擦掉,但只要再按一下运行按钮就会重新显示出来。
在GTK+2的低版本(2.2.x)中保存图像会出问题,不过高版本中就不存在这个问题了。 本文所有代码在RedHat Linux 9.0 系统环境下编译通过,GTK+库版本为2.4.0,Guile版本为1.6.4。在Fedora Core 2系统和最新的guile1.6.5版本中也可以正常运行。
相关主题
- Scheme语言爱好者的大本营,http://www.schemers.org。
- Guile的家,http://www.gnu.org/software/guile/
- Guile的文档,http://www.gnu.org/software/guile/docs/docs.html
- GTK+的主网站 http://www.gtk.org
- GTK+的在线文档,http://developer.gnome.org/doc/API/gtk/index.html
- GDK的在线文档,http://developer.gnome.org/doc/API/gdk/index.html
- 源码包下载 brush-lite-1.0.tar.gz