Tutorial

Inti Logo

« Creating a Composite Widget Building an GNU Autotools Project  »

Creating a widget from scratch

  1. Displaying a widget on the screen
  2. The origins of the Dial Widget
  3. The Basics
  4. The Realize Signal
  5. Size Negotiation
  6. The Expose Event
  7. Event Handling
  8. Possible Enhancements

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 a  analog dial widget with a pointer that the user can drag to set the value.




Dial is a custom widget that derives directly from Gtk::Widget. Creating this kind of kind of widget is akin to creating a new C widget in GTK+. It's a little more involved than simply deriving a new widget from an existing widget and making a few changes. Most of the time you can find a predefined widget that will meets your needs, like Tictactoe in the previous section, but occasionally you wont. This is when you need to create a new widget from scratch. The steps involved are the same as those used to create a GTK+ widget, except in C++ there are fewer of them. Lets take a look. You will find the source code for Dial in the <examples/dial> directory.


Displaying a widget on the screen

There are several steps that are involved in displaying a widget on the screen. After the widget is constructed you will have to override several protected Gtk::Widget signal handlers:

virtual void on_realize();

on_realize() is responsible for creating an X window for the widget if it has one.

virtual void on_map();

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

virtual bool on_expose_event(const Gdk::EventExpose& event);

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


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

The header file for the Dial class is dial.h

