Although the GTK distribution comes with many types of widgets that should cover most basic needs, there may come a time when you need to create your own new widget type. Since GTK uses widget inheritance extensively, and there is already a widget that is close to what you want, it is often possible to make a useful new widget type in just a few lines of code. But before starting work on a new widget, check around first to make sure that someone has not already written it. This will prevent duplication of effort and keep the number of GTK widgets out there to a minimum, which will help keep both the code and the interface of different applications consistent. As a flip side to this, once you finish your widget, announce it to the world so other people can benefit.
In order to create a new widget, it is important to have an understanding of how GTK objects work. This section is just meant as a brief overview.
GTK widgets are implemented in an object oriented fashion. However, they are implemented in standard C. This greatly improves portability and stability over using current generation C++ compilers'; however, it does mean that the widget writer has to pay attention to some of the implementation details. The information common to all instances of one class of widgets (e.g., to all Button widgets) is stored in the class structure. There is only one copy of this in which is stored information about the class's signals (which act like virtual functions in C). To support inheritance, the first field in the class structure must be a copy of the parent's class structure.
Of course, since we're using the Pascal instead of using C structures we declare Pascal types in Gtk units. The declaration of the class structure of GtkButtton looks like:
type PGtkButtonClass = ^TGtkButtonClass; TGtkButtonClass = record parent_class : TGtkContainerClass; pressed : procedure (button : pGtkButton); cdecl; released : procedure (button : pGtkButton); cdecl; clicked : procedure (button : pGtkButton); cdecl; enter : procedure (button : pGtkButton); cdecl; leave : procedure (button : pGtkButton); cdecl; end;
When a button is treated as a container (for instance, when it is resized), its class structure can be cast to GtkContainerClass, and the relevant fields used to handle the signals.
There is also a structure for each widget that is created on a per-instance basis. This structure has fields to store information that is different for each instance of the widget. We'll call this structure the object structure. For the Button class, it looks like:
type PGtkButton = ^TGtkButton; TGtkButton = record container : TGtkContainer; child : PGtkWidget; flag0 : {$ifdef win32} longint {$else} word {$endif}; end; const bm_in_button = 1; bp_in_button = 0; bm_button_down = 2; bp_button_down = 1;
Note that, similar to the class structure, the first field is the object record of the parent class, so that this record can be cast to the parent class' object type as needed.
One type of widget that you may be interested in creating is a widget that is merely an aggregate of other GTK widgets. This type of widget does nothing that couldn't be done without creating new widgets, but provides a convenient way of packaging user interface elements for reuse. The FileSelection and ColorSelection widgets in the standard distribution are examples of this type of widget.
The example widget that we'll create in this section is the Tictactoe widget, a 3x3 array of toggle buttons which triggers a signal when all three buttons in a row, column, or on one of the diagonals are depressed.
Note: the full source code for the Tictactoe example described below is in the Code Examples Appendix
The parent class for a composite widget is typically the container class that holds all of the elements of the composite widget. For example, the parent class of the FileSelection widget is the Dialog class. Since our buttons will be arranged in a table, it is natural to make our parent class the Table class.
Each GObject class has a unit file which contains both the
interface
and the implementation
part.
The interface
part declares the object and class
record
types for that object, along with public functions
and procedures.
interface uses glib2, gdk2, gtk2; type PTictactoe = ^TTictactoe; TTictactoe = record table : TGtkTable; buttons : array [0..2 , 0..2] of PGtkWidget; end; PTictactoeClass = ^TTictactoeClass; TTictactoeClass = record parent_class : TGtkTableClass; tictactoe : procedure (ttt : PTictactoe); cdecl; end; function tictactoe_get_type () : guint; function tictactoe_new () : pGtkWidget; procedure tictactoe_clear (ttt : pTictactoe);
Along with the functions and structures, we declare five standard macros in our header file, TICTACTOE_TYPE, TICTACTOE(obj), TICTACTOE_CLASS(klass), IS_TICTACTOE(obj), and IS_TICTACTOE_CLASS(klass), which cast a pointer into a pointer to the object or class structure, and check if an object is a Tictactoe widget respectively.
_get_type()
functionWe now continue on to the implementation of our widget. A core function
for every widget is the function WIDGETNAME_get_type()
. This
function, when first called, tells Glib about the widget class, and gets an
ID that uniquely identifies the widget class. Upon subsequent calls, it just
returns the ID.
var ttt_type : GType = 0; function tictactoe_get_type () : GType; const ttt_info: TGTypeInfo = (class_size : sizeof (TTictactoeClass); base_init : nil; base_finalize : nil; class_init : TGClassInitFunc(@tictactoe_class_init); class_finalize : nil; class_data : nil; instance_size : sizeof (TTictactoe); n_preallocs: 0; instance_init : TGInstanceInitFunc(@tictactoe_init);); begin if ttt_type = 0 then ttt_type := g_type_register_static (GTK_TYPE_TABLE, 'Tictactoe', @ttt_info, 0); tictactoe_get_type := ttt_type; end;
The GTypeInfo record has the following definition:
type PGTypeInfo = ^TGTypeInfo; TGTypeInfo = record { interface types, classed types, instantiated types } class_size : guint16; base_init : TGBaseInitFunc; base_finalize : TGBaseFinalizeFunc; { classed types, instantiated types } class_init : TGClassInitFunc; class_finalize : TGClassFinalizeFunc; class_data : gconstpointer; { instantiated types } instance_size : guint16; n_preallocs : guint16; instance_init : TGInstanceInitFunc; { value handling } value_table : PGTypeValueTable; end;
The important fields of this record are pretty self-explanatory. We'll
ignore the base_init
and base_finalize
as well as
the value_table
fields here. Once Glib has a correctly filled in
copy of this record, it knows how to create objects of a particular type.
_class_init()
procedureThe WIDGETNAME_class_init()
procedure initializes the fields
of the widget's class record, and sets up any signals for the class.
For our Tictactoe widget it looks like:
type TTT_Signals = (TICTACTOE_SIGNAL); const tictactoe_signals: array [TTT_Signals] of guint = (0); procedure tictactoe_class_init (klass : PTictactoeClass); begin tictactoe_signals[TICTACTOE_SIGNAL] := gtk_signal_new('tictactoe', G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_FIRST or G_SIGNAL_ACTION, @klass^.tictactoe - pointer(klass), nil, nil, @g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); end;
Our widget has just one signal, the tictactoe
signal that is
invoked when a row, column, or diagonal is completely filled in. Not every
composite widget needs signals, so if you are reading this for the first time,
you may want to skip to the next section now, as things are going to get
a bit complicated.
The function:
function g_signal_new (signal_name : pgchar; itype : GType; signal_flags : GSignalFlags; class_offset : guint; accumulator : PGSignalAccumulator; accu_data : gpointer; c_marshaller : PGSignalCMarshaller; return_type : GType; n_params : guint; args : array of const) : guint; cdecl;
creates a new signal. The parameters are:
signal_name
: The name of the signal.itype
: The ID of the object that this signal applies to. (It
will also apply to that objects descendants.)signal_flags
: Whether the default handler runs before or
after user handlers and other flags. Usually this will be one of
G_SIGNAL_RUN_FIRST
or G_SIGNAL_RUN_LAST
,
although there are other possibilities. The flag
G_SIGNAL_ACTION
specifies that no extra code needs to run
that performs special pre or post emission adjustments. This means that
the signal can also be emitted from object external code.class_offset
: The offset within the class record of a pointer
to the default handler.accumulator
: For most classes this can be set to nil.accu_data
: User data that will be handed to the accumulator
function.c_marshaller
: A function that is used to invoke the signal
handler. For signal handlers that have no arguments other than the object
that emitted the signal and user data, we use the pre-supplied marshaller
procedure g_cclosure_marshal_VOID__VOID
.return_type
: The type of the return value.n_params
: The number of parameters of the signal handler
(other than the two default ones mentioned above)args
: The types of the parameters.When specifying types, the following standard types can be used:
G_TYPE_INVALID G_TYPE_NONE G_TYPE_INTERFACE G_TYPE_CHAR G_TYPE_UCHAR G_TYPE_BOOLEAN G_TYPE_INT G_TYPE_UINT G_TYPE_LONG G_TYPE_ULONG G_TYPE_INT64 G_TYPE_UINT64 G_TYPE_ENUM G_TYPE_FLAGS G_TYPE_FLOAT G_TYPE_DOUBLE G_TYPE_STRING G_TYPE_POINTER G_TYPE_BOXED G_TYPE_PARAM G_TYPE_OBJECT
g_signal_new
() returns a unique integer identifier for
the signal, that we store in the tictactoe_signals
array, which
we index using an enumeration. (Conventionally, the enumeration elements
are the signal name, uppercased, but here there could be a conflict with
the TICTACTOE()
macro, so we called it
TICTACTOE_SIGNAL
instead.)
Each class also needs a procedure to initialize the object record. Usually, this procedure has the fairly limited role of setting the fields of the record to default values. For composite widgets, however, this procedure also creates the component widgets.
procedure tictactoe_init (ttt : PTictactoe); cdecl; var i, j : gint; begin gtk_table_resize(GTK_TABLE(ttt), 3, 3); gtk_table_set_homogeneous(GTK_TABLE(ttt), true); for i := 0 to 2 do for j := 0 to 2 do begin ttt^.buttons[i][j] := gtk_toggle_button_new(); gtk_table_attach_defaults(GTK_TABLE(ttt), ttt^.buttons[i][j], i, i + 1, j, j + 1); g_signal_connect(G_OBJECT(ttt^.buttons[i][j]), 'toggled', G_CALLBACK(@tictactoe_toggle), ttt); gtk_widget_set_size_request(ttt^.buttons[i][j], 20, 20); gtk_widget_show(ttt^.buttons[i][j]); end; end;
There is one more function that every object (except for abstract classes
like Bin that cannot be instantiated) needs to have - the function that
the user calls to create an object of that type. This is conventionally called
OBJECTNAME_new()
. In some widgets, though not for the Tictactoe
widgets, this function takes arguments, and does some setup based on
the arguments. The other two procedures are specific to the Tictactoe
widget.
tictactoe_clear()
is a public procedure that resets all
the buttons in the widget to the up position. Note the use of
g_signal_handlers_block_matched
() to keep our signal handler
for button toggles from being triggered unnecessarily.
tictactoe_toggle()
is the signal handler that is invoked when
the user clicks on a button. It checks to see if there are any winning
combinations that involve the toggled button, and if so, emits
the "tictactoe" signal.
function tictactoe_new () : PGtkWidget; begin tictactoe_new:= GTK_WIDGET(g_object_new(tictactoe_get_type(), nil)); end; procedure tictactoe_clear (ttt : PTictactoe); var i, j : integer; begin for i := 0 to 2 do for j := 0 to 2 do begin g_signal_handlers_block_matched(G_OBJECT(ttt^.buttons[i][j]), G_SIGNAL_MATCH_DATA, 0, 0, nil, nil, ttt); gtk_toggle_button_set_active(PGtkToggleButton(ttt^.buttons[i][j]), false); g_signal_handlers_unblock_matched(G_OBJECT(ttt^.buttons[i][j]), G_SIGNAL_MATCH_DATA, 0, 0, nil, nil, ttt); end; end; procedure tictactoe_toggle (widget : pGtkWidget ; ttt: pTictactoe); cdecl; const rwins: array[0..7, 0..2] of integer = ((0, 0, 0), (1, 1, 1), (2, 2, 2), (0, 1, 2), (0, 1, 2), (0, 1, 2), (0, 1, 2), (0, 1, 2)); cwins:array [0..7, 0..2] of integer = ((0, 1, 2), (0, 1, 2), (0, 1, 2), (0, 0, 0), (1, 1, 1), (2, 2, 2), (0, 1, 2), (2, 1, 0)); var i, k : integer; success, found : boolean; begin for k := 0 to 7 do begin success := true; found := false; for i := 0 to 2 do begin success := success and gtk_toggle_button_get_active(pGTKTOGGLEBUTTON(ttt^.buttons[rwins[k, i], cwins[k, i]])); found := found or (ttt^.buttons[rwins[k, i], cwins[k, i]] = widget); end; if success and found then begin g_signal_emit(ttt, tictactoe_signals[TICTACTOE_SIGNAL], 0); break; end; end; end;
And finally, an example program using our Tictactoe widget:
program ttt_test; uses glib2, gdk2, gtk2, tictactoe; procedure win (widget : PGtkWidget; data : gpointer); cdecl; begin writeln('Yay!'); tictactoe_clear(pTicTacToe(widget)); end; var window, ttt : pGtkWidget; begin gtk_init(@argc, @argv); window := gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(window), 'Tictactoe'); g_signal_connect(G_OBJECT(window), 'destroy', G_CALLBACK(@gtk_exit), nil); gtk_container_set_border_width(pGtkContainer(window), 10); ttt := tictactoe_new(); gtk_container_add(GTK_CONTAINER(window), ttt); gtk_widget_show(ttt); g_signal_connect(G_OBJECT(ttt), 'tictactoe', G_CALLBACK(@win), nil); gtk_widget_show(window); gtk_main(); end.
In this section, we'll learn more about how widgets display themselves on the screen and interact with events. As an example of this, we'll create an analog dial widget with a pointer that the user can drag to set the value.
There are several steps that are involved in displaying on the screen.
After the widget is created with a call to WIDGETNAME_new()
,
several more functions are needed:
WIDGETNAME_realize()
is responsible for creating an X
window for the widget if it has one.
WIDGETNAME_map()
is invoked after the user calls
gtk_widget_show()
. It is responsible for making sure the widget
is actually drawn on the screen (mapped). For a container class,
it must also make calls to map()
functions of any child
widgets.
WIDGETNAME_draw()
is invoked when
gtk_widget_draw()
is called for the widget or one of
its ancestors. It makes the actual
calls to the drawing functions to draw the widget on the screen. For
container widgets, this function must make calls to
gtk_widget_draw()
for its child widgets.
WIDGETNAME_expose()
is a handler for expose events for the
widget. It makes the necessary calls to the drawing functions to draw
the exposed portion on the screen. For container widgets, this
function must generate expose events for its child widgets which don't
have their own windows. (If they have their own windows, then X will
generate the necessary expose events.)
You might notice that the last two functions are quite similar - each
is responsible for drawing the widget on the screen. In fact many
types of widgets don't really care about the difference between the
two. The default draw()
function in the widget class simply
generates a synthetic expose event for the redrawn area. However, some
types of widgets can save work by distinguishing between the two functions.
For instance, if a widget has multiple X windows, then since expose events
identify the exposed window, it can redraw only the affected window,
which is not possible for calls to draw()
.
Container widgets, even if they don't care about the difference for
themselves, can't simply use the default draw()
function because
their child widgets might care about the difference. However,
it would be wasteful to duplicate the drawing code between the two
functions. The convention is that such widgets have a function called
WIDGETNAME_paint()
that does the actual work of drawing the
widget, that is then called by the draw()
and expose()
functions.
In our example approach, since the dial widget is not a container
widget, and only has a single window, we can take the simplest
approach and use the default draw()
function and only implement
an expose()
function.
Just as all land animals are just variants on the first amphibian that crawled up out of the mud, GTK widgets tend to start off as variants of some other, previously written widget. Thus, although this section is entitled "Creating a Widget from Scratch", the Dial widget really began with the source code for the Range widget. This was picked as a starting point because it would be nice if our Dial had the same interface as the Scale widgets which are just specialized descendants of the Range widget. So, though the source code is presented below in finished form, it should not be implied that it was written, ab initio in this fashion. Also, if you aren't yet familiar with how scale widgets work from the application writer's point of view, it would be a good idea to look them over before continuing.
Quite a bit of our widget should look pretty familiar from the Tictactoe widget. First, we have an interface:
{ GTK - The GIMP Toolkit Copyright for C version (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald Copyright for Pascal version 2014 Zbigniew Jurkiewicz 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. } interface uses glib2, gdk2, gtk2, math; type PGtkDial = ^TGtkDial; TGtkDial = record widget : TGtkWidget; { Update policy (GTK_UPDATE_[CONTINUOUS/DELAYED/DISCONTINUOUS]) } policy : guint; {: 2} { Button currently pressed or 0 if none } button :guint8; { Dimensions of dial components } radius : gint; pointer_width : gint; { ID of update timer, or 0 if none } timer : guint32; { Current angle } angle : gfloat; last_angle : gfloat; { Old values from adjustment stored so we know when something changes } old_value : gfloat; old_lower : gfloat; old_upper : gfloat; { The adjustment object that stores the data for this dial } adjustment : PGtkAdjustment; end; PGtkDialClass = ^TGtkDialClass; TGtkDialClass = record parent_class : TGtkWidgetClass; end; function gtk_dial_new (adjustment : PGtkAdjustment) : PGtkWidget; function gtk_dial_get_type () : GType; function gtk_dial_get_adjustment (dial : PGtkDial) : PGtkAdjustment; procedure gtk_dial_set_update_policy (dial : PGtkDial; policy : TGtkUpdateType); procedure gtk_dial_set_adjustment (dial : PGtkDial; adjustment : PGtkAdjustment); function GTK_DIAL (obj : pointer) : PGtkDial; function GTK_DIAL_CLASS (klass : pointer ) : PGtkDialClass; function GTK_IS_DIAL (obj : pointer) : boolean;
Since there is quite a bit more going on in this widget than the last one, we have more fields in the data structure, but otherwise things are pretty similar.
Next, after implementing standard macros and declaring a few constants, we have some functions to provide information about the widget and initialize it:
implementation function GTK_DIAL (obj : pointer) : PGtkDial; begin GTK_DIAL := PGtkDial(G_TYPE_CHECK_INSTANCE_CAST(obj, gtk_dial_get_type())); end; function GTK_DIAL_CLASS (klass : pointer) : PGtkDialClass; begin GTK_DIAL_CLASS := PGtkDialClass(G_TYPE_CHECK_CLASS_CAST(klass, gtk_dial_get_type())); end; function GTK_IS_DIAL (obj : pointer) : boolean; begin GTK_IS_DIAL := G_TYPE_CHECK_INSTANCE_TYPE(obj, gtk_dial_get_type()); end; const SCROLL_DELAY_LENGTH = 300; DIAL_DEFAULT_SIZE = 100; { Forward declarations } [ omitted to save space ] { Local data } var parent_class : PGtkWidgetClass = nil; var dial_type : GType = 0; function gtk_dial_get_type () : GType; const dial_info: TGTypeInfo = (class_size: sizeof(TGtkDialClass); base_init: nil; base_finalize: nil; class_init: TGClassInitFunc(@gtk_dial_class_init); class_finalize: nil; class_data: nil; instance_size: sizeof(TGtkDial); n_preallocs: 0; instance_init: TGInstanceInitFunc(@gtk_dial_init);); begin if dial_type = 0 then dial_type := g_type_register_static(GTK_TYPE_WIDGET, 'GtkDial', @dial_info, 0); gtk_dial_get_type := dial_type; end; procedure gtk_dial_class_init (klass : PGtkDialClass); cdecl; var object_class : PGtkObjectClass; widget_class : PGtkWidgetClass; begin object_class := PGtkObjectClass(klass); widget_class := PGtkWidgetClass(klass); parent_class := g_type_class_peek_parent(klass); 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; end; procedure gtk_dial_init (dial : PGtkDial); cdecl; begin 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 := nil; end; function gtk_dial_new (adjustment : PGtkAdjustment) : PGtkWidget; var dial : PGtkDial; begin dial := g_object_new(gtk_dial_get_type(), nil); if adjustment = nil then adjustment := PGtkAdjustment(gtk_adjustment_new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)); gtk_dial_set_adjustment(dial, adjustment); gtk_dial_new := GTK_WIDGET(dial); end; procedure gtk_dial_destroy (_object : PGtkObject); cdecl; var dial : PGtkDial; begin if (_object = nil) or not GTK_IS_DIAL(_object) then Exit(); dial := GTK_DIAL(_object); if dial^.adjustment <> nil then begin g_object_unref(GTK_OBJECT(dial^.adjustment)); dial^.adjustment := nil; end; {GTK_OBJECT_CLASS(parent_class)^.destroy(object);} end;
Note that this init()
function does less than for
the Tictactoe widget, since this is not a composite widget, and the
new()
function does more, since it now has an argument.
Also, note that when we store a pointer to the Adjustment object,
we increment its reference count, (and correspondingly decrement it when
we no longer use it) so that GTK can keep track of when it can be safely
destroyed.
Also, there are a few function to manipulate the widget's options:
function gtk_dial_get_adjustment (dial : PGtkDial) : PGtkAdjustment; begin if (dial <> nil) and GTK_IS_DIAL(dial) then gtk_dial_get_adjustment := dial^.adjustment else gtk_dial_get_adjustment := nil; end; procedure gtk_dial_set_update_policy (dial : PGtkDial; policy : TGtkUpdateType); begin if (dial <> nil) and GTK_IS_DIAL(dial) then dial^.policy := policy; end; procedure gtk_dial_set_adjustment (dial : PGtkDial; adjustment : PGtkAdjustment); begin if (dial = nil) or not GTK_IS_DIAL(dial) then Exit(); if dial^.adjustment <> nil then begin g_signal_handlers_disconnect_by_func(GTK_OBJECT(dial^.adjustment), nil, gpointer(dial)); g_object_unref(GTK_OBJECT(dial^.adjustment)); end; dial^.adjustment := adjustment; g_object_ref(GTK_OBJECT(dial^.adjustment)); g_signal_connect(G_OBJECT(adjustment), 'changed', G_CALLBACK(@gtk_dial_adjustment_changed), gpointer(dial)); g_signal_connect(G_OBJECT(adjustment), 'value_changed', G_CALLBACK(@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); end;
Now we come to some new types of functions. First, we have a function
that does the work of creating the X window. Notice that a mask is
passed to the function gdk_window_new()
which specifies which
fields of the GdkWindowAttr structure actually have data in them (the remaining
fields will be given default values). Also worth noting is the way the
event mask of the widget is created. We call
gtk_widget_get_events()
to retrieve the event mask that the user
has specified for this widget (with gtk_widget_set_events()
), and
add the events that we are interested in ourselves.
After creating the window, we set its style and background, and put a pointer to the widget in the user data field of the GdkWindow. This last step allows GTK to dispatch events for this window to the correct widget.
procedure gtk_dial_realize (widget : PGtkWidget); cdecl; var dial : PGtkDial; attributes : TGdkWindowAttr; attributes_mask : gint; begin if (widget = nil) or not GTK_IS_DIAL(widget) then Exit(); {gtk_widget_set_realized(widget, true);} 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) or GDK_EXPOSURE_MASK or GDK_BUTTON_PRESS_MASK or GDK_BUTTON_RELEASE_MASK or GDK_POINTER_MOTION_MASK or GDK_POINTER_MOTION_HINT_MASK; attributes.visual := gtk_widget_get_visual(widget); attributes.colormap := gtk_widget_get_colormap(widget); attributes_mask := GDK_WA_X or GDK_WA_Y or GDK_WA_VISUAL or 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); end;
Before the first time that the window containing a widget is
displayed, and whenever the layout of the window changes, GTK asks
each child widget for its desired size. This request is handled by the
function gtk_dial_size_request()
. Since our widget isn't a
container widget, and has no real constraints on its size, we just
return a reasonable default value.
procedure gtk_dial_size_request (widget : PGtkWidget; requisition : PGtkRequisition); cdecl; begin requisition^.width := DIAL_DEFAULT_SIZE; requisition^.height := DIAL_DEFAULT_SIZE; end;
After all the widgets have requested an ideal size, the layout of the
window is computed and each child widget is notified of its actual
size. Usually, this will be at least as large as the requested size,
but if for instance the user has resized the window, it may
occasionally be smaller than the requested size. The size notification
is handled by the function gtk_dial_size_allocate()
. Notice that
as well as computing the sizes of some component pieces for future
use, this routine also does the grunt work of moving the widget's X
window into the new position and size.
procedure gtk_dial_size_allocate (widget : PGtkWidget; allocation : PGtkAllocation); cdecl; var dial : PGtkDial; begin if (widget = nil) or (not GTK_IS_DIAL(widget)) or (allocation = nil) then Exit(); widget^.allocation := allocation^; dial := GTK_DIAL(widget); if gtk_widget_realized(widget) then gdk_window_move_resize(widget^.window, allocation^.x, allocation^.y, allocation^.width, allocation^.height); dial^.radius := trunc(min(allocation^.width, allocation^.height) * 0.45); dial^.pointer_width := dial^.radius div 5; end;
As mentioned above, all the drawing of this widget is done in the
handler for expose events. There's not much to remark on here except
the use of the function gtk_draw_polygon
to draw the pointer with
three dimensional shading according to the colors stored in the
widget's style.
function gtk_dial_expose (widget : PGtkWidget; event : PGdkEventExpose) : gboolean; cdecl; var dial : PGtkDial; points : array [0..5] of TGdkPoint; s, c : gdouble; theta, last, increment : gdouble; blankstyle : PGtkStyle; xc, yc : gint; upper, lower : gint; tick_length : gint; i, inc : gint; begin if (widget = nil) or (not GTK_IS_DIAL(widget)) or (event = nil) then Exit(false); if event^.count > 0 then Exit(false); dial := GTK_DIAL(widget); xc := widget^.allocation.width div 2; yc := widget^.allocation.height div 2; upper := trunc(dial^.adjustment^.upper); lower := trunc(dial^.adjustment^.lower); { Erase old pointer } s := sin(dial^.last_angle); c := cos(dial^.last_angle); dial^.last_angle := dial^.angle; points[0].x := trunc(xc + s * dial^.pointer_width / 2); points[0].y := trunc(yc + c * dial^.pointer_width / 2); points[1].x := trunc(xc + c * dial^.radius); points[1].y := trunc(yc - s * dial^.radius); points[2].x := trunc(xc - s * dial^.pointer_width / 2); points[2].y := trunc(yc - c * dial^.pointer_width / 2); points[3].x := trunc(xc - c * dial^.radius / 10); points[3].y := trunc(yc + s * dial^.radius / 10); points[4].x := points[0].x; points[4].y := points[0].y; blankstyle := gtk_style_new(); blankstyle^.bg_gc[GTK_STATE_NORMAL] := widget^.style^.bg_gc[GTK_STATE_NORMAL]; blankstyle^.dark_gc[GTK_STATE_NORMAL] := widget^.style^.bg_gc[GTK_STATE_NORMAL]; blankstyle^.light_gc[GTK_STATE_NORMAL] := widget^.style^.bg_gc[GTK_STATE_NORMAL]; blankstyle^.black_gc := widget^.style^.bg_gc[GTK_STATE_NORMAL]; blankstyle^.depth := gdk_drawable_get_depth(GDK_DRAWABLE(widget^.window)); gtk_paint_polygon(blankstyle, widget^.window, GTK_STATE_NORMAL, GTK_SHADOW_OUT, nil, widget, nil, points, 5, false); g_object_unref(blankstyle); { Draw ticks } if (upper - lower) = 0 then Exit(false); increment := (100 * pi) / (dial^.radius * dial^.radius); inc := upper - lower; while inc < 100 do inc := 10 * inc; while inc >= 1000 do inc := inc div 10; last := -1; for i := 0 to inc do begin theta := (i * pi / (18 * inc / 24.0) - pi / 6.0); if (theta - last) < increment then continue; last := theta; s := sin(theta); c := cos(theta); if i mod (inc div 10) = 0 then tick_length := dial^.pointer_width else tick_length := dial^.pointer_width div 2; gdk_draw_line(widget^.window, widget^.style^.fg_gc[widget^.state], trunc(xc + c * (dial^.radius - tick_length)), trunc(yc - s * (dial^.radius - tick_length)), trunc(xc + c * dial^.radius), trunc(yc - s * dial^.radius)); end; { Draw pointer } s := sin(dial^.angle); c := cos(dial^.angle); dial^.last_angle := dial^.angle; points[0].x := xc + trunc(s * dial^.pointer_width / 2); points[0].y := yc + trunc(c * dial^.pointer_width / 2); points[1].x := xc + trunc(c * dial^.radius); points[1].y := yc - trunc(s * dial^.radius); points[2].x := xc - trunc(s * dial^.pointer_width / 2); points[2].y := yc - trunc(c * dial^.pointer_width / 2); points[3].x := xc - trunc(c * dial^.radius / 10); points[3].y := yc + trunc(s * dial^.radius / 10); points[4].x := points[0].x; points[4].y := points[0].y; gtk_paint_polygon(widget^.style, widget^.window, GTK_STATE_NORMAL, GTK_SHADOW_OUT, nil, widget, nil, points, 5, true); gtk_dial_expose := false; end;
The rest of the widget's code handles various types of events, and isn't too different from what would be found in many GTK applications. Two types of events can occur - either the user can click on the widget with the mouse and drag to move the pointer, or the value of the Adjustment object can change due to some external circumstance.
When the user clicks on the widget, we check to see if the click was
appropriately near the pointer, and if so, store the button that the
user clicked with in the button
field of the widget
structure, and grab all mouse events with a call to
gtk_grab_add()
. Subsequent motion of the mouse causes the
value of the control to be recomputed (by the function
gtk_dial_update_mouse
). Depending on the policy that has been
set, "value_changed" events are either generated instantly
(GTK_UPDATE_CONTINUOUS
), after a delay in a timer added with
g_timeout_add()
(GTK_UPDATE_DELAYED
), or only when
the button is released (GTK_UPDATE_DISCONTINUOUS
).
function gtk_dial_button_press (widget : PGtkWidget; event : PGdkEventButton) : gboolean; cdecl; var dial : PGtkDial; dx, dy : gint; s, c : double; d_parallel : double; d_perpendicular : double; begin if (widget = nil) or (not GTK_IS_DIAL(widget)) or (event = nil) then Exit(false); dial := GTK_DIAL(widget); { Determine if button press was within pointer region - we do this by computing the parallel and perpendicular distance of the point where the mouse was pressed from the line passing through the pointer } dx := trunc(event^.x - widget^.allocation.width / 2); dy := trunc(widget^.allocation.height div 2 - event^.y); s := sin(dial^.angle); c := cos(dial^.angle); d_parallel := s * dy + c * dx; d_perpendicular := abs(s * dx - c * dy); if (dial^.button = 0) and (d_perpendicular < dial^.pointer_width / 2) and (d_parallel > - dial^.pointer_width) then begin gtk_grab_add(widget); dial^.button := event^.button; gtk_dial_update_mouse(dial, trunc(event^.x), trunc(event^.y)); end; gtk_dial_button_press := false; end; function gtk_dial_button_release (widget : PGtkWidget; event : PGdkEventButton): gboolean; cdecl; var dial : PGtkDial; begin if (widget = nil) or (not GTK_IS_DIAL(widget)) or (event = nil) then Exit(false); dial := GTK_DIAL(widget); if dial^.button = event^.button then begin gtk_grab_remove(widget); dial^.button := 0; if dial^.policy = GTK_UPDATE_DELAYED then g_source_remove(dial^.timer); if (dial^.policy <> GTK_UPDATE_CONTINUOUS) and (dial^.old_value <> dial^.adjustment^.value) then g_signal_emit_by_name(GTK_OBJECT(dial^.adjustment), 'value_changed'); end; gtk_dial_button_release := false; end; function gtk_dial_motion_notify (widget : PGtkWidget; event : PGdkEventMotion) : gboolean; cdecl; var dial : PGtkDial; mods : TGdkModifierType; x, y, mask : gint; begin if (widget = nil) or (not GTK_IS_DIAL(widget)) or (event = nil) then Exit(false); dial := GTK_DIAL(widget); if dial^.button <> 0 then begin x := trunc(event^.x); y := trunc(event^.y); if (event^.is_hint = 1) or (event^.window <> widget^.window) then gdk_window_get_pointer(widget^.window, @x, @y, @mods); case dial^.button of 1: mask := GDK_BUTTON1_MASK; 2: mask := GDK_BUTTON2_MASK; 3: mask := GDK_BUTTON3_MASK; else mask := 0; end; if (mods and mask) <> 0 then gtk_dial_update_mouse(dial, x, y); end; gtk_dial_motion_notify := false; end; function gtk_dial_timer (data : gpointer) : gboolean; cdecl; var dial : PGtkDial; begin if (data = nil) or not GTK_IS_DIAL(data) then Exit(false); dial := GTK_DIAL(data); if dial^.policy = GTK_UPDATE_DELAYED then g_signal_emit_by_name(GTK_OBJECT(dial^.adjustment), 'value_changed'); gtk_dial_timer := false; end; procedure gtk_dial_update_mouse (dial : PGtkDial; x : gint; y : gint); var xc, yc : gint; old_value : gfloat; begin if (dial = nil) or not GTK_IS_DIAL(dial) then Exit(); xc := GTK_WIDGET(dial)^.allocation.width div 2; yc := GTK_WIDGET(dial)^.allocation.height div 2; old_value := dial^.adjustment^.value; dial^.angle := arctan2(yc - y, x - xc); if dial^.angle < -pi / 2.0 then dial^.angle := dial^.angle + 2 * pi; if dial^.angle < -pi / 6 then dial^.angle := -pi / 6; if dial^.angle > 7.0 * pi / 6.0 then dial^.angle := 7.0 * pi / 6.0; dial^.adjustment^.value := dial^.adjustment^.lower + (7.0 * pi / 6 - dial^.angle) * (dial^.adjustment^.upper - dial^.adjustment^.lower) / (4.0 * pi / 3.0); if dial^.adjustment^.value <> old_value then begin if dial^.policy = GTK_UPDATE_CONTINUOUS then g_signal_emit_by_name(GTK_OBJECT (dial^.adjustment), 'value_changed') else begin gtk_widget_queue_draw(GTK_WIDGET(dial)); if dial^.policy = GTK_UPDATE_DELAYED then begin if dial^.timer <> 0 then g_source_remove(dial^.timer); dial^.timer := g_timeout_add(SCROLL_DELAY_LENGTH, @gtk_dial_timer, gpointer(dial)); end; end; end; end;
Changes to the Adjustment by external means are communicated to our
widget by the "changed" and "value_changed" signals. The handlers
for these functions call gtk_dial_update()
to validate the
arguments, compute the new pointer angle, and redraw the widget (by
calling gtk_widget_draw()
).
procedure gtk_dial_update (dial : PGtkDial); var new_value : gfloat ; begin if (dial = nil) or not GTK_IS_DIAL(dial) then Exit(); new_value := dial^.adjustment^.value; if new_value < dial^.adjustment^.lower then new_value := dial^.adjustment^.lower; if new_value > dial^.adjustment^.upper then new_value := dial^.adjustment^.upper; if new_value <> dial^.adjustment^.value then begin dial^.adjustment^.value := new_value; g_signal_emit_by_name(GTK_OBJECT(dial^.adjustment), 'value_changed'); end; dial^.angle := 7.0 * pi / 6.0 - (new_value - dial^.adjustment^.lower) * 4.0 * pi / 3.0 / (dial^.adjustment^.upper - dial^.adjustment^.lower); gtk_widget_queue_draw(GTK_WIDGET(dial)); end; procedure gtk_dial_adjustment_changed (adjustment : PGtkAdjustment; data : gpointer); cdecl; var dial : PGtkDial; begin if (adjustment = nil) or (data = nil) then Exit(); dial := GTK_DIAL(data); if (dial^.old_value <> adjustment^.value) or (dial^.old_lower <> adjustment^.lower) or (dial^.old_upper <> adjustment^.upper) then begin gtk_dial_update(dial); dial^.old_value := adjustment^.value; dial^.old_lower := adjustment^.lower; dial^.old_upper := adjustment^.upper; end; end; procedure gtk_dial_adjustment_value_changed (adjustment : PGtkAdjustment; data : gpointer); cdecl; var dial : PGtkDial; begin if (adjustment = nil) or (data = nil) then Exit(); dial := GTK_DIAL(data); if dial^.old_value <> adjustment^.value then begin gtk_dial_update(dial); dial^.old_value := adjustment^.value; end; end;
The Dial widget as we've described it so far runs about 670 lines of code. Although that might sound like a fair bit, we've really accomplished quite a bit with that much code, especially since much of that length is headers and boilerplate. However, there are quite a few more enhancements that could be made to this widget:
If you try this widget out, you'll find that there is some flashing as the pointer is dragged around. This is because the entire widget is erased every time the pointer is moved before being redrawn. Often, the best way to handle this problem is to draw to an offscreen pixmap, then copy the final results onto the screen in one step. (The ProgressBar widget draws itself in this fashion.)
The user should be able to use the up and down arrow keys to increase and decrease the value.
It would be nice if the widget had buttons to increase and decrease the value in small or large steps. Although it would be possible to use embedded Button widgets for this, we would also like the buttons to auto-repeat when held down, as the arrows on a scrollbar do. Most of the code to implement this type of behavior can be found in the Range widget.
The Dial widget could be made into a container widget with a single child widget positioned at the bottom between the buttons mentioned above. The user could then add their choice of a label or entry widget to display the current value of the dial.
Only a small part of the many details involved in creating widgets could be described above. If you want to write your own widgets, the best source of examples is the GTK source itself. Ask yourself some questions about the widget you want to write: IS it a Container widget? Does it have its own window? Is it a modification of an existing widget? Then find a similar widget, and start making changes. Good luck!