GTK+ 2.0 Tutorial

<<< Previous

Writing Your Own Widgets

Next >>>


Создание виджета с нуля

Введение

В этом разделе большое внимание будет уделено самостоятельному отображению виджетов и взаимодействию с событиями. Для примера мы создадим аналог циферблата, чтобы пользователь при помощи перемещения стрелки мог устанавливать необходимое значение.

Отображение виджета на экране

Отображение виджета на экране проходит в несколько шагов. После создания виджета функцией WIDGETNAME_new(), необходимо задействовать ещё несколько функций:

Как вы наверное заметили последние две функции очень похожи - каждая отвечает за отрисовку виджета на экране. Большинство типов виджетов не беспокоят различия между двумя этими функциями. По умолчанию функция draw() в виджет классе просто генерирует событие для перерисовки площади.  Однако, некоторые типы виджетов могут сохранить работу, различая две функции. Например, если виджет имеет многоуровневые окна X, то экспозиционное событие идентифицируют окно и может перерисовать только затронутое окно, которое не доступно для вызова draw().

Контейнерные виджеты, даже если они не беспокоятся о различии сами, не могут просто использовать по умолчанию draw() функцию, потому что их дочерние виджеты могут зависеть от этих различий.  Однако, было бы расточительно дублировать код отрисовки между двумя функциями. Поэтому виджеты вызывают функцию WIDGETNAME_paint() которая выполняет основную работу по прорисовке виджета, которая в свою очередь вызывает функции draw() и expose().

В нашем примере, циферблат является не контейнерным виджетом и имеет единственное окно, поэтому мы можем по умолчанию использовать функцию draw() и только обеспечивать выполнение функции expose().

Истоки виджета циферблата

Большинство виджетов GTK берут своё начало с уже существующих виджетов. Хотя раздел называется "Создание виджета с нуля", на самом деле виджет циферблата создаётся на основе существующего виджета регулировок (Range widget). Это сделано потому, что наш виджет циферблата будет иметь интерфейс виджета масштабирования, который является специализированным потомком виджета регулировок (Range widget). Хотя код представленный ниже имеет законченную форму, не нужно думать, что это было написано ab initio способом. Кроме того не мешало бы просмотреть методы работы виджетов масштабирования.

Основы

Для начала заголовочный файл:

/* GTK - The GIMP Toolkit * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, write to the Free * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, 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 /* 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); }

gtk_dial_realize()

Теперь мы дошли до нового типа функций. Первая функция выполняет работу по созданию 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; } }

gtk_dial_expose()

Как упомянуто выше, прорисовка данного виджета выполнена обработчиком для событий экспозиции (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 строк кода. Однако, есть еще довольно много расширений, которые можно добавить этому виджету:


<<< Previous

Home

Next >>>

Creating a Composite widget

Up

Learning More