Journal Articles

CVu Journal Vol 16, #4 - Aug 2004 + Programming Topics
Browse in : All > Journals > CVu > 164 (12)
All > Topics > Programming (877)
Any of these categories - All of these categories

Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.

Title: An Introduction to Programming with GTK+ and Glade in ISO C and ISO C++

Author: Administrator

Date: 03 August 2004 13:16:06 +01:00 or Tue, 03 August 2004 13:16:06 +01:00

Summary: 

This short tutorial is intended as a simple introduction to writing GTK+ applications in C and C++, using the current 2.0/2.2 version of libgtk. It also covers the use of the Glade user interface designer for rapid application development (RAD).

Body: 

What is GTK+?

GTK+ is a toolkit used for writing graphical applications. Originally written for the X11 windowing system, it has now been ported to other systems, such as Microsoft Windows and the Apple Macintosh, and so may be used for cross-platform software development. GTK+ was written as a part of the GNU Image Manipulation Program (GIMP), but has long been a separate project, used by many other free software projects, one of the most notable being the GNU Network Object Model Environment (GNOME) Project.

GTK+ is written in C and, because of the ubiquity of the C language, bindings have been written to allow the development of GTK+ applications in many other languages. This short tutorial is intended as a simple introduction to writing GTK+ applications in C and C++, using the current 2.0/2.2 version of libgtk. It also covers the use of the Glade user interface designer for rapid application development (RAD).

It is assumed that the reader is familiar with C and C++ programming, and it would be helpful to work through the "Getting Started" chapter of the GTK+ tutorial before reading further. The GTK+, Glib, libglade, Gtkmm and libglademm API references will be useful while working through the examples.

I hope you find this tutorial informative.

Building the Example Code

Several working, commented examples accompany the tutorial. They are also available from http://people.debian.org/~rleigh/gtk/ogcalc/. To build them, type:

./configure
make

This will check for the required libraries and build the example code. Each program may then be run from within its subdirectory.

I have been asked on various occasions to write a tutorial to explain how the GNU autotools work. While this is not the aim of this tutorial, I have converted the build to use the autotools as a simple example of their use.

Legal Bit

This tutorial document, the source code and compiled binaries, and all other files distributed in the source package are copyright © 2003 - 2004 Roger Leigh. These files and binary programs are free software; you can redistribute them and/or modify them under the terms of the GNU General Public Licence as published by the Free Software Foundation; either version 2 of the Licence, or (at your option) any later version.

A copy of the GNU General Public Licence version 2 is provided in the file COPYING in the source package this document was generated from.

GTK+ Basics

Objects

GTK+ is an object-oriented (OO) toolkit. I'm afraid that unless one is aware of the basic OO concepts (classes, class methods, inheritance, polymorphism), this tutorial (and GTK+ in general) will seem rather confusing. On my first attempt at learning GTK+, I didn't "get" it, but after I learnt C++, the concepts GTK+ is built on just "clicked", and I understood it quite quickly.

The C language does not natively support classes, and so GTK+ provides its own object/type system, GObject. GObject provides objects, inheritance, polymorphism, constructors, destructors and other facilities such as reference counting and signal emission and handling. Essentially, it provides C++ classes in C. The syntax differs a little from C++ though. As an example, the following C++

myclass c;
c.add(2);

would be written like this using GObject:

myclass *c = myclass_new();
myclass_add(c, 2);

The difference is due to the lack of a this pointer in the C language (since objects do not exist). This means that class methods require the object pointer passing as their first argument. This happens automatically in C++, but it needs doing "manually" in C.

Another difference is seen when dealing with polymorphic objects. All GTK+ widgets (the controls, such as buttons, checkboxes, labels, etc.) are derived from GtkWidget. That is to say, a GtkButton is a GtkWidget, which is a GtkObject, which is a GObject. In C++, one can call member functions from both the class and the classes it is derived from. With GTK+, the object needs explicit casting to the required type. For example

GtkButton mybutton;
mybutton.set_label("Cancel");
mybutton.show();

would be written as