/*  Inti: Integrated Foundation Classes
 *  Copyright (C) 2003 The Inti Development Team.
 *
 *  dial.h - Dial is a custom widget. It's a C++ version of the GtkDial example.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program 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 program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#ifndef INTI_GTK_DIAL_H
#define INTI_GTK_DIAL_H

#ifndef INTI_GTK_WIDGET_H
#include <inti/gtk/widget.h>
#endif

namespace Inti {

namespace Gtk {

class Adjustment;

class Dial : public Widget
{
    UpdateType policy_;
    // Either UPDATE_CONTINUOUS, UPDATE DISCONTINUOUS or UPDATE_DELAYED.

    unsigned int button;
    // The mouse button currently pressed or 0 if none.

    int radius;
    int pointer_width;
    // Dimensions of the dial components.

    Connection timer;
    // Signal connection of the update timer.

    float angle;
    float last_angle;
    // Current angle.

    float old_value;
    float old_lower;
    float old_upper;
    // Old values from adjustment stored so we know when something changes.

    Pointer<Adjustment> adjustment_;
    // The adjustment object that stores the data for this dial. A smart
    // pointer is used to handle the reference counting.

    void update();
    void update_mouse(int x, int y);

    bool on_timeout();

protected:
// Signal Handlers
    virtual void on_realize();

    virtual bool on_expose_event(const Gdk::EventExpose& event);

    virtual void on_size_request(Requisition *requisition);

    virtual void on_size_allocate(const Allocation& allocation);

    virtual bool on_button_press_event(const Gdk::EventButton& event);

    virtual bool on_button_release_event(const Gdk::EventButton& event);

    virtual bool on_motion_notify_event(const Gdk::EventMotion& event);

    void on_adjustment_changed();

    void on_adjustment_value_changed();

public:
// Constructors
    Dial(Adjustment *adjustment = 0);

    virtual ~Dial();
   
// Accessors
    Adjustment* get_adjustment() const;
   
// Methods
    void set_adjustment(Adjustment *adjustment);

    void set_update_policy(UpdateType policy);
};

} // namespace Gtk

} // namespace Inti

#endif // INTI_GTK_DIAL_H


Since there is quite a bit more going on in this widget than the last one, we have more class data members and signal handlers, but otherwise things are pretty similar. Note that when we store a pointer to the Adjustment object, we need to 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. For convenience, Dial uses a smart pointer to manage the adjustment's reference counting but it could have done it manually.

Also, there are a few functions to manipulate the widget's options: get_adjustment(), set_adjustment() and set_update_policy().

The source file for the Dial class is dial.cc:

#include "dial.h"
#include <inti/gtk/private/widget_p.h>
#include <inti/gtk/adjustment.h>
#include <inti/gtk/style.h>
#include <inti/gdk/window.h>
#include <inti/main.h>
#include <algorithm>
#include <math.h>

const int scroll_delay_length = 300;
const int dial_default_size = 100;

using namespace Inti;

Gtk::Dial::Dial(Adjustment *adjustment)
: Gtk::Widget((GtkWidget*)Gtk::WidgetClass::create()), adjustment_(0)
{
    button = 0;
    policy_ = UPDATE_CONTINUOUS;
    radius = 0;
    pointer_width = 0;
    angle = 0.0;
    old_value = 0.0;
    old_lower = 0.0;
    old_upper = 0.0;

    if (!adjustment)
        adjustment = new Gtk::Adjustment;

    set_adjustment(adjustment);
}

Gtk::Dial::~Dial()
{
}

Gtk::Adjustment*
Gtk::Dial::get_adjustment() const
{
    return adjustment_;
}

void
Gtk::Dial::set_adjustment(Adjustment *adjustment)
{
    if (adjustment_)
    {
        adjustment_->disconnect_by_name("changed");
        adjustment_->disconnect_by_name("value_changed");
    }

    adjustment_ = adjustment;

    if (adjustment_)
    {
        adjustment_->sig_changed().connect(slot(this, &Dial::on_adjustment_changed));
        adjustment_->sig_value_changed().connect(slot(this, &Dial::on_adjustment_value_changed));
    }

    old_value = adjustment_->get_value();
    old_lower = adjustment_->lower();
    old_upper = adjustment_->upper();
    update();
}

void
Gtk::Dial::on_realize()
{
    set_flags(REALIZED);

    Gdk::EventMaskField mask = get_events() | Gdk::EXPOSURE_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK;
    mask |= (Gdk::POINTER_MOTION_MASK | Gdk::POINTER_MOTION_HINT_MASK);
    Gdk::WindowAttr attributes(get_allocation(), Gdk::WINDOW_CHILD, mask);
    attributes.set_visual(*get_visual());
    attributes.set_colormap(*get_colormap());

    Gdk::Window *window = new Gdk::Window(*get_parent_window(), attributes);
    set_window(*window);
    get_style()->attach(*window);
    window->set_user_data(gtk_widget());
    get_style()->set_background(*window, STATE_ACTIVE);
}
   
void
Gtk::Dial::on_size_request(Requisition *requisition)
{
    requisition->set(dial_default_size, dial_default_size);
}

void
Gtk::Dial::on_size_allocate(const Allocation& allocation)
{
    set_allocation(allocation);

    if (is_realized())
    {
        get_window()->move_resize(allocation);
    }

    radius = (int)(std::min(allocation.width(), allocation.height()) * 0.45);
    pointer_width = radius / 5;
}

bool
Gtk::Dial::on_expose_event(const Gdk::EventExpose& event)
{
    if (event.count() > 0)
        return false;

    Allocation allocation = get_allocation();
    int xc = allocation.width() / 2;
    int yc = allocation.height() / 2;

    int upper = (int)adjustment_->upper();
    int lower = (int)adjustment_->lower();

    double s = sin(last_angle);
    double c = cos(last_angle);
    last_angle = angle;

    Gdk::Point points[6];
    points[0].set(int(xc + s * pointer_width / 2), int(yc + c * pointer_width / 2));
    points[1].set(int(xc + c * radius), int(yc - s * radius));
    points[2].set(int(xc - s * pointer_width / 2), int(yc - c * pointer_width / 2));
    points[3].set(int(xc - c * radius / 10), int(yc + s * radius / 10));
    points[4].set(points[0].x(), points[0].y());

    Gdk::Window *window = get_window();
    window->invalidate(allocation, false);
    Gtk::Style *style = get_style();
    style->draw_polygon(*window, STATE_NORMAL, SHADOW_OUT, points, 5, false, 0, this);

    // Draw ticks
    int inc = upper - lower;
    if (inc == 0)
        return false;

    double increment = (100 * G_PI) / ( radius * radius);
    while (inc < 100) inc *= 10;
    while (inc >= 1000) inc /= 10;
    double last = -1;

    for (int i = 0; i <= inc; i++)
    {
        double theta = ((float)i * G_PI / (18 * inc / 24.) - G_PI / 6.);
        if ((theta - last) < (increment))
            continue;

        last = theta;
        s = sin(theta);
        c = cos(theta);

        int tick_length = (i % (inc / 10) == 0) ? pointer_width : pointer_width / 2;

        window->draw_line(*style->fg_gc(get_state()), int(xc + c * (radius - tick_length)),
        int(yc - s * (radius - tick_length)), int(xc + c * radius), int(yc - s * radius));
    }

    // Draw pointer
    s = sin(angle);
    c = cos(angle);
    last_angle = angle;

    points[0].set(int(xc + s * pointer_width / 2), int(yc + c * pointer_width / 2));
    points[1].set(int(xc + c * radius), int(yc - s * radius));
    points[2].set(int(xc - s * pointer_width / 2), int(yc - c * pointer_width / 2));
    points[3].set(int(xc - c * radius / 10), int(yc + s * radius / 10));
    points[4].set(points[0].x(), points[0].y());

    style->draw_polygon(*window, STATE_NORMAL, SHADOW_OUT, points, 5, true, 0, this);
    return false;
}


bool
Gtk::Dial::on_button_press_event(const Gdk::EventButton& event)
{
    // 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.

    int dx = int(event.x()) - get_allocation().width() / 2;
    int dy = get_allocation().height() / 2 - int(event.y());

    double s = sin(angle);
    double c = cos(angle);
    double d_parallel = s * dy + c * dx;
    double d_perpendicular = fabs(s * dx - c * dy);

    if (!button && (d_perpendicular < pointer_width/2) && (d_parallel > - pointer_width))
    {
        Main::grab_add(*this);
        button = event.button();
        update_mouse(int(event.x()), int(event.y()));
    }
    return false;
}

bool
Gtk::Dial::on_button_release_event(const Gdk::EventButton& event)
{
    if (button == event.button())
    {
        Main::grab_remove(*this);
        button = 0;

        if (policy_ == UPDATE_DELAYED)
            timer.disconnect();

        if ((policy_ != UPDATE_CONTINUOUS) && (old_value != adjustment_->get_value()))
            adjustment_->value_changed();
    }
    return false;
}

bool
Gtk::Dial::on_motion_notify_event(const Gdk::EventMotion& event)
{
    Gtk::Widget::on_motion_notify_event(event);

    if (button != 0)
    {
       
        int x = int(event.x());
        int y = int(event.y());


        Gdk::ModifierTypeField mods;
        Gdk::Window *window = get_window();
        if (event.is_hint() || (event.window() != window))
            window->get_pointer(&x, &y, &mods);

        int mask;
        switch (button)
        {
        case 1:
            mask = GDK_BUTTON1_MASK;
            break;
        case 2:
            mask = GDK_BUTTON2_MASK;
            break;
        case 3:
            mask = GDK_BUTTON3_MASK;
            break;
        default:
            mask = 0;
            break;
        }

        if (mods & mask)
            update_mouse(x, y);
    }
    return false;
}

void
Gtk::Dial::set_update_policy(UpdateType policy)
{
    policy_ = policy;
}

void
Gtk::Dial::update()
{
    double value = adjustment_->get_value();
    double lower = adjustment_->lower();
    double upper = adjustment_->upper();

    double new_value = std::max(lower, value);
    new_value = std::min(new_value, upper);

    if (new_value != value)
    {
        adjustment_->set_value(new_value);
        adjustment_->value_changed();
    }

    angle = 7.*G_PI/6. - (new_value - lower) * 4.*G_PI/3. / (upper - lower);
    queue_draw();
}

void
Gtk::Dial::update_mouse(int x, int y)
{
    int xc = get_allocation().width() / 2;
    int yc = get_allocation().height() / 2;

    float old_value = adjustment_->get_value();
    angle = atan2(yc - y, x - xc);

    if (angle < -G_PI/2.)
        angle += 2*G_PI;

    if (angle < -G_PI/6)
        angle = -G_PI/6;

    if (angle > 7.*G_PI/6.)
        angle = 7.*G_PI/6.;

    double lower = adjustment_->lower();
    adjustment_->set_value(lower + (7.*G_PI/6 - angle) * (adjustment_->upper() - lower) / (4.*G_PI/3.));

    if (adjustment_->get_value() == old_value)
    {
        if (policy_ == UPDATE_CONTINUOUS)
        {
            adjustment_->value_changed();
        }
        else
        {
            queue_draw();
            if (policy_ == UPDATE_DELAYED)
            {
                timer = Main::timeout_signal.connect(slot(this, &Dial::on_timeout), scroll_delay_length);
            }
        }
    }
}

bool
Gtk::Dial::on_timeout()
{
    if (policy_ == UPDATE_DELAYED)
        adjustment_->value_changed();
    return false;
}

void
Gtk::Dial::on_adjustment_changed()
{
    double value = adjustment_->get_value();
    double lower = adjustment_->lower();
    double upper = adjustment_->upper();

    if ((old_value != value) || (old_lower != lower) || (old_upper != upper))
    {
        update();
        old_value = value;
        old_lower = lower;
        old_upper = upper;
    }
}

void
Gtk::Dial::on_adjustment_value_changed()
{
    double value = adjustment_->get_value();

    if (old_value != value)
    {
        update();
        old_value = value;
    }
}



The Realize Signal

Now we come to some new types of functions - signal handlers. First, we have a signal handler that does the work of creating the X window.

void
Gtk::Dial::on_realize()
{
    set_flags(REALIZED);

    Gdk::EventMaskField mask = get_events() | Gdk::EXPOSURE_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK;
    mask |= (Gdk::POINTER_MOTION_MASK | Gdk::POINTER_MOTION_HINT_MASK);
    Gdk::WindowAttr attributes(get_allocation(), Gdk::WINDOW_CHILD, mask);
    attributes.set_visual(*get_visual());
    attributes.set_colormap(*get_colormap());

    Gdk::Window *window = new Gdk::Window(*get_parent_window(), attributes);
    set_window(*window);
    get_style()->attach(*window);
    window->set_user_data(gtk_widget());
    get_style()->set_background(*window, STATE_ACTIVE);
}

First, on_realize() sets the Gtk::REALIZE flag and then sets up a mask specifying the events the Dial widget should receive. When setting the event mask you first call Gtk::Widget::get_events() to retrieve the event mask that the user has set for the widget (with Gtk::Widget::set_events()), and add then add the events we're interested in ourselves.

Gdk::WindowAttr specifies the creation parameters for the new window and is later passed to the Gdk::Window constructor.  Gdk::WindowAttr is a wrapper class for the GdkWindowAttr structure. It manages the GdkWindowAttributesType mask internally so you don't need to set this mask manually. Next, the visual and colormap the widget will use is set. After creating the new window the widget Style and background are set. Note the Gtk::Style::attach() method. This is the only time you should use this method, and there is no corresponding detach() method because GtkWidget will detach the Style for you when the widget is destroyed.

Also, you need to put a pointer to the widget in the user data field of the Gdk::Window. This last step allows GTK to dispatch events for this window to the correct widget.


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::on_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.

void
Gtk::Dial::on_size_request(Requisition *requisition)
{
    requisition->set(dial_default_size, dial_default_size);
}

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::on_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.

void
Gtk::Dial::on_size_allocate(const Allocation& allocation)
{
    set_allocation(allocation);

    if (is_realized())
    {
        get_window()->move_resize(allocation);
    }

    radius = (int)(std::min(allocation.width(), allocation.height()) * 0.45);
    pointer_width = radius / 5;
}


The Expose Event

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 Gdk::Drawable::draw_polygon to draw the pointer with three dimensional shading according to the colors stored in the widget's style.

bool
Gtk::Dial::on_expose_event(const Gdk::EventExpose& event)
{
    if (event.count() > 0)
        return false;

    Allocation allocation = get_allocation();
    int xc = allocation.width() / 2;
    int yc = allocation.height() / 2;

    int upper = (int)adjustment_->upper();
    int lower = (int)adjustment_->lower();

    double s = sin(last_angle);
    double c = cos(last_angle);
    last_angle = angle;

    Gdk::Point points[6];
    points[0].set(int(xc + s * pointer_width / 2), int(yc + c * pointer_width / 2));
    points[1].set(int(xc + c * radius), int(yc - s * radius));
    points[2].set(int(xc - s * pointer_width / 2), int(yc - c * pointer_width / 2));
    points[3].set(int(xc - c * radius / 10), int(yc + s * radius / 10));
    points[4].set(points[0].x(), points[0].y());

    Gdk::Window *window = get_window();
    window->invalidate(allocation, false);
    Gtk::Style *style = get_style();
    style->draw_polygon(*window, STATE_NORMAL, SHADOW_OUT, points, 5, false, 0, this);

    // Draw ticks
    int inc = upper - lower;
    if (inc == 0)
        return false;

    double increment = (100 * G_PI) / ( radius * radius);
    while (inc < 100) inc *= 10;
    while (inc >= 1000) inc /= 10;
    double last = -1;

    for (int i = 0; i <= inc; i++)
    {
        double theta = ((float)i * G_PI / (18 * inc / 24.) - G_PI / 6.);
        if ((theta - last) < (increment))
            continue;

        last = theta;
        s = sin(theta);
        c = cos(theta);

        int tick_length = (i % (inc / 10) == 0) ? pointer_width : pointer_width / 2;

        window->draw_line(*style->fg_gc(get_state()), int(xc + c * (radius - tick_length)),
        int(yc - s * (radius - tick_length)), int(xc + c * radius), int(yc - s * radius));
    }

    // Draw pointer
    s = sin(angle);
    c = cos(angle);
    last_angle = angle;

    points[0].set(int(xc + s * pointer_width / 2), int(yc + c * pointer_width / 2));
    points[1].set(int(xc + c * radius), int(yc - s * radius));
    points[2].set(int(xc - s * pointer_width / 2), int(yc - c * pointer_width / 2));
    points[3].set(int(xc - c * radius / 10), int(yc + s * radius / 10));
    points[4].set(points[0].x(), points[0].y());

    style->draw_polygon(*window, STATE_NORMAL, SHADOW_OUT, points, 5, true, 0, this);
    return false;
}


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 Inti 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, and grab all mouse events with a call to Main::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 the timeout_signal(Gtk::UPDATE_DELAYED), or only when the button is released (Gtk::UPDATE_DISCONTINUOUS).

bool
Gtk::Dial::on_button_press_event(const Gdk::EventButton& event)
{
    // 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.

    int dx = int(event.x()) - get_allocation().width() / 2;
    int dy = get_allocation().height() / 2 - int(event.y());

    double s = sin(angle);
    double c = cos(angle);
    double d_parallel = s * dy + c * dx;
    double d_perpendicular = fabs(s * dx - c * dy);

    if (!button && (d_perpendicular < pointer_width/2) && (d_parallel > - pointer_width))
    {
        Main::grab_add(*this);
        button = event.button();
        update_mouse(int(event.x()), int(event.y()));
    }
    return false;
}

bool
Gtk::Dial::on_button_release_event(const Gdk::EventButton& event)
{
    if (button == event.button())
    {
        Main::grab_remove(*this);
        button = 0;

        if (policy_ == UPDATE_DELAYED)
            timer.disconnect();

        if ((policy_ != UPDATE_CONTINUOUS) && (old_value != adjustment_->get_value()))
            adjustment_->value_changed();
    }
    return false;
}

bool
Gtk::Dial::on_motion_notify_event(const Gdk::EventMotion& event)
{
    Gtk::Widget::on_motion_notify_event(event);

    if (button != 0)
    {
       
        int x = int(event.x());
        int y = int(event.y());


        Gdk::ModifierTypeField mods;
        Gdk::Window *window = get_window();
        if (event.is_hint() || (event.window() != window))
            window->get_pointer(&x, &y, &mods);

        int mask;
        switch (button)
        {
        case 1:
            mask = GDK_BUTTON1_MASK;
            break;
        case 2:
            mask = GDK_BUTTON2_MASK;
            break;
        case 3:
            mask = GDK_BUTTON3_MASK;
            break;
        default:
            mask = 0;
            break;
        }

        if (mods & mask)
            update_mouse(x, y);
    }
    return false;
}

void
Gtk::Dial::update()
{
    double value = adjustment_->get_value();
    double lower = adjustment_->lower();
    double upper = adjustment_->upper();

    double new_value = std::max(lower, value);
    new_value = std::min(new_value, upper);

    if (new_value != value)
    {
        adjustment_->set_value(new_value);
        adjustment_->value_changed();
    }

    angle = 7.*G_PI/6. - (new_value - lower) * 4.*G_PI/3. / (upper - lower);
    queue_draw();
}

void
Gtk::Dial::update_mouse(int x, int y)
{
    int xc = get_allocation().width() / 2;
    int yc = get_allocation().height() / 2;

    float old_value = adjustment_->get_value();
    angle = atan2(yc - y, x - xc);

    if (angle < -G_PI/2.)
        angle += 2*G_PI;

    if (angle < -G_PI/6)
        angle = -G_PI/6;

    if (angle > 7.*G_PI/6.)
        angle = 7.*G_PI/6.;

    double lower = adjustment_->lower();
    adjustment_->set_value(lower + (7.*G_PI/6 - angle) * (adjustment_->upper() - lower) / (4.*G_PI/3.));

    if (adjustment_->get_value() == old_value)
    {
        if (policy_ == UPDATE_CONTINUOUS)
        {
            adjustment_->value_changed();
        }
        else
        {
            queue_draw();
            if (policy_ == UPDATE_DELAYED)
            {
                timer = Main::timeout_signal.connect(slot(this, &Dial::on_timeout), scroll_delay_length);
            }
        }
    }
}

bool
Gtk::Dial::on_timeout()
{
    if (policy_ == UPDATE_DELAYED)
        adjustment_->value_changed();
    return false;
}

void
Gtk::Dial::on_adjustment_changed()
{
    double value = adjustment_->get_value();
    double lower = adjustment_->lower();
    double upper = adjustment_->upper();

    if ((old_value != value) || (old_lower != lower) || (old_upper != upper))
    {
        update();
        old_value = value;
        old_lower = lower;
        old_upper = upper;
    }
}

void
Gtk::Dial::on_adjustment_value_changed()
{
    double value = adjustment_->get_value();

    if (old_value != value)
    {
        update();
        old_value = value;
    }
}


Possible Enhancements

The Dial widget as we've described it so far runs about 374 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 GtkRange 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.



« Creating a Composite Widget Index
Writing Your Own Widgets
Top
Building an GNU Autotools Project  »