GTK+ 2.0 Tutorial |
||
---|---|---|
Writing Your Own Widgets |
В этом разделе большое внимание будет уделено самостоятельному отображению виджетов и взаимодействию с событиями. Для примера мы создадим аналог циферблата, чтобы пользователь при помощи перемещения стрелки мог устанавливать необходимое значение.
Отображение виджета на экране проходит в несколько шагов. После создания виджета функцией WIDGETNAME_new(), необходимо задействовать ещё несколько функций:
WIDGETNAME_realize() отвечает за создание X window для виджета, если это необходимо.
WIDGETNAME_map() вызывается после того, как пользователь вызвал gtk_widget_show(). Чтобы удостоверится в том, что виджет отобразился на экране (mapped). Для контейнерного класса, необходим вызов функции map()> для любых дочерних виджетов.
WIDGETNAME_draw() вызывается, когда вызвана функция gtk_widget_draw()для виджета или одного из предков. Это фактически вызов для отрисовки виджета на экране. Для контейнерных виджетов эта функция должна вызвать gtk_widget_draw() для всех дочерних вызовов.
WIDGETNAME_expose() является обработчиком событий виджета. Производит необходимые вызовы функций прорисовки для доступной части экрана. Для контейнерных виджетов, эта функция генерирует отображающие события для их дочерних виджетов неимеющих собственных окон. (Если они имеют собственные окна, то необходимые события генерирует X.)
Как вы наверное заметили последние две функции очень похожи - каждая отвечает за отрисовку виджета на экране. Большинство типов виджетов не беспокоят различия между двумя этими функциями. По умолчанию функция draw() в виджет классе просто генерирует событие для перерисовки площади. Однако, некоторые типы виджетов могут сохранить работу, различая две функции. Например, если виджет имеет многоуровневые окна X, то экспозиционное событие идентифицируют окно и может перерисовать только затронутое окно, которое не доступно для вызова draw().
Контейнерные виджеты, даже если они не беспокоятся о различии сами, не могут просто использовать по умолчанию draw() функцию, потому что их дочерние виджеты могут зависеть от этих различий. Однако, было бы расточительно дублировать код отрисовки между двумя функциями. Поэтому виджеты вызывают функцию WIDGETNAME_paint() которая выполняет основную работу по прорисовке виджета, которая в свою очередь вызывает функции draw() и expose().
В нашем примере, циферблат является не контейнерным виджетом и имеет единственное окно, поэтому мы можем по умолчанию использовать функцию draw() и только обеспечивать выполнение функции expose().
Большинство виджетов GTK берут своё начало с уже существующих виджетов. Хотя раздел называется "Создание виджета с нуля", на самом деле виджет циферблата создаётся на основе существующего виджета регулировок (Range widget). Это сделано потому, что наш виджет циферблата будет иметь интерфейс виджета масштабирования, который является специализированным потомком виджета регулировок (Range widget). Хотя код представленный ниже имеет законченную форму, не нужно думать, что это было написано ab initio способом. Кроме того не мешало бы просмотреть методы работы виджетов масштабирования.
Затем, после включения заголовочного файла, мы получаем некоторые функции для обеспечения информации о виджете и его инициализации:
#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
/* Forward declarations */
[ omitted to save space ]
/* Локальные данные */
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() делает меньше чем в виджете Tictactoe, так как это не сложный виджет, а функция new()делает больше, так как имеет аргумент. Кроме того, заметьте что указатель на объект регулирования мы увеличиваем подсчетом ссылок, (и соответственно уменьшаем когда больше не используем).
Кроме того, есть несколько функций, чтобы управлять опциями виджета:
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);
}
|
Теперь мы дошли до нового типа функций. Первая функция выполняет работу по созданию X window (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;
}
}
|
Как упомянуто выше, прорисовка данного виджета выполнена обработчиком для событий экспозиции (expose events). Здесь изменений не много, только функция 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. Два типа событий могут произойти - или пользователь может нажать на виджет мышкой и переместиться, чтобы переместить стрелку, или значение объекта настройки (Adjustment object) может измениться из-за некоторого внешнего обстоятельства.
Если пользователь нажал на виджет, мы выясняем был ли щелчок возле стрелки, ели это так, то заносим кнопку которую нажал пользователь в поле "кнопка" структуры виджета и захватываем все события связанные с мышкой с помощью вызова gtk_grab_add(). Последующее движение мыши заставляет повторно вычислить значение управления (функцией gtk_dial_update_mouse). В зависимости от политики, которая была установлена, событие "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 widget) который мы описали имеет 670 строк кода. Однако, есть еще довольно много расширений, которые можно добавить этому виджету:
Если вы попробуете использовать этот виджет, вы заметите некоторое мерцание при вращении указателя. Это происходит потому, что каждый раз при изменении указателя перерисовывается весь виджет. Лучшим решением этой проблемы будет закадровая отрисовка pixmap, затем копирование финального результата на экран за один шаг. (Виджет ProgressBar отрисовывается таким образом.)
Пользователь должен иметь возможность использовать клавиши up и down для изменения значений указателя.
Было бы хорошо иметь кнопки регулирующие размер шага увеличения или уменьшения значения, при удержании которых происходит автоматическое изменение, также как это сделано в виджете полосы прокрутки (scrollbar). Большая часть кода, для осуществления этого типа поведения может быть найдена в виджете регулирования (Range widget).
Виджет циферблата мог быть превращен в контейнерный виджет с единственным дочерним виджетом, позиционированным в основание между упомянутыми выше кнопками.
Creating a Composite widget |
Learning More |