GtkButton *mybutton = gtk_button_new();
gtk_button_set_label(mybutton, "Cancel");
gtk_widget_show(GTK_WIDGET(mybutton))

In this example, set_label() is a method of GtkButton, whilst show() is a method of GtkWidget, which requires an explicit cast. The GTK_WIDGET() cast is actually a form of run-time type identification (RTTI). This ensures that the objects are of the correct type when they are used.

Objects and C work well, but there are some issues, such as a lack of type-safety of callbacks and limited compile-time type checking. Using GObject, deriving new widgets is complex and error-prone. For these, and other, reasons, C++ may be a better language to use. libsigc++ provides type-safe signal handling, and all of the GTK+ (and GLib, Pango et. al.) objects are available as standard C++ classes. Callbacks may also be class methods, which makes for cleaner code, since the class can contain object data without having to resort to passing in data as a function argument. These potential problems will become clearer in the next sections.

Widgets

A user interface consists of different objects with which the user can interact. These include buttons which can be pushed, text entry fields, tick boxes, labels and more complex things such as menus, lists, multiple selections, colour and font pickers. Some example widgets are shown in Figure 1.

Not all widgets are interactive. For example, the user cannot usually interact with a label, or a framebox. Some widgets, such as containers, boxes and event boxes are not even visible to the user (there is more about this in the next section).

Different types of widget have their own unique properties. For example, a label widget contains the text it displays, and there are functions to get and set the label text. A checkbox may be ticked or not, and there are functions to get and set its state. An options menu has functions to set the valid options, and get the option the user has chosen.

A selection of GTK+ widgets

Figure 1. A selection of GTK+ widgets

Containers

The top-level of every GTK+ interface is the window. A window is what one might expect it to be: it has a title bar, borders (which may allow resizing), and it contains the rest of the interface.

In GTK+, a GtkWindow is a GtkContainer. In English, this means that the window is a widget that can contain another widget. More precisely, a GtkContainer can contain exactly one widget. This is usually quite confusing compared with the behaviour of other graphics toolkits, which allow one to place the controls on some sort of "form".

The fact that a GtkWidget can only contain one widget initially seems quite useless. After all, user interfaces usually consist of more than a single button. In GTK+, there are other kinds of GtkContainer. The most commonly used are horizontal boxes, vertical boxes, and tables. The structure of these containers is shown in Figure 2.

Figure 2 shows the containers as having equal size, but in a real interface, the containers resize themselves to fit the widgets they contain.

GTK+ containers

Each container may contain other widgets in the shaded areas. Containers may contain more containers, allowing them to nest. Complex interfaces may be constructed by nesting the different types of container.

Figure 2. GTK+ containers

In other cases, widgets may be expanded or shrunk to fit the space allotted to them. There are several ways to control this behaviour, to give fine control over the appearance of the interface.

In addition to the containers discussed above, there are more complex containers available, such are horizontal and vertical panes, tabbed notebooks, and viewports and scrolled windows. These are out of the scope of this tutorial, however.

Newcomers to GTK+ may find the concept of containers quite strange. Users of Microsoft Visual Basic or Visual C++ may be used to the free-form placement of controls. The placement of controls at fixed positions on a form has no advantages over automatic positioning and sizing. All decent modern toolkits use automatic positioning. This fixes several issues with fixed layouts:

  • The hours spent laying out forms, particularly when maintaining existing code.

  • Windows that are too big for the screen.

  • Windows that are too small for the form they contain.

  • Issues with spacing when accommodating translated text.

  • Bad things happen when changing the font size from the default.

The nesting of containers results in a widget tree, which has many useful properties, some of which will be used use later. One important advantage is that they can dynamically resize and accommodate different lengths of text, important for internationalisation, when translations in different languages may vary widely in their size.

The Glade user interface designer can be very instructive when exploring how containers and widget packing work. It allows easy manipulation of the interface, and all of the standard GTK+ widgets are available. Modifying an existing interface is trivial, even when doing major reworking. Whole branches of the widget tree may be cut, copied and pasted at will, and a widget's properties may be manipulated using the "Properties" dialogue. While studying the code examples, Glade may be used to interactively build and manipulate the interface, to visually follow how the code is working. More detail about Glade is provided in a later section, where libglade is used to dynamically load a user interface.

Signals

Most graphical toolkits are event-driven, and GTK+ is no exception. Traditional console applications tend not to be event-driven; these programs follow a fixed path of execution. A typical program might do something along these lines:

  • Prompt the user for some input

  • Do some work

  • Print the results

This type of program does not give the user any freedom to do things in a different order. Each of the above steps might be a single function (each of which might be split into helper functions, and so on).

GTK+ applications differ from this model. The programs must react to events, such as the user clicking on a button, or pressing Enter in an text entry field. These widgets emit signals in response to user actions. For each signal of interest, a function defined by the programmer is called. In these functions, the programmer can do whatever needed. For example, in the ogcalc program, when the "Calculate" button is pressed, a function is called to read the data from entry fields, do some calculations, and then display the results.

Each event causes a signal to be emitted from the widget handling the event. The signals are sent to signal handlers. A signal handler is a function which is called when the signal is emitted. The signal handler is connected to the signal. In C, these functions are known as callbacks. The process is illustrated graphically in Figure 3.

A signal may have zero, one or many signal handlers connected (registered) with it. If there is more than one signal handler, they are called in the order they were connected in.

Without signals, the user interface would display on the screen, but would not actually do anything. By associating signal handlers with signals one is interested in, events triggered by the user interacting with the widgets will cause things to happen.

A typical signal handler.

When the button is pressed, a signal is emitted, causing the registered callback function to be called.

Figure 3. A typical signal handler.

Libraries

GTK+ is comprised of several separate libraries:

<colgroup> <col> <col></colgroup> <tbody> </tbody>
atk Accessibility Toolkit, to enable use by disabled people.
gdk GIMP Drawing Kit (XLib abstraction layer - windowing system dependent part).
gdk-pixbuf Image loading and display.
glib Basic datatypes and common algorithms.
gmodule Dynamic module loader (libdl portability wrapper).
gobject Object/type system.
gtk GIMP Tool Kit (windowing system independent part).
pango Typeface layout and rendering.

When using libglade another library is required:

<colgroup> <col> <col></colgroup> <tbody> </tbody>
glade User Interface description loader/constructor.

Lastly, when using C++, some additional C++ libraries are also needed:

<colgroup> <col> <col></colgroup> <tbody> </tbody>
atkmm C++ ATK wrapper.
gdkmm C++ GDK wrapper.
gtkmm C++ GTK+ wrapper.
glademm C++ Glade wrapper.
pangomm C++ Pango wrapper.
sigc++ Advanced C++ signal/slot event handling (wraps GObject signals).

This looks quite intimidating! However, there is no need to worry, since compiling and linking programs is quite easy. Since the libraries are released together as a set, there are few library interdependency issues.

Designing an Application

Planning Ahead

Before starting to code, it is necessary to plan ahead by thinking about what the program will do, and how it should do it. When designing a graphical interface, one should pay attention to how the user will interact with it, to ensure that it is easy to understand, and efficient to use.

When designing a GTK+ application, it is useful to sketch the interface on paper, before constructing it. Interface designers such as Glade are helpful here, but a pen and paper are best for the initial design.

Introducing ogcalc

As part of the production (and quality control) processes in the brewing industry, it is necessary to determine the alcohol content of each batch at several stages during the brewing process. This is calculated using the density (gravity) in g/cm3 and the refractive index. A correction factor is used to align the calculated value with that determined by distillation, which is the standard required by HM Customs & Excise. Because alcoholic beverages are only slightly denser than water, the PG value is (density-1) x 100. That is, 1.0052 would be entered as 52.

Original gravity is the density during fermentation. As alcohol is produced during fermentation, the density falls. Traditionally, this would be similar to the PG, but with modern high-gravity brewing (at a higher concentration) it tends to be higher. It is just as important that the OG is within the set limits of the specification for the product as the ABV.

The ogcalc program performs the following calculation:

O = (R x 2.597) - (P x 1.644) - 34.4165 + C

If O is less than 60, then

A = (O - P) x 0.130

otherwise

A = (O - P) x 0.134

The symbols have the following meanings:

<colgroup> <col> <col></colgroup> <tbody> </tbody>
A Percentage Alcohol By Volume
C Correction Factor
O Original Gravity
P Present Gravity
R Refractive Index

Designing the Interface

The program needs to ask the user for the values of C, P, and R. It must then display the results, A and O. A simple sketch of the interface is shown in Figure 4.

Sketching a user interface

The ogcalc main window is drawn simply, to illustrate its functionality. The top row contains three numeric entry fields, followed by two result fields on the middle row. The bottom row contains buttons to quit the program, reset the interface and do the calculation.

Figure 4. Sketching a user interface

Creating the Interface

Due to the need to build up an interface from the bottom up, due to the containers being nested, the interface is constructed starting with the window, then the containers that fit in it. The widgets the user will use go in last. This is illustrated in Figure 5.

Widget packing

The steps taken during the creation of an interface are shown, demonstrating the use of nested containers to pack widgets.

Figure 5. Widget packing

Once a widget has been created, signal handlers may be connected to its signals. After this is completed, the interface can be displayed, and the main event loop may be entered. The event loop receives events from the keyboard, mouse and other sources, and causes the widgets to emit signals. To end the program, the event loop must first be left.

GTK+ and C

Introduction

Many GTK+ applications are written in C alone. This section demonstrates the C/plain/ogcalc program discussed in the previous section. Figure 6 is a screenshot of the finished application.

C/plain/ogcalc in action

Figure 6. C/plain/ogcalc in action

This program consists of just three functions:

on_button_clicked_reset() - Reset the interface to its default state

on_button_clicked_calculate - Get the values the user has entered, do a calculation, then display the results.

main() - Initialise GTK+, construct the interface, connect the signal handlers, then enter the GTK+ event loop.

Code Listing

The program code is listed on pages 23-26. The source code is extensively commented, to explain what is going on.

To build the source, do the following:

cd C/plain
cc 'pkg-config -cflags gtk+-2.0' -c ogcalc.c
cc 'pkg-config -libs gtk+-2.0' -o ogcalc 
ogcalc.o

Roger Leigh

[I would recommend entering the code, it would be a valuable exercise in learning how a GTK application is built up. It may take a little while, but it certainly helped me to understand what is going on. - Ed]

// C/plain/ogcalc.c
#include <gtk/gtk.h>

GtkWidget *create_spin_entry(
          const gchar *label_text,
          const gchar *tooltip_text,
          GtkWidget **spinbutton_pointer,
          GtkAdjustment *adjustment,
          guint digits);
GtkWidget *create_result_label(
          const gchar *label_text,
          const gchar *tooltip_text,
          GtkWidget **result_label_pointer);
void on_button_clicked_reset(
          GtkWidget *widget,
          gpointer data);
void on_button_clicked_calculate(
          GtkWidget *widget,
          gpointer data);

/* This structure holds all of the widgets
   needed to get all the values for the
   calculation. */
struct calculation_widgets {
  GtkWidget *pg_val;     // PG entry widget
  GtkWidget *ri_val;     // RI entry widget
  GtkWidget *cf_val;     // CF entry widget
  GtkWidget *og_result;  // OG result label
  GtkWidget *abv_result; // ABV% result label
};

int main(int argc, char *argv[]) {
  /* These are pointers to widgets used in
     constructing the interface, and later
     used by signal handlers. */
  GtkWidget *window;
  GtkWidget *vbox1, *vbox2, *hbox1, *hbox2,
            *button1, *button2;
  GtkObject *adjustment, *hsep;
  struct calculation_widgets cb_widgets;

  gtk_init(&argc, &argv); // Initialise GTK+.
  
  /* Create a new top-level window. */
  window =
         gtk_window_new(GTK_WINDOW_TOPLEVEL);
  /* Set the window title. */
  gtk_window_set_title(GTK_WINDOW(window),
                       "OG & ABV Calculator");
  /* Disable window resizing */
  gtk_window_set_resizable(GTK_WINDOW(window),
                           FALSE);
  /* Connect the window close button
     ("destroy" event) to gtk_main_quit(). */
  g_signal_connect(G_OBJECT(window),
                   "destroy", gtk_main_quit,
                   NULL);

  /* Create a GtkVBox to hold the other
     widgets.  This contains other widgets,
     which are packed in to it vertically. */
  vbox1 = gtk_vbox_new(FALSE, 0);
  /* Add the VBox to the Window.  A GtkWindow
    /is a/ GtkContainer which /is a/
    GtkWidget.  GTK_CONTAINER casts the
    GtkWidget to a GtkContainer, like a C++
    dynamic_cast. */
  gtk_container_add(GTK_CONTAINER(window),
                    vbox1);
  /* Display the Vbox.  At this point, the
     Window has not yet been displayed, so the
     window isn't yet visible. */
  gtk_widget_show(vbox1);

  /* Create a second GtkVBox.  Unlike the
     previous VBox, the widgets it will
     contain will be of uniform size and
     separated by a 5 pixel gap. */
  vbox2 = gtk_vbox_new(TRUE, 5);
  /* Set a 10 pixel border */
  gtk_container_set_border_width(
                   GTK_CONTAINER(vbox2), 10);
  /* Add this VBox to our first VBox. */
  gtk_box_pack_start(GTK_BOX(vbox1), vbox2,
                     FALSE, FALSE, 0);

  gtk_widget_show(vbox2);

  /* Create a GtkHBox.  This is identical to a
     GtkVBox except that the widgets pack
     horizontally, instead of vertically. */
  hbox1 = gtk_hbox_new(FALSE, 10);
  /* Add to vbox2.  The function's other
     arguments mean to expand into any extra
     space alloted to it, to fill the extra
     space and to add 0 pixels of padding
     between it and its neighbour. */
  gtk_box_pack_start(GTK_BOX(vbox2), hbox1,
                     TRUE, TRUE, 0);
  gtk_widget_show (hbox1);

  /* A GtkAdjustment is used to hold a numeric
     value: the initial value, minimum and
     maximum values, "step" and "page"
     increments and the "page size".  It's
     used by spin buttons, scrollbars, sliders
     etc. */
  adjustment = gtk_adjustment_new(0.0, 0.0,
                       10000.0, 0.01, 1.0, 0);
  /* Call a helper function to create a
     GtkSpinButton entry together with a label
     and a tooltip.  The spin button is stored
     in the cb_widgets.pg_val pointer for
     later use. */
  hbox2 = create_spin_entry("PG:",
                  "Present Gravity (density)",
                  &cb_widgets.pg_val,
                  GTK_ADJUSTMENT(adjustment),
                  2);

  /* Pack the returned GtkHBox into the
     interface. */
  gtk_box_pack_start(GTK_BOX(hbox1), hbox2,
                     TRUE, TRUE, 0);
  gtk_widget_show(hbox2);

  /* Repeat the above for the next spin
     button. */
  adjustment = gtk_adjustment_new (0.0, 0.0,
                       10000.0, 0.01, 1.0, 0);
  hbox2 = create_spin_entry("RI:",
               "Refractive Index",
               &cb_widgets.ri_val,
               GTK_ADJUSTMENT(adjustment), 2);
  gtk_box_pack_start(GTK_BOX(hbox1), hbox2,
                     TRUE, TRUE, 0);
  gtk_widget_show(hbox2);

  /* Repeat again for the last spin button. */
  adjustment = gtk_adjustment_new (0.0, -50.0,
                           50.0, 0.1, 1.0, 0);
  hbox2 = create_spin_entry("CF:",
                  "Correction Factor",
                  &cb_widgets.cf_val,
                  GTK_ADJUSTMENT(adjustment),
                  1);
  gtk_box_pack_start(GTK_BOX(hbox1), hbox2,
                     TRUE, TRUE, 0);
  gtk_widget_show(hbox2);

  /* Now we move to the second "row" of the
     interface, for displaying the results. */

  /* Firstly, a new GtkHBox to pack the labels
     into. */
  hbox1 = gtk_hbox_new (TRUE, 10);
  gtk_box_pack_start(GTK_BOX(vbox2), hbox1,
                     TRUE, TRUE, 0);
  gtk_widget_show (hbox1);

  /* Create the OG result label, then pack and
     display. */
  hbox2 = create_result_label("OG:",
                 "Original Gravity (density)",
                 &cb_widgets.og_result);
  gtk_box_pack_start(GTK_BOX(hbox1), hbox2,
                     TRUE, TRUE, 0);
  gtk_widget_show(hbox2);

  /* Repeat as above for the second result
     value. */
  hbox2 = create_result_label("ABV %:",
                 "Percent Alcohol By Volume",
                 &cb_widgets.abv_result);
  gtk_box_pack_start(GTK_BOX(hbox1), hbox2,
                     TRUE, TRUE, 0);
  gtk_widget_show(hbox2);

  /* Create a horizontal separator
     (GtkHSeparator) and add it to the VBox.*/
  hsep = gtk_hseparator_new();
  gtk_box_pack_start(GTK_BOX(vbox1), hsep,
                     FALSE, FALSE, 0);
  gtk_widget_show(hsep);

  /* Create a GtkHBox to hold the bottom row
     of buttons. */
  hbox1 = gtk_hbox_new(TRUE, 5); 
  gtk_container_set_border_width(
                    GTK_CONTAINER(hbox1), 10);
  gtk_box_pack_start(GTK_BOX(vbox1), hbox1,
                     TRUE, TRUE, 0);
  gtk_widget_show(hbox1);

  /* Create the "Quit" button.  We use a
     "stock" button-commonly-used buttons that
     have a set title and icon. */
  button1 =
    gtk_button_new_from_stock(GTK_STOCK_QUIT);
  /* We connect the "clicked" signal to the
     gtk_main_quit() callback which will end
     the program. */
  g_signal_connect(G_OBJECT(button1),
              "clicked", gtk_main_quit, NULL);
  gtk_box_pack_start(GTK_BOX(hbox1), button1,
                     TRUE, TRUE, 0);
  gtk_widget_show(button1);

  /* This button resets the interface. */
  button1 =
       gtk_button_new_with_mnemonic("_Reset");
  /* The "clicked" signal is connected to the
     on_button_clicked_reset() callback above,
     and our "cb_widgets" widget list is
     passed as the second argument, cast to a
     gpointer (void). */
  g_signal_connect(G_OBJECT(button1),
          "clicked",
          G_CALLBACK(on_button_clicked_reset),
          (gpointer)&cb_widgets);
  /* g_signal_connect_swapped is used to
     connect a signal from one widget to the
     handler of another.  The last argument is
     the widget that will be passed as the
     first argument of the callback.  This
     causes gtk_widget_grab_focus to switch
     the focus to the PG entry. */
  g_signal_connect_swapped(G_OBJECT(button1),
     "clicked",
     G_CALLBACK(gtk_widget_grab_focus),
     (gpointer)GTK_WIDGET(cb_widgets.pg_val));
  /* This lets the default action (Enter)
     activate this widget even when the focus
     is elsewhere. This doesn't set the
     default, it just makes it possible to
     set.*/
  GTK_WIDGET_SET_FLAGS(button1,
                       GTK_CAN_DEFAULT);
  gtk_box_pack_start(GTK_BOX(hbox1), button1,
                     TRUE, TRUE, 0);
  gtk_widget_show(button1);

  /* The final button is the Calculate button.*/
  button2 =
   gtk_button_new_with_mnemonic("_Calculate");
  /* When the button is clicked, call the
     on_button_clicked_calculate() function.
     This is the same as for the Reset button.*/
  g_signal_connect(G_OBJECT(button2),
      "clicked",
      G_CALLBACK(on_button_clicked_calculate),
     (gpointer)&cb_widgets);
  /* Switch the focus to the Reset button when
     the button is clicked. */
  g_signal_connect_swapped(G_OBJECT(button2),
            "clicked",
            G_CALLBACK(gtk_widget_grab_focus),
            (gpointer)GTK_WIDGET(button1));
  /* As before, the button can be the default.*/
  GTK_WIDGET_SET_FLAGS(button2,
                       GTK_CAN_DEFAULT);
  gtk_box_pack_start(GTK_BOX(hbox1),
                     button2, TRUE, TRUE, 0);
  /* Make this button the default.  Note the
     thicker border in the interface-this
     button is activated if you press enter in
     the CF entry field. */

  gtk_widget_grab_default(button2);
  gtk_widget_show(button2);

  /* Set up data entry focus movement.  This
     makes the interface work correctly with
     the keyboard, so that you can touch-type
     through the interface with no mouse usage
     or tabbing between the fields. */

  /* When Enter is pressed in the PG entry
     box, focus is transferred to the RI
     entry. */
  g_signal_connect_swapped(
     G_OBJECT(cb_widgets.pg_val), "activate",
     G_CALLBACK(gtk_widget_grab_focus),
     (gpointer)GTK_WIDGET(cb_widgets.ri_val));
  /* RI -> CF. */
  g_signal_connect_swapped(
     G_OBJECT(cb_widgets.ri_val), "activate",
     G_CALLBACK(gtk_widget_grab_focus),
     (gpointer)GTK_WIDGET(cb_widgets.cf_val));
  /* When Enter is pressed in the RI field, it
     activates the Calculate button. */
  g_signal_connect_swapped(
     G_OBJECT(cb_widgets.cf_val), "activate",
     G_CALLBACK(gtk_window_activate_default),
     (gpointer)GTK_WIDGET(window));

  /* The interface is complete, so finally we
     show the top-level window.  This is done
     last or else the user might see the
     interface drawing itself during the short
     time it takes to construct.  It's nicer
     this way. */
  gtk_widget_show(window);

  /* Enter the GTK Event Loop.  This is where
     all the events are caught and handled. 
     It is exited with gtk_main_quit(). */
  gtk_main();

  return 0;
}

/* A utility function for UI construction.  It constructs part of the widget tree, then returns its root. */
GtkWidget *create_spin_entry(
          const gchar *label_text,
          const gchar *tooltip_text,
          GtkWidget **spinbutton_pointer,
          GtkAdjustment *adjustment,
          guint digits) {
  GtkWidget *hbox, *eventbox, *spinbutton,
            *label;
  GtkTooltips *tooltip;

  /* A GtkHBox to pack the entry child widgets
     into. */
  hbox = gtk_hbox_new(FALSE, 5);

  /* An eventbox.  This widget is just a
     container for widgets (like labels) that
     don't have an associated X window, and so
     can't receive X events.  This is just
     used to we can add tooltips to each
     label. */
  eventbox = gtk_event_box_new();
  gtk_widget_show(eventbox);
  gtk_box_pack_start(GTK_BOX(hbox),
                    eventbox,FALSE, FALSE, 0);
  /* Create a label. */
  label = gtk_label_new(label_text);

  /* Add the label to the eventbox */
  gtk_container_add(GTK_CONTAINER(eventbox),
                    label);
  gtk_widget_show(label);

  /* Create a GtkSpinButton and associate it
     with the adjustment. It adds/substracts
     0.5 when the spin buttons are used, and
     has digits accuracy. */
  spinbutton = gtk_spin_button_new(
                adjustment, 0.5, digits);
  /* Only numbers can be entered. */
  gtk_spin_button_set_numeric(
           GTK_SPIN_BUTTON(spinbutton), TRUE);
  gtk_box_pack_start(GTK_BOX(hbox),
           spinbutton, TRUE, TRUE, 0);
  gtk_widget_show(spinbutton);

  /* Create a tooltip and add it to the
     EventBox previously created. */
  tooltip = gtk_tooltips_new();
  gtk_tooltips_set_tip(tooltip, eventbox,
                       tooltip_text, NULL);

  *spinbutton_pointer = spinbutton;
  return hbox;
}

/* A utility function for UI construction.  It
   constructs part of the widget tree, then
   returns its root. */
GtkWidget *create_result_label(
          const gchar *label_text,
          const gchar *tooltip_text,
          GtkWidget **result_label_pointer) {
  GtkWidget *hbox, *eventbox, *result_label,
            *result_value;
  GtkTooltips *tooltip;

  /*A GtkHBox to pack entry child widgets into*/
  hbox = gtk_hbox_new(FALSE, 5);

  /* As before, a label in an event box with a
     tooltip. */
  eventbox = gtk_event_box_new();
  gtk_widget_show(eventbox);
  gtk_box_pack_start(GTK_BOX(hbox), eventbox,
                     FALSE, FALSE, 0);
  result_label = gtk_label_new(label_text);
  gtk_container_add(GTK_CONTAINER(eventbox),
                    result_label);
  gtk_widget_show(result_label);

  /* This is a label, used to display the OG
     result. */
  result_value = gtk_label_new (NULL);
  /* Because it's a result, it is set
     "selectable", to allow copy/paste of the
     result, but it's not modifiable. */
  gtk_label_set_selectable(
               GTK_LABEL(result_value), TRUE);
  gtk_box_pack_start(GTK_BOX(hbox),
               result_value, TRUE, TRUE, 0);
  gtk_widget_show(result_value);

  /* Add the tooltip to the event box. */
  tooltip = gtk_tooltips_new();
  gtk_tooltips_set_tip(tooltip, eventbox,
                       tooltip_text, NULL);

  *result_label_pointer = result_value;
  return hbox;
}

/* This is a callback function.  It resets
   the values of the entry widgets, and
   clears the results.  "data" is the
   calculation_widgets structure, which needs
   casting back to its correct type from a
   gpointer (void) type. */
void on_button_clicked_reset(
          GtkWidget *widget,
          gpointer data ) {
  /* Widgets to manipulate. */
  struct calculation_widgets *w;

  w = (struct calculation_widgets *) data;

  gtk_spin_button_set_value(
             GTK_SPIN_BUTTON(w->pg_val), 0.0);
  gtk_spin_button_set_value(
             GTK_SPIN_BUTTON(w->ri_val), 0.0);
  gtk_spin_button_set_value(
             GTK_SPIN_BUTTON(w->cf_val), 0.0);
  gtk_label_set_text(
             GTK_LABEL(w->og_result), "");
  gtk_label_set_text(
             GTK_LABEL(w->abv_result), "");
}

/* This callback does the actual calculation. 
   Its arguments are the same as for
   on_button_clicked_reset(). */
void on_button_clicked_calculate(
          GtkWidget *widget,
          gpointer data ) {
  gdouble pg, ri, cf, og, abv;
  gchar *og_string, *abv_string;
  struct calculation_widgets *w;

  w = (struct calculation_widgets *) data;

  /* Get the numerical values from the entry
     widgets. */
  pg = gtk_spin_button_get_value(
                  GTK_SPIN_BUTTON(w->pg_val));
  ri = gtk_spin_button_get_value(
                  GTK_SPIN_BUTTON(w->ri_val));
  cf = gtk_spin_button_get_value(
                  GTK_SPIN_BUTTON(w->cf_val));

  og = (ri*2.597) - (pg*1.644) - 34.4165 + cf;

  /* Do the sums. */
  if (og < 60)
    abv = (og - pg) * 0.130;
  else
    abv = (og - pg) * 0.134;

  /* Display the results.  Note the
     <b></b> GMarkup tags to make it display
     in Bold. */
  og_string =
         g_strdup_printf("<b>%0.2f</b>", og);
  abv_string =
         g_strdup_printf("<b>%0.2f</b>", abv);

  gtk_label_set_markup(
                    GTK_LABEL(w->og_result),
                    og_string);
  gtk_label_set_markup(
                    GTK_LABEL(w->abv_result),
                    abv_string);

  g_free(og_string);
  g_free(abv_string);
}

Notes: 

More fields may be available via dynamicdata ..