介紹
在這一節,我們將學習如何讓元件把自己顯示在螢幕上,以及如何讓元件與事件交互。期間,我們將做一個類比刻度盤元件,用戶可以拖動刻度盤上的指針來設定值。
在螢幕上顯示元件
在螢幕上顯示需要幾個相關步驟。在呼叫 WIDGETNAME_new() 創建元件之後,如下幾個函式需要用到:
- WIDGETNAME_realize() 如果元件有 X 視窗,該函式負責為元件創建 X 視窗。
- WIDGETNAME_map() 在用戶呼叫 gtk_widget_show() 之後會呼叫該函式。它負責確保元件繪製在螢幕上。對於容器類別,該函式必須呼叫每個子元件的 map() 函式。
- WIDGETNAME_draw() 當為元件或它的一個祖先呼叫 gtk_widget_draw() 時該函式被呼叫。它實際上是呼叫繪製函式在螢幕上繪製元件。對於容器元件,該函式必須為它的子元件呼叫 gtk_widget_draw()。
- WIDGETNAME_expose() 是元件的暴露事件處理函式。它呼叫繪製函式把暴露的部分繪製在螢幕上。對於容器元件,該函式必須為無視窗子元件產生暴露事件。(如果它們有自己的視窗,X 會產生必需的暴露事件。)
你可能注意到後面的兩個函式十分相似,都是負責在屏幕上繪製元件。實際上許多元件並不真正關心它們之間的不同。元件類別裡的預設 draw() 函式只是簡單的為重繪區域產生一個暴露事件。然而,一些元件通過區分這兩個函式可以減少操作。例如,如果一個元件有多個 X 視窗,因為暴露事件標識了暴露的視窗,它可以只重繪受影響的視窗,呼叫 draw() 是不可能這樣的。
容器元件,即使它們自身並不關心這個差別,也不能簡單的使用預設 draw() 函式,因為它的子元件可能需要注意這個差別。然而,在兩個函式裡重複繪製程式碼是一種浪費的。按慣例,元件有一個名為 WIDGETNAME_paint() 的函數做實際的繪製元件的工作,draw() 和 expose() 函式再呼叫它。
在我們的範例裡,因為表格刻度盤元件不是一個容器元件,並且只有一個視窗,我們採用最簡便的方法,用預設的 draw() 函式,並且僅僅實現一個 expose() 函式。
刻度盤元件的原形
正像陸上動物是從泥裡爬出的兩棲動物的變體,GTK 元件是其它的、以前寫的元件的變體。因此,雖然這個章節命名為「從草稿中產生元件」,但刻度元件實際上是從範圍元件的程式碼上開始的。以它為起點是因為如果我們的刻度元件能與比例元件有相同的介面會好一些,比例元件是範圍元件的繼承。所以,雖然原始碼在下面以完整的形式出現,它不能說是從頭寫出來的。如果你還不熟悉比例元件如何以應用程式作者的觀點來運作,最好先看一下前面的章節。
基本
我們的元件中的相當多的一部分看起來與井字遊戲元件十分相似。首先,我們有一個標頭檔:
/* GTK - GIMP工具包 * 版權 (C) 1995-1997 Peter Mattis, Spencer Kimball 和 Josh MacDonald 所有 * * 本程序是自由軟件。你可以在自由軟件基金發佈的 GNU GPL 的條款下重新分發 * 或修改它。GPL 可以使用版本 2 或(由你選擇)任何隨後的版本。 * * 本程序分發的目的是它可能對其他人有用,但不提供任何的擔保,包括隱含的 * 和適合特定用途的保證。請查閱GNU通用公共許可證獲得詳細的信息。 * * 你應該已經隨該軟件一起收到一份GNU通用公共許可。如果還沒有,請寫信給 * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ #ifndef __GTK_DIAL_H__ #define __GTK_DIAL_H__ #include <gdk/gdk.h> #include <gtk/gtkadjustment.h> #include <gtk/gtkwidget.h> #ifdef __cplusplus extern "C" { #endif /* __cplusplus */ #define GTK_DIAL(obj) GTK_CHECK_CAST (obj, gtk_dial_get_type (), GtkDial) #define GTK_DIAL_CLASS(klass) GTK_CHECK_CLASS_CAST (klass, gtk_dial_get_type (), GtkDialClass) #define GTK_IS_DIAL(obj) GTK_CHECK_TYPE (obj, gtk_dial_get_type ()) typedef struct _GtkDial GtkDial; typedef struct _GtkDialClass GtkDialClass; struct _GtkDial { GtkWidget widget; /* 更新方式 (GTK_UPDATE_[CONTINUOUS/DELAYED/DISCONTINUOUS]) */ guint policy : 2; /* 當前按下的按鈕,如果沒有該值是 0 */ guint8 button; /* 刻度盤指針的大小 */ gint radius; gint pointer_width; /* 更新計時器的ID , 如果沒有該值是 0 */ guint32 timer; /* 當前角度 */ gfloat angle; /* 將從調整物件中得到的舊值保存起來,這樣在改變時我們就會知道 */ gfloat old_value; gfloat old_lower; gfloat old_upper; /* 為這個刻度盤元件存儲資料的調整物件 */ GtkAdjustment *adjustment; }; struct _GtkDialClass { GtkWidgetClass parent_class; }; GtkWidget* gtk_dial_new (GtkAdjustment *adjustment); GtkType gtk_dial_get_type (void); GtkAdjustment* gtk_dial_get_adjustment (GtkDial *dial); void gtk_dial_set_update_policy (GtkDial *dial, GtkUpdateType policy); void gtk_dial_set_adjustment (GtkDial *dial, GtkAdjustment *adjustment); #ifdef __cplusplus } #endif /* __cplusplus */ #endif /* __GTK_DIAL_H__ */ |
因為相對於上一個元件,這個元件我們要做的工作更多,所以在資料結構裡有更多的欄位,但是其它地方一樣。
接下來,在包含了標頭檔和宣告了幾個常數之後,我們有幾個提供元件訊息的函式和初始化元件的函式:
#include <math.h> #include <stdio.h> #include <gtk/gtkmain.h> #include <gtk/gtksignal.h> #include "gtkdial.h" #define SCROLL_DELAY_LENGTH 300 #define DIAL_DEFAULT_SIZE 100 /* 宣告 */ [ 省略以節省空間 ] /* 局部資料 */ static GtkWidgetClass *parent_class = NULL; GtkType gtk_dial_get_type () { static GtkType dial_type = 0; if (!dial_type) { static const GtkTypeInfo dial_info = { "GtkDial", sizeof (GtkDial), sizeof (GtkDialClass), (GtkClassInitFunc) gtk_dial_class_init, (GtkObjectInitFunc) gtk_dial_init, /* reserved_1 */ NULL, /* reserved_1 */ NULL, (GtkClassInitFunc) NULL }; dial_type = gtk_type_unique (GTK_TYPE_WIDGET, &dial_info); } return dial_type; } static void gtk_dial_class_init (GtkDialClass *class) { GtkObjectClass *object_class; GtkWidgetClass *widget_class; object_class = (GtkObjectClass*) class; widget_class = (GtkWidgetClass*) class; parent_class = gtk_type_class (gtk_widget_get_type ()); object_class->destroy = gtk_dial_destroy; widget_class->realize = gtk_dial_realize; widget_class->expose_event = gtk_dial_expose; widget_class->size_request = gtk_dial_size_request; widget_class->size_allocate = gtk_dial_size_allocate; widget_class->button_press_event = gtk_dial_button_press; widget_class->button_release_event = gtk_dial_button_release; widget_class->motion_notify_event = gtk_dial_motion_notify; } static void gtk_dial_init (GtkDial *dial) { dial->button = 0; dial->policy = GTK_UPDATE_CONTINUOUS; dial->timer = 0; dial->radius = 0; dial->pointer_width = 0; dial->angle = 0.0; dial->old_value = 0.0; dial->old_lower = 0.0; dial->old_upper = 0.0; dial->adjustment = NULL; } GtkWidget* gtk_dial_new (GtkAdjustment *adjustment) { GtkDial *dial; dial = gtk_type_new (gtk_dial_get_type ()); if (!adjustment) adjustment = (GtkAdjustment*) gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0); gtk_dial_set_adjustment (dial, adjustment); return GTK_WIDGET (dial); } static void gtk_dial_destroy (GtkObject *object) { GtkDial *dial; g_return_if_fail (object != NULL); g_return_if_fail (GTK_IS_DIAL (object)); dial = GTK_DIAL (object); if (dial->adjustment) gtk_object_unref (GTK_OBJECT (dial->adjustment)); if (GTK_OBJECT_CLASS (parent_class)->destroy) (* GTK_OBJECT_CLASS (parent_class)->destroy) (object); } |
注意 init() 函式所做的工作比井字遊戲元件少,因為這個元件不是組合元件,new() 函式所做的工作多一些,因為現在它具有一個參數。還要注意,當我們存儲一個到調整物件的指標的時候,我們增加它的參照次數,(並在不再使用它的時候相對的減少它),這樣 GTK 就能明瞭在何時可以安全的銷毀這個物件。
還有幾個操作元件選項的函式:
GtkAdjustment* gtk_dial_get_adjustment (GtkDial *dial) { g_return_val_if_fail (dial != NULL, NULL); g_return_val_if_fail (GTK_IS_DIAL (dial), NULL); return dial->adjustment; } void gtk_dial_set_update_policy (GtkDial *dial, GtkUpdateType policy) { g_return_if_fail (dial != NULL); g_return_if_fail (GTK_IS_DIAL (dial)); dial->policy = policy; } void gtk_dial_set_adjustment (GtkDial *dial, GtkAdjustment *adjustment) { g_return_if_fail (dial != NULL); g_return_if_fail (GTK_IS_DIAL (dial)); if (dial->adjustment) { gtk_signal_disconnect_by_data (GTK_OBJECT (dial->adjustment), (gpointer) dial); gtk_object_unref (GTK_OBJECT (dial->adjustment)); } dial->adjustment = adjustment; gtk_object_ref (GTK_OBJECT (dial->adjustment)); gtk_signal_connect (GTK_OBJECT (adjustment), "changed", (GtkSignalFunc) gtk_dial_adjustment_changed, (gpointer) dial); gtk_signal_connect (GTK_OBJECT (adjustment), "value_changed", (GtkSignalFunc) gtk_dial_adjustment_value_changed, (gpointer) dial); dial->old_value = adjustment->value; dial->old_lower = adjustment->lower; dial->old_upper = adjustment->upper; gtk_dial_update (dial); } |
gtk_dial_realize()
現在我們來看幾個新的函式。第一個是創建 X 視窗的函式。注意有一個遮罩傳遞給函式 gdk_window_new(),它指出 GdkWindowAttr 結構的哪些欄位實際上有資料在裡面(其餘的值會設為預設值)。同時也應該注意創建元件的事件遮罩的方法。我們呼叫 gtk_widget_get_events() 去獲取用戶為這個元件設置的事件遮罩 (用 gtk_widget_set_events() ),並把我們需要的事件加入其中。
創建視窗之後,我們設置它的風格和背景,並把指向元件的指標放入 GdkWindow 的用戶資料欄位。最後一步允許 GTK 分派這個視窗的事件到正確的元件。
static void gtk_dial_realize (GtkWidget *widget) { GtkDial *dial; GdkWindowAttr attributes; gint attributes_mask; g_return_if_fail (widget != NULL); g_return_if_fail (GTK_IS_DIAL (widget)); GTK_WIDGET_SET_FLAGS (widget, GTK_REALIZED); dial = GTK_DIAL (widget); attributes.x = widget->allocation.x; attributes.y = widget->allocation.y; attributes.width = widget->allocation.width; attributes.height = widget->allocation.height; attributes.wclass = GDK_INPUT_OUTPUT; attributes.window_type = GDK_WINDOW_CHILD; attributes.event_mask = gtk_widget_get_events (widget) | GDK_EXPOSURE_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK; attributes.visual = gtk_widget_get_visual (widget); attributes.colormap = gtk_widget_get_colormap (widget); attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL | GDK_WA_COLORMAP; widget->window = gdk_window_new (widget->parent->window, &attributes, attributes_mask); widget->style = gtk_style_attach (widget->style, widget->window); gdk_window_set_user_data (widget->window, widget); gtk_style_set_background (widget->style, widget->window, GTK_STATE_ACTIVE); } |
大小磋商
在包含元件的視窗第一次被顯示前和每當視窗佈局改變時,GTK 會詢問每個子元件所期望的大小。函式 gtk_dial_size_request() 處理這個請求。因為我們的元件不是一個容器元件,且在其上也沒有容器元件,我們僅僅傳回一個合理的預設值。
static void gtk_dial_size_request (GtkWidget *widget, GtkRequisition *requisition) { requisition->width = DIAL_DEFAULT_SIZE; requisition->height = DIAL_DEFAULT_SIZE; } |
在所有的元件已經請求了一個想要的大小之後,就開始計算視窗的佈局,且每個子元件被告知它們實際的大小。通常,它至少是請求的大小,但是,如果,比如用戶調整了視窗的大小,它偶爾可能小於請求的大小。函式 gtk_dial_size_allocate() 處理大小通知。注意在計算組件將要使用的大小的同時,這個常式也把元件的 X 視窗移到新位置和和設置新的大小。
static void gtk_dial_size_allocate (GtkWidget *widget, GtkAllocation *allocation) { GtkDial *dial; g_return_if_fail (widget != NULL); g_return_if_fail (GTK_IS_DIAL (widget)); g_return_if_fail (allocation != NULL); widget->allocation = *allocation; if (GTK_WIDGET_REALIZED (widget)) { dial = GTK_DIAL (widget); gdk_window_move_resize (widget->window, allocation->x, allocation->y, allocation->width, allocation->height); dial->radius = MAX(allocation->width,allocation->height) * 0.45; dial->pointer_width = dial->radius / 5; } } |
gtk_dial_expose()
像前面講的一樣,元件的所有的繪製在暴露事件處理函式裡做。這裡不需要多講,除了它用三維陰影法,按照存儲在元件的風格裡的顏色,用函式 gtk_draw_polygon 繪製表格的指標。
static gint gtk_dial_expose (GtkWidget *widget, GdkEventExpose *event) { GtkDial *dial; GdkPoint points[3]; gdouble s,c; gdouble theta; gint xc, yc; gint tick_length; gint i; g_return_val_if_fail (widget != NULL, FALSE); g_return_val_if_fail (GTK_IS_DIAL (widget), FALSE); g_return_val_if_fail (event != NULL, FALSE); if (event->count > 0) return FALSE; dial = GTK_DIAL (widget); gdk_window_clear_area (widget->window, 0, 0, widget->allocation.width, widget->allocation.height); xc = widget->allocation.width/2; yc = widget->allocation.height/2; /* 繪製刻度 */ for (i=0; i<25; i++) { theta = (i*M_PI/18. - M_PI/6.); s = sin(theta); c = cos(theta); tick_length = (i%6 == 0) ? dial->pointer_width : dial->pointer_width/2; gdk_draw_line (widget->window, widget->style->fg_gc[widget->state], xc + c*(dial->radius - tick_length), yc - s*(dial->radius - tick_length), xc + c*dial->radius, yc - s*dial->radius); } /* 繪製指針 */ s = sin(dial->angle); c = cos(dial->angle); points[0].x = xc + s*dial->pointer_width/2; points[0].y = yc + c*dial->pointer_width/2; points[1].x = xc + c*dial->radius; points[1].y = yc - s*dial->radius; points[2].x = xc - s*dial->pointer_width/2; points[2].y = yc - c*dial->pointer_width/2; gtk_draw_polygon (widget->style, widget->window, GTK_STATE_NORMAL, GTK_SHADOW_OUT, points, 3, TRUE); return FALSE; } |
事件處理
我們的元件還剩下處理各種類型的事件的程式碼,但我們會發現和許多其它 GTK 程式裡的沒多大區別。可以產生兩種類型的事件,一個是用戶可以點擊元件並拖動指針,另一個是通過外部的情況來改變調整物件的值。
當用戶點擊元件時,我們檢查看這個點擊是否是在刻度盤的指針裡,如果是這樣,把用戶所點擊的按鈕存入元件結構的 button 欄位,並且呼叫 gtk_grab_add() 強佔所有滑鼠事件。隨後的滑鼠移動引發控制值被重新計算(通過函式 gtk_dial_update_mouse)。按照已經設定的方式(policy),”value_changed” 事件被立即產生 (GTK_UPDATE_CONTINUOUS),在用gtk_timeout_add()添加的定時器裡定義的一段延遲後 (GTK_UPDATE_DELAYED),或只在按鈕被釋放時 (GTK_UPDATE_DISCONTINUOUS)產生。
static gint gtk_dial_button_press (GtkWidget *widget, GdkEventButton *event) { GtkDial *dial; gint dx, dy; double s, c; double d_parallel; double d_perpendicular; g_return_val_if_fail (widget != NULL, FALSE); g_return_val_if_fail (GTK_IS_DIAL (widget), FALSE); g_return_val_if_fail (event != NULL, FALSE); dial = GTK_DIAL (widget); /* 判斷按鈕是否是在表盤指針你按下 - 我們通過計算鼠標按下 點到表盤指針中線的水平和垂直距離來判斷。 */ dx = event->x - widget->allocation.width / 2; dy = widget->allocation.height / 2 - event->y; s = sin(dial->angle); c = cos(dial->angle); d_parallel = s*dy + c*dx; d_perpendicular = fabs(s*dx - c*dy); if (!dial->button && (d_perpendicular < dial->pointer_width/2) && (d_parallel > - dial->pointer_width)) { gtk_grab_add (widget); dial->button = event->button; gtk_dial_update_mouse (dial, event->x, event->y); } return FALSE; } static gint gtk_dial_button_release (GtkWidget *widget, GdkEventButton *event) { GtkDial *dial; g_return_val_if_fail (widget != NULL, FALSE); g_return_val_if_fail (GTK_IS_DIAL (widget), FALSE); g_return_val_if_fail (event != NULL, FALSE); dial = GTK_DIAL (widget); if (dial->button == event->button) { gtk_grab_remove (widget); dial->button = 0; if (dial->policy == GTK_UPDATE_DELAYED) gtk_timeout_remove (dial->timer); if ((dial->policy != GTK_UPDATE_CONTINUOUS) && (dial->old_value != dial->adjustment->value)) gtk_signal_emit_by_name (GTK_OBJECT (dial->adjustment), "value_changed"); } return FALSE; } static gint gtk_dial_motion_notify (GtkWidget *widget, GdkEventMotion *event) { GtkDial *dial; GdkModifierType mods; gint x, y, mask; g_return_val_if_fail (widget != NULL, FALSE); g_return_val_if_fail (GTK_IS_DIAL (widget), FALSE); g_return_val_if_fail (event != NULL, FALSE); dial = GTK_DIAL (widget); if (dial->button != 0) { x = event->x; y = event->y; if (event->is_hint || (event->window != widget->window)) gdk_window_get_pointer (widget->window, &x, &y, &mods); switch (dial->button) { case 1: mask = GDK_BUTTON1_MASK; break; case 2: mask = GDK_BUTTON2_MASK; break; case 3: mask = GDK_BUTTON3_MASK; break; default: mask = 0; break; } if (mods & mask) gtk_dial_update_mouse (dial, x,y); } return FALSE; } static gint gtk_dial_timer (GtkDial *dial) { g_return_val_if_fail (dial != NULL, FALSE); g_return_val_if_fail (GTK_IS_DIAL (dial), FALSE); if (dial->policy == GTK_UPDATE_DELAYED) gtk_signal_emit_by_name (GTK_OBJECT (dial->adjustment), "value_changed"); return FALSE; } static void gtk_dial_update_mouse (GtkDial *dial, gint x, gint y) { gint xc, yc; gfloat old_value; g_return_if_fail (dial != NULL); g_return_if_fail (GTK_IS_DIAL (dial)); xc = GTK_WIDGET(dial)->allocation.width / 2; yc = GTK_WIDGET(dial)->allocation.height / 2; old_value = dial->adjustment->value; dial->angle = atan2(yc-y, x-xc); if (dial->angle < -M_PI/2.) dial->angle += 2*M_PI; if (dial->angle < -M_PI/6) dial->angle = -M_PI/6; if (dial->angle > 7.*M_PI/6.) dial->angle = 7.*M_PI/6.; dial->adjustment->value = dial->adjustment->lower + (7.*M_PI/6 - dial->angle) * (dial->adjustment->upper - dial->adjustment->lower) / (4.*M_PI/3.); if (dial->adjustment->value != old_value) { if (dial->policy == GTK_UPDATE_CONTINUOUS) { gtk_signal_emit_by_name (GTK_OBJECT (dial->adjustment), "value_changed"); } else { gtk_widget_draw (GTK_WIDGET(dial), NULL); if (dial->policy == GTK_UPDATE_DELAYED) { if (dial->timer) gtk_timeout_remove (dial->timer); dial->timer = gtk_timeout_add (SCROLL_DELAY_LENGTH, (GtkFunction) gtk_dial_timer, (gpointer) dial); } } } } |
通過外部方式產生的對Adjustment的改變通過 “changed” 和 “value_changed” 信號傳到我們的元件。處理這些事情的處理函式將呼叫gtk_dial_update()來驗證參數,計算新的刻度盤指針角度,並重新繪製元件 (通過呼叫 gtk_widget_draw() 函式 )。
static void gtk_dial_update (GtkDial *dial) { gfloat new_value; g_return_if_fail (dial != NULL); g_return_if_fail (GTK_IS_DIAL (dial)); new_value = dial->adjustment->value; if (new_value < dial->adjustment->lower) new_value = dial->adjustment->lower; if (new_value > dial->adjustment->upper) new_value = dial->adjustment->upper; if (new_value != dial->adjustment->value) { dial->adjustment->value = new_value; gtk_signal_emit_by_name (GTK_OBJECT (dial->adjustment), "value_changed"); } dial->angle = 7.*M_PI/6. - (new_value - dial->adjustment->lower) * 4.*M_PI/3. / (dial->adjustment->upper - dial->adjustment->lower); gtk_widget_draw (GTK_WIDGET(dial), NULL); } static void gtk_dial_adjustment_changed (GtkAdjustment *adjustment, gpointer data) { GtkDial *dial; g_return_if_fail (adjustment != NULL); g_return_if_fail (data != NULL); dial = GTK_DIAL (data); if ((dial->old_value != adjustment->value) || (dial->old_lower != adjustment->lower) || (dial->old_upper != adjustment->upper)) { gtk_dial_update (dial); dial->old_value = adjustment->value; dial->old_lower = adjustment->lower; dial->old_upper = adjustment->upper; } } static void gtk_dial_adjustment_value_changed (GtkAdjustment *adjustment, gpointer data) { GtkDial *dial; g_return_if_fail (adjustment != NULL); g_return_if_fail (data != NULL); dial = GTK_DIAL (data); if (dial->old_value != adjustment->value) { gtk_dial_update (dial); dial->old_value = adjustment->value; } } |
可能的增強
迄今為止我們描繪的Dial元件有大約670行程式碼。不過我們真正完成的只有一點點,標頭檔和模板佔了其中的很大一部分。然而,對這個元件還有很多地方可以進行增強。
- 如果你試一下這個元件,你會發現當拖動pointer轉圈的時候有閃爍。這是因為每次刻度盤指針移動,整個元件在重繪前都要被擦除。最好的處理這個問題的方法 就是把這些變化繪製到一個不顯示在螢幕上的pixmap上,然後一步將最後結果直接複製到螢幕上。(進度顯示器元件就是以這種方式繪製它自身。)
- 用戶應該可以通過上下游標鍵來增加或減少這個值。
- 最好讓元件有一些按鈕來小步或大步增加或減少這個值。雖然有可能用你含的(embedded)按鈕來實現這個,但我們還是想讓這個按鈕在持續被按下的時候認為用戶按下了很多次,就像捲軸上的箭頭一樣。在範圍元件的程式碼中可以找到實現這種動作的大部分程式碼。
- 刻度盤元件可以做成一個容器元件,在以上所述的按鈕中間克度盤元件的底部放置一個簡單的子元件。用戶可以自己選擇加入一個標籤或文字輸入元件來顯示刻度盤的當前值。
2 則留言