介绍
在这一节,我们将学习如何让元件把自己显示在荧幕上,以及如何让元件与事件交互。期间,我们将做一个类比刻度盘元件,用户可以拖动刻度盘上的指针来设定值。
在荧幕上显示元件
在荧幕上显示需要几个相关步骤。在呼叫 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 則留言
Comments are closed.