制作GTK+控件

转自: http://www.miidoo.cn/info_detail-261.html
在做GTK 自定义控件之前,应先了解两个问题,其一是GTK 中的GDK库完成了对X Window的核心Xlib库的封装,使之简化易用;其二是GTK 本身完成了绝大部分常用控件的封装,使之可在编程中灵活运用。所以读懂GTK 控件的源代码就应会写简单的自定义控件,透彻掌握GDK则会做出复杂的GTK 控件来。
 
1.GTK 控件简介
 
与大多数图形界面开发工具一样,GTK 的控件也是以对象的形式出现的。GTK 控件的基础对象GtkObject继承自GObject,所以具有GObject的所有特征,完全可以用创建GObject对象的方法来创建 GtkObject对象或新的GTK 控件,同时GTK 还提供了一套新的自定义控件的方式。
 
直接继承自GtkObject对象的控件主要是GtkWidget,它几乎是所有可视控件的共同的祖先,大多数控件共有的属性都包括在其中。与其它GUI开发工具不同的是,我们不用直接创建GtkObject或GtkWidget对象,而是用定义对象的实例结构和类结构的方式来定义对象,然后再通过类型注册来实现对象。
 
下图简单说明了一个GOBJECT对象的创建过程,也就是GTK 控件创建的过程:
 
2.一个简单的组合型控件的实现
 
我们的目的是创建一个控件,它由两部分构成,左面是一个文字标签,右面是一个单行文本输入控件,两者一同显示出来,在向文本控件输入信息,点击回车键后,会提示相应信息。基于上面的考虑,此控件应该继承自横向盒状容器。
 
(1)实例结构与类结构
 
与GObject 对象相同,GTK 控件对象也分为实例结构和类构,不同的是实例结构和类结构中的变量类型大多是在GTK 中已经定义过的了,我们可以直接应用。在实例结构中定义parent变量和在类结构中定义parent_class变量来实现读者朋友们关注的控件的继承关系。
 
下面是ouritem.h的代码:
 
#ifndef __OUR_ITEM_H__
#define __OUR_ITEM_H__
 
#include <gtk/gtk.h>
 
//定义类型宏和转换宏
#define TYPE_OUR_ITEM (our_item_get_type())
#define OUR_ITEM(obj) (GTK_CHECK_CAST(obj,TYPE_OUR_ITEM,OurItem))
 
//定义实例结构和类结构
 
typedef struct _OurItem OurItem;
typedef struct _OurItemClass OurItemClass;
 
struct _OurItem {
 GtkHBox parent; //父控件为横向盒状容器
 GtkWidget *label; //标签
 GtkWidget *entry; //单行文本录入
};
 
struct _OurItemClass {
 GtkHBoxClass parent_class;
 //下面定义函数指针,为所有OurItem实例所使用,即所有的OurItem控件在输入信息点击回车键后都执行此函数
 void (*enter_ok)(void);
};
 
GtkType our_item_get_type(void);
GtkWidget* our_item_new(void);
void our_item_set_label(GtkWidget* item, gchar* label);
GtkWidget* our_item_new_with_label(gchar* label);
 
#endif //__OUR_ITEM_H__
 
在上面的代码中我们定义了控件的类结构_OurItemClass和实例结构_OurItem,其中实例结构_OurItem中包含三个成员变量,一个是表示控件实例的父对象parent,另两个分别是控件前面显示的标签label和标签后面的单行输入控件entry,它们相当于控件的两个属性,我们定义的函数our_item_set_label就是用来改变标签属性的方法。这里未定义控件的函数,有兴趣的朋友们可以试一试,为控件加上函数。需要说明的是在实例结构中定义的属性或函数,每个控件的实例都有自己的属性和函数,它们可以是不同的值,也可以是相同的值,实例与实例之间并不影响。而在类结构中定义的属性或函数指针是唯一的,也就是说所有实例共有的,一旦某一实例改变了这一属性,其它实例得到的这一属性的值就是前一实例改变后的值,这也是为什么信号定义在类结构中的原因,因为控件的所有实例都有相同的信号。
 
控件的类结构 _OurItemClass中只有两个成员,一个是表示控件类的父对象类parent_class,另一个是函数指针enter_ok,它用来在发射我们自定义的信号时执行,以测试我们自定义的信号是否发射成功,由于这个函数指针是定义在类结构中的,所以此控件的所有实例都可以调用此函数指针执行。
 
(2)GtkTypeInfo结构
 
GTK 中的GtkTypeInfo结构可取代GObject中的GTypeInfo结构,包含以下内容:
 
1)控件的名称,字符串;
 
2)控件的类结构的长度,整型,一般用sizeof来取得
 
3)控件的实例结构的长度,整型,同上
 
4)控件的类结构的初始化函数,需要转换为GtkClassInitFunc型函数指针
 
5)控件的实例结构的初始化函数,需要转换为GtkObjectInitFunc型函数指针
 
6)最后两个值是未定义的,预留给以后扩展自定义控件功能时用
 
详细的定义见下面的代码。这样的结构定义较之GObject中的GTypeInfo结构的定义简化了许多,也更加清晰易懂。在定义完GtkTypeInfo 结构后,可以用gtk_type_unique函数来注册自定义的控件,这个函数有两个参数,第一个参数是自定义控件的父类型,如本例中将自定义控件封装在一个横向盒状容器中了,所以用GTK_TYPE_HBOX,读者可以根据自己需要的控件类型来定义;第二个参数是上面定义的GtkTypeInfo结构的地址或指针。如此,就完成了控件的定义和注册。同样也可以参考GObject对象的创建方法,来创建自定义的GTK 控件。
(3)信号的定义、发射与连接
 
这里自定义的信号是当按下回车键后,自动输出一行信息,说明输入已经结束。需要说明的是信号的定义不是在实例结构和类结构的定义(ouritem.h)中定义,而是在实例结构和类结构的实现(ouritem.c)中定义和实现的。
 
首先为信号定义标记,它是以枚举类型来实现的(见代码),为我们定义的信号命名为OURITEM_OK_SIGNAL,也是第一个信号名;最后一个信号名为LAST_SIGNAL,这样按照C语言中枚举类型进行定义,如果定义多个信号的话,可以自行添加。
 
然后,再定义一个整型数组ouritem_signals,其长度为LAST_SIGNAL,来保存信号创建后返回的值,这个值很关键,当发射信号的时候用到,如果多个信号的话,每个数组元素对应一个信号,而LAST_SIGNAL是没有具体做用的,只用来标识数组的长度。
 
函数g_signal_new来创建一个新的信号,它的第一个参数是信号名,它是以字母开始,其后可以是字母、数字、下划线或减号,这里命名为 "ouritem_ok";第二个参数是此对象的类型,用宏G_TYPE_FROM_CLASS来取得;第三个参数是信号运行时的标记,我们取值为 G_SIGNAL_RUN_FIRST,还有许多其它值可取(详见GOBJECT的API参考);第四个参数是函数指针在此对象的类结构中的偏移,一般用于调用对象的方法,这里我们调用函数指针enter_ok;第五个参数和第六个参数分别是此信号的类聚(accumulator)和类聚的数据,可以为空;第七个参数是用一标明此信号回调函数的返回类型和参数类型的closure_marshal,我们取值为g_cclosure_marshal_VOID__VOID;第八个参数是信号的返回值的类型,如果没有返回值,则为 G_TYPE_NONE;第九个参数标识我们自己加的参数的个数,我们不加自定义参数,所以设为0,如果有自定义参数可以加在这里,最后一个参数必需是 NULL。
 
对于一个有众多参数的函数,我们在应用时一定要多加小心,仔细理解,如果一个函数的参数超过三个的话,出错的情况就会大增加,何况有这么多呢:->
 
信号的创建一般在类初始化(our_item_class_init)中进行,这里我们用函数g_signal_emit来发射信号,它也有多个参数,它的第一个参数是对象的实例,用G_OBJECT来转换;第二个参数是信号的标记,即我们上面定义的ouritem_signals数组中的一个值,这里取 ouritem_signals[OURITEM_OK_SIGNAL];第三个参数是详细内容,可以设为0不做处理。还可以用 g_signal_emitv函数来发射信号,它的用法参考GOBJECT的API手册。
 
当我们在控件的类结构中定义并实现了信号的发射后,我们就可以在应用此控件时为控件的信号连接回调函数,即用g_signal_connect宏就可以实现最常用的连接,如要连接我们上面定义的ouritem_ok信号,代码可以写成:
 
g_signal_connect(G_OBJECT(ouritem),"ouritem_ok",
 
G_CALLBACK(our_callback), NULL);
 
这和常见的为按钮的clicked信号加回调函数是一样的,详细用法见下面的测试代码。
(4)其它函数的实现
 
这里除了信号的定义等函数外,还有一些函数需要定义,以进一步完善控件的功能。首先是信号发射的时机,我们这里设定当用户按下回车键后即发射此信号,所以要为单行文本录入控件的key_release_event信号加回调函数,来判断键入的信息,如果是回车键则发射我们定义的信号。
 
在发射信号后,我们需要显示一个简短的信息以证明信号已经发射,就是下面代码中定义的enter_ok函数,它的功能就是显示信息,表明信号成功发射。至于实例初始函数our_item_init和类初始化函数our_item_class_init,他们的实现和Gobject中的对象的实现是相同的。
 
此外创建控件的方法our_item_new和our_item_new_with_label都很简单,改变标签文字内容的方法our_item_set_label也只有几行代码,相信读者定会一目了然的。
以下为ouritem.c的完整代码:
 
 
 
#include <gtk/gtk.h>
 
#include <gdk/gdkkeysyms.h>
 
#include "ouritem.h"
 
//定义枚举类型,说明信号的名称和次序
 
enum {
 
 OURITEM_OK_SIGNAL,
 
 LAST_SIGNAL
 
};
 
static gint ouritem_signals[LAST_SIGNAL] = { 0 };
 
static void our_item_init(OurItem *ouritem);
 
static void our_item_class_init(OurItemClass *ouritemclass);
 
static void enter_ok(void);
 
void on_key_release(GtkWidget *entry, GdkEventKey *event, gpointer data);
 
//注册自定义控件
 
GtkType our_item_get_type(void)
 
{
 
 static GtkType our_item_type = 0;
 
 if(!our_item_type)
 
 {
 
 GtkTypeInfo our_item_info = {
 
   "OurItem", //控件名
 
   sizeof(OurItem), //控件实例的尺寸
 
   sizeof(OurItemClass), //控件类的尺寸
 
   (GtkClassInitFunc)our_item_class_init, //控件类初始化函数
 
   (cour_item_init, //控件实例初始化函数
 
   NULL, //
 
   NULL //
 
 };
 
 our_item_type = gtk_type_unique(GTK_TYPE_HBOX, &our_item_info);//注册此控件
 
 }
 
 return our_item_type;
 
}
 
//初始化实例结构
 
static void our_item_init(OurItem *ouritem)
 
{
 
 ouritem->label = gtk_label_new(NULL);
 
 gtk_box_pack_start(GTK_BOX(ouritem),ouritem->label,FALSE,FALSE,2);
 
 ouritem->entry = gtk_entry_new();
 
 g_signal_connect(G_OBJECT(ouritem->entry),"key_release_event",
 
   G_CALLBACK(on_key_release),ouritem);
 
 gtk_box_pack_start(GTK_BOX(ouritem),ouritem->entry,TRUE,TRUE,2);
 
}
 
//初始化类结构
 
static void our_item_class_init(OurItemClass *ouritemclass)
 
{
 
 GtkObjectClass *object_class;
 
 object_class = (GtkObjectClass*)ouritemclass;
 
 //下面函数创建一个新的信号
 
 ouritem_signals[OURITEM_OK_SIGNAL] = g_signal_new("ouritem_ok",
 
     G_TYPE_FROM_CLASS(object_class),
 
     G_SIGNAL_RUN_FIRST,
 
     G_STRUCT_OFFSET(OurItemClass, enter_ok),
 
     NULL,NULL,
 
     g_cclosure_marshal_VOID__VOID,
 
     G_TYPE_NONE, 0, NULL);
 
 ouritemclass->enter_ok = enter_ok;//此函数在下面定义
 
}
 
//创建新的自定义控件
 
GtkWidget* our_item_new(void)
 
{
 
 return GTK_WIDGET(g_object_new(TYPE_OUR_ITEM,0));
 
}
 
//设定自定义控件前面的静态文本
 
void our_item_set_label(GtkWidget* item, gchar* label)
 
{
 
 gtk_label_set_text(GTK_LABEL(OUR_ITEM(item)->label),label);
 
}
 
//带参数创建自定义控件
 
GtkWidget* our_item_new_with_label(gchar* label)
 
{
 
 GtkWidget* item;
 
 item = our_item_new();
 
 our_item_set_label(item,label);
 
 return item;
 
}
 
//此函数只是简单的在终端上提示你已经按了一次回车键
 
static void enter_ok(void)
 
{
 
 g_print("OK! Enter key was clicked! /n");
 
}
 
//以下函数捕获键盘输入消息
 
void on_key_release(GtkWidget *entry, GdkEventKey *event, gpointer data)
 
{
 
 if(event->keyval == GDK_Return) //当按下回车键后发射自定义的信号
 
 {
 
 g_signal_emit(G_OBJECT(data),ouritem_signals[OURITEM_OK_SIGNAL],0);
 
 }
 
}
(5)编译与测试
 
可以编写一个小程序来测试一下这个自定义控件,测试的前提是先将实例结构和类结构的定义头文件ouritem.h包含到测试文件中来,先用 our_item_new_with_label来创建一个我们自定义的控件,然后为控件的ouritem_ok信号加一个回调函数 on_item_ok,函数的功能是向另一个单行录入控件中加入文本,最后将我们自定义的控件加入到窗口的纵向盒状容器中来。测试代码如下:
 
//main.c
 
#include <gtk/gtk.h>
 
#include "ouritem.h"
 
static GtkWidget *entry = NULL;
 
//以下函数为自定义控件ouritem的"ouritem_ok"信号的回调函数
 
void on_item_ok(GtkWidget *widget, gpointer data)
 
{
 
 gtk_entry_set_text(GTK_ENTRY(entry),"OK!项目一录入结束");
 
}
 
int main(int argc, char* argv[])
 
{
 
 GtkWidget *window, *vbox, *item1, *button;
 
 gtk_init(&argc, &argv);
 
 window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
 
 gtk_window_set_title(GTK_WINDOW(window),"自定义控件测试");
 
 g_signal_connect(G_OBJECT(window),"delete_event",
 
   G_CALLBACK(gtk_main_quit),NULL);
 
 gtk_container_set_border_width(GTK_CONTAINER(window),10);
 
 vbox = gtk_vbox_new(FALSE,0);
 
 gtk_container_add(GTK_CONTAINER(window),vbox);
 
 //创建自定义控件
 
 item1 = our_item_new_with_label("项目一:");
 
 //为自定义控件的"ouritem_ok"信号连接函数
 
 g_signal_connect(G_OBJECT(item1),"ouritem_ok",
 
   G_CALLBACK(on_item_ok),NULL);
 
 gtk_box_pack_start(GTK_BOX(vbox),item1,FALSE,FALSE,5);
 
 entry = gtk_entry_new();
 
 gtk_box_pack_start(GTK_BOX(vbox),entry,FALSE,FALSE,5);
 
 button = gtk_button_new_with_label("退出");
 
 gtk_box_pack_start(GTK_BOX(vbox),button,FALSE,FALSE,5);
 
 g_signal_connect(G_OBJECT(button),"clicked",
 
   G_CALLBACK(gtk_main_quit),NULL);
 
 gtk_widget_show_all(window);
 
 gtk_main();
 
 return FALSE;
 
}
 
由于编译此测试程序的同时还要编译自定义控件的代码,所以需要写一个Makefile文件,然后用make来处理,Makefile内容如下所示:
 
 CC = gcc
 
test:main.o ouritem.o
 
 $(CC) main.o ouritem.o -o test `pkg-config --libs gtk -2.0`
 
main.o:main.c ouritem.h
 
 $(CC) -c main.c -o main.o `pkg-config --cflags gtk -2.0`
 
ouritem.o:ouritem.c ouritem.h
 
 $(CC) -c ouritem.c -o ouritem.o `pkg-config --cflags gtk -2.0`
 
运行此程序后,在项目一后面输入内容,点击回车键,会在下面的单行录入控件中显示"OK!项目一录入结束",同时在终端上也会显示"OK! Enter key was clicked!",这表明信号定义成功,并已经连接到了相应的回调函数。
 
至此我们初步完成了简单的自定义控件的定义、创建和测试过程。有兴趣的读者可以自行扩展和修改此控件的功能。
 
GTK 的入门向导中有两个完整的自定义控件的例程,对初学者来说可能过于复杂,相信看这个例程后会加深读者对它们的理解。
 
要编写功能更强大的控件会涉及到X Window底层的很多知识,如将X事件封装为GTK 的信号,对画布进行的画图操作等,这还有待于喜欢GTK 的朋友们进一步去研究和发掘。

你可能感兴趣的:(制作GTK+控件)