[Previous] [Contents] [Next]

23. Writing Your Own Widgets

23.1 Overview

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.


23.2 The Anatomy Of A Widget

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.

23.3 Creating a Composite Widget

Introduction

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

Choosing a Parent Class

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.

The Unit's Interface

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.

The _get_type() function

We 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.

The _class_init() procedure

The 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:

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.)

The _init() procedure

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;

And the rest...

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.

23.4 Creating a widget from scratch

Introduction

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.

Displaying a widget on the screen

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:

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.

The origins of the Dial Widget

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.

The Basics

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;

gtk_dial_realize()

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;

Size negotiation

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;

gtk_dial_expose()

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;

>Event handling

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;

Possible Enhancements

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:

23.5 Learning More

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!


[Previous] [Contents] [Next]