製作GTK+控件

在做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 的朋友們進一步去研究和發掘。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章