The category of range widgets includes the ubiquitous scrollbar widget and the less common scale widget. Though these two types of widgets are typically used for vastly different purposes, they are quite similar in function and implementation. Range widgets allow the user to visually manipulate a value within a specified range (hence the name).
All range widgets share a set of common graphic elements, each of which has its own X window and receives events. They all contain a "trough" and a "slider" (what is sometimes called a "thumbwheel" in other GUI environments). Dragging the slider with the pointer moves it back and forth within the trough, while clicking in the trough advances the slider towards the location of the click, either completely, or by a designated amount (called a "page"), depending on which button was used.
Scale widgets are used to set an explicitly numeric parameter which has a visual correlate, and which the user might be expected to adjust primarily by sight. For example, the GtkColorSelection compound widget contains scale widgets which control the components of the colour being selected. Typically, the precise value of the number is less important here than its side-effects, and thus the user should be spared the effort of reaching for the keyboard.
There are actually two types of scale widget: GtkHScale widgets, which are horizontal, and GtkVScale widgets, which
are vertical. (Most programmers seem to favour horizontal
scale widgets). Since they work essentially the same way,
there's no need to treat them separately here. The
following functions, defined in
<gtk/gtkvscale.h>
and
<gtk/gtkhscale.h>
, create vertical and
horizontal scale widgets, respectively:
GtkWidget* gtk_vscale_new( GtkAdjustment *adjustment );
GtkWidget* gtk_hscale_new( GtkAdjustment *adjustment );
adjustment
can either be an adjustment which has
already been created with gtk_adjustment_new()
, or
NULL
, in which case, an anonymous GtkAdjustment is
created with all of its values set to 0.0
. If you're
thoroughly confused by now, see
The Adjustment Object
below for an explanation of what exactly the adjustment
argument does and how to create and manipulate it.
Scale widgets can display their current value as a number beside the trough. The default behaviour is to show the value, but you can change this with this function:
void gtk_scale_set_draw_value( GtkScale *scale,
gint draw_value );
As you might have guessed, draw_value
is either
TRUE
or FALSE
, with predictable consequences for
either one.
The value displayed by a scale widget is rounded to one
decimal point by default (as is the value
field in its
GtkAdjustment... but I digress). You can change this with:
void gtk_scale_set_digits( GtkScale *scale,
gint digits);
where digits
is the number of decimal places you want.
You can set digits
to anything you like, but no more
than 13 decimal places will actually be drawn on screen.
This probably isn't too horribly restrictive.
Finally, the value can be drawn in different positions relative to the trough:
void gtk_scale_set_value_pos( GtkScale *scale,
GtkPositionType pos );
If you've read the section on the notebook widget, then you
know what the possible values of pos
are. They are
defined as type GtkPositionType
and can take one
of the following values:
If you position the value on the "side" of the trough (e.g. on the top or bottom of a horizontal scale widget), then it will follow the slider up and down the trough.
All the preceding functions are defined in
<gtk/gtkscale.h>
. The other signals and
functions defined in the header files for the scale widgets
are either not useful for anyone other than writers of scale
widgets, or are the standard GTK+ type-casting macros and
functions.
These are your standard, run-of-the-mill scrollbars. As with the scale widgets, there are separate types for horizontal and vertical scrollbars. There really isn't much to say about these. You create them with the following functions:
GtkWidget* gtk_hscrollbar_new( GtkAdjustment *adjustment );
GtkWidget* gtk_vscrollbar_new( GtkAdjustment *adjustment );
and that's about it (if you don't believe me, look in the
header files!). Again, adjustment
can either be a
pointer to an existing GtkAdjustment, or NULL, in which case
one will be created for you.
As you might have noticed, there really isn't much to the
various range widgets themselves from the programmer's point
of view. Most of your program's interaction with these
widgets will take place by way of the heretofore mysterious
adjustment
object.
Every range widget contains a pointer to a GtkAdjustment
object. You'll usually create one of these in order to pass
it to the gtk_*_new()
function which creates a range
widget, or some compound widget that uses range widgets, such
as GtkScrolledWindow or GtkCList.
Aside from specifying some characteristics related to the range widget's appearance and behaviour, the GtkAdjustment you pass to this function becomes "attached" to the newly-created range widget and from that point on will always contain the numerical value corresponding to the position of the slider (unless, at some point in the future, you set a new adjustment for the range widget).
One adjustment object can be shared between many range widgets. Reusing the same adjustment object across several range widgets will cause them all to change when one of them is changed.
You create an adjustment using:
GtkObject* gtk_adjustment_new( gfloat value,
gfloat lower,
gfloat upper,
gfloat step_increment,
gfloat page_increment,
gfloat page_size );
It may or may not be obvious by now that the values given to
gtk_adjustment_new()
are simply arbitrary floating-point
values. The mapping between these values and the on-screen
size of the range widget and its constituent parts is
handled by the range widget. Thus, you're free to use
whatever numbers are most meaningful to your program.
The value
argument is the initial value you want to
give to the adjustment. The lower
argument specifies
the lowest value which the adjustment can hold, or, in other
words, the lowest value which the user can select using the
range widget which uses this adjustment. The
step_increment
argument specifies the "smaller" of the
two increments by which the user can change the value, while
the page_increment
is the "larger" one.
Key and Mouse Bindings below
describes the default key and mouse bindings for range
widgets, and how they relate to these increments. The
page_size
argument is only relevant for scrollbars.
Its most obvious effect is that it determines the size of
the slider; however, you should set it based on the "size"
of the visible area of whatever you're scrolling.
As an example, say you're writing a text editor. You might
want to have the value of the vertical scrollbar beside the
editing area correspond to the line number
of the first visible line in the editing area. In that
case, you might call gtk_adjustment_new()
like this:
GtkObject *adj;
adj = gtk_adjustment_new (0, first_line, last_line, 1,
window_height - 2, window_height);
where window_height
is the number of visible lines in
the window.
Finally, with regard to the upper
argument to
gtk_adjustment_new
, you'll notice that, since the value
of the adjustment corresponds to the first visible line
in the window, the maximum value in the adjustment is not
actually last_line
, but rather last_line -
window_height
(or, in more general terms, upper -
page_height
). This is a little confusing at first, but
it makes sense if you think about it in terms of what the
user expects to see in a scrolled window when the
scrollbar's slider is moved all the way to the end of the
trough.
Since the size of the slider on scale widgets is invariable,
to avoid excessive confusion, it's a good idea to set the
page_size
to 0.0
for adjustments that are only
going to be used for scale widgets.
OK, you say, that's nice, but how do I get at all these
values, and, more importantly, how do I know when the user
has moved the slider around? To answer these questions and
more, let's start by taking a look at struct _GtkAdjustment
itself:
struct _GtkAdjustment
{
GtkData data;
gfloat lower;
gfloat upper;
gfloat value;
gfloat step_increment;
gfloat page_increment;
gfloat page_size;
};
struct _GtkAdjustmentClass
{
GtkDataClass parent_class;
void (* changed) (GtkAdjustment *adjustment);
void (* value_changed) (GtkAdjustment *adjustment);
};
The first thing you should know is that there aren't any
handy-dandy macros or accessor functions for getting the
value
out of a GtkAdjustment, so you'll have to (horror
of horrors) do it like a real C programmer. Don't
worry - the GTK_ADJUSTMENT (Object)
macro does
run-time type checking (as do all the GTK+ type-casting
macros, actually). On the other hand, unless you're writing
a new type of range widget, you probably don't want to
set these fields directly. To set value
, you can
use:
void gtk_adjustment_set_value( GtkAdjustment *adjustment,
gfloat value );
If you need to change the other fields, and you don't intend
to do this very frequently, it's best to create a new
GtkAdjustment and set it with
gtk_range_set_adjustment()
, as detailed in
Common Functions, Signals, and Macros below.
You might have noticed that, while adjustments are not widgets, they are still a "subclass" of GtkObject. Therefore, they can (and do) emit signals of their own.
The various widgets that use the GtkAdjustment object will
emit the "value_changed" signal on an adjustment whenever
they change its value (see
Update Policies below for more detail). This
happens both when user input causes the slider to move on a
range widget, as well as when the program explicitly changes
the value with gtk_adjustment_set_value()
. So, for
example, if you have a scale widget, and you want to change
the rotation of a picture whenever its value changes, you
would create a callback like this:
void cb_rotate_picture (GtkAdjustment *adj, GtkWidget *picture)
{
set_picture_rotation (picture, adj->value);
...
and connect it to the scale widget's adjustment like this:
gtk_signal_connect (GTK_OBJECT (adj), "value_changed",
GTK_SIGNAL_FUNC (cb_rotate_picture), picture);
The "changed" signal is somewhat more elusive. It is never
emitted directly due to the user's actions. Rather,
programs or other widgets should emit it on a GtkAdjustment
when they modify any of its fields directly. This will
force any range widgets that use this adjustment to
recalculate and redraw if necessary. This is useful if you
have a number of range widgets using the same GtkAdjustment,
and don't want to call gtk_range_set_adjustment()
for
all of them. It's also handy if you are going to be
continuously changing these values, such as in our
hypothetical text editor, where the upper
field will
have to change every time a new line is added, and you don't
want the extra overhead of creating a new GtkAdjustment
object every time.
The GtkRange widget class is fairly complicated internally, but, like all the "base class" widgets, most of its complexity is only interesting if you want to hack on it. Also, almost all of the functions and signals it defines are only really used in writing derived widgets. There are, however, a few useful functions and concepts that are defined in gtkrange.h and are common to all range widgets.
The "update policy" of a range widget defines at what points
during user interaction it will change the value
field
of its GtkAdjustment and emit the "value_changed" signal on
this GtkAdjustment. The update policies, defined in
<gtk/gtkenums.h>
as the enum
GtkUpdateType
, are:
The update policy of a range widget can be set by casting it
using the GTK_RANGE (Widget)
macro and passing it
to this function:
void gtk_range_set_update_policy( GtkRange *range,
GtkUpdateType policy );
Getting and setting the adjustment for a range widget "on the fly" is done, predictably, with:
GtkAdjustment* gtk_range_get_adjustment( GtkRange *range );
void gtk_range_set_adjustment( GtkRange *range,
GtkAdjustment *adjustment );
gtk_range_get_adjustment()
returns a pointer to the
adjustment to which range
is connected.
gtk_range_set_adjustment()
does absolutely nothing if
you pass it the adjustment that range
is already using,
regardless of whether you changed any of its fields or not.
If you pass it a new GtkAdjustment, it will unreference the
old one if it exists (possibly destroying it), connect the
appropriate signals to the new one, and call the private
function gtk_range_adjustment_changed()
, which will (or
at least, is supposed to...) recalculate the size and/or
position of the slider and redraw if necessary. As
mentioned above, if you wish to reuse the same
GtkAdjustment, when you modify its values directly, you
should emit the "changed" signal on it, like this:
gtk_signal_emit_by_name (GTK_OBJECT (adjustment), "changed");
All of the GTK+ range widgets react to mouse clicks in more
or less the same way. Clicking button 1 in the trough will
cause its adjustment's page_increment
to be added or
subtracted from its value
, and the slider to be moved
accordingly. Clicking button 2 in the trough will jump the
slider to the point at which the button was clicked.
Clicking any button on a scrollbar's arrows will cause its
adjustment's value to change step_increment
at a time.
The key bindings, by contrast, are slightly different between horizontal and vertical range widgets, for obvious reasons. They are also not quite the same for scale widgets as they are for scrollbars, for somewhat less obvious reasons (possibly to avoid confusion between the keys for horizontal and vertical scrollbars in scrolled windows, where both operate on the same area).
All vertical range widgets can be operated with the up and
down arrow keys, as well as with the Page Up
and
Page Down
keys. The arrows move the slider up and
down by step_increment
, while Page Up
and
Page Down
move it by page_increment
.
The user can also move the slider all the way to one end
or the other of the trough using the keyboard. With the
GtkVScale widget, this is done with the Home
and
End
keys, whereas with the GtkVScrollbar widget, this
is done by typing Control-Page Up
and
Control-Page Down
.
The left and right arrow keys work as you might expect in
these widgets, moving the slider back and forth by
step_increment
. The Home
and End
keys move
the slider to the ends of the trough. For the GtkHScale
widget, moving the slider by page_increment
is
accomplished with Control-Left
and
Control-Right
, while for GtkHScrollbar, it's done
with Control-Home
and Control-End
.
This example is a somewhat modified version of the "range
widgets" test from testgtk.c
. It basically puts up a
window with three range widgets all connected to the same
adjustment, and a couple of controls for adjusting some of the
parameters for scale widgets mentioned above, so you can see
how they affect the way these widgets work for the user.
/* example-start rangewidgets rangewidgets.c */
#include <gtk/gtk.h>
GtkWidget *hscale, *vscale;
void cb_pos_menu_select (GtkWidget *item, GtkPositionType pos)
{
/* set the value position on both scale widgets */
gtk_scale_set_value_pos (GTK_SCALE (hscale), pos);
gtk_scale_set_value_pos (GTK_SCALE (vscale), pos);
}
void cb_update_menu_select (GtkWidget *item, GtkUpdateType policy)
{
/* set the update policy for both scale widgets */
gtk_range_set_update_policy (GTK_RANGE (hscale), policy);
gtk_range_set_update_policy (GTK_RANGE (vscale), policy);
}
void cb_digits_scale (GtkAdjustment *adj)
{
/* set the number of decimal places to which adj->vaule is rounded
*/
gtk_scale_set_digits (GTK_SCALE (hscale), (gint) adj->value);
gtk_scale_set_digits (GTK_SCALE (vscale), (gint) adj->value);
}
void cb_page_size (GtkAdjustment *get, GtkAdjustment *set)
{
/* set the page size and page increment size of the sample
adjustment to the value specified by the "Page Size" scale */
set->page_size = get->value;
set->page_increment = get->value;
/* now emit the "changed" signal to reconfigure all the widgets that
are attached to this adjustment */
gtk_signal_emit_by_name (GTK_OBJECT (set), "changed");
}
void cb_draw_value (GtkToggleButton *button)
{
/* turn the value display on the scale widgets off or on depending
on the state of the checkbutton */
gtk_scale_set_draw_value (GTK_SCALE (hscale), button->active);
gtk_scale_set_draw_value (GTK_SCALE (vscale), button->active);
}
/* convenience functions */
GtkWidget *make_menu_item (gchar *name, GtkSignalFunc callback,
gpointer data)
{
GtkWidget *item;
item = gtk_menu_item_new_with_label (name);
gtk_signal_connect (GTK_OBJECT (item), "activate",
callback, data);
gtk_widget_show (item);
return item;
}
void scale_set_default_values (GtkScale *scale)
{
gtk_range_set_update_policy (GTK_RANGE (scale),
GTK_UPDATE_CONTINUOUS);
gtk_scale_set_digits (scale, 1);
gtk_scale_set_value_pos (scale, GTK_POS_TOP);
gtk_scale_set_draw_value (scale, TRUE);
}
/* makes the sample window */
void create_range_controls (void)
{
GtkWidget *window;
GtkWidget *box1, *box2, *box3;
GtkWidget *button;
GtkWidget *scrollbar;
GtkWidget *separator;
GtkWidget *opt, *menu, *item;
GtkWidget *label;
GtkWidget *scale;
GtkObject *adj1, *adj2;
/* standard window-creating stuff */
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_signal_connect (GTK_OBJECT (window), "destroy",
GTK_SIGNAL_FUNC(gtk_main_quit),
NULL);
gtk_window_set_title (GTK_WINDOW (window), "range controls");
box1 = gtk_vbox_new (FALSE, 0);
gtk_container_add (GTK_CONTAINER (window), box1);
gtk_widget_show (box1);
box2 = gtk_hbox_new (FALSE, 10);
gtk_container_border_width (GTK_CONTAINER (box2), 10);
gtk_box_pack_start (GTK_BOX (box1), box2, TRUE, TRUE, 0);
gtk_widget_show (box2);
/* value, lower, upper, step_increment, page_increment, page_size */
/* note that the page_size value only makes a difference for
scrollbar widgets, and the highest value you'll get is actually
(upper - page_size). */
adj1 = gtk_adjustment_new (0.0, 0.0, 101.0, 0.1, 1.0, 1.0);
vscale = gtk_vscale_new (GTK_ADJUSTMENT (adj1));
scale_set_default_values (GTK_SCALE (vscale));
gtk_box_pack_start (GTK_BOX (box2), vscale, TRUE, TRUE, 0);
gtk_widget_show (vscale);
box3 = gtk_vbox_new (FALSE, 10);
gtk_box_pack_start (GTK_BOX (box2), box3, TRUE, TRUE, 0);
gtk_widget_show (box3);
/* reuse the same adjustment */
hscale = gtk_hscale_new (GTK_ADJUSTMENT (adj1));
gtk_widget_set_usize (GTK_WIDGET (hscale), 200, 30);
scale_set_default_values (GTK_SCALE (hscale));
gtk_box_pack_start (GTK_BOX (box3), hscale, TRUE, TRUE, 0);
gtk_widget_show (hscale);
/* reuse the same adjustment again */
scrollbar = gtk_hscrollbar_new (GTK_ADJUSTMENT (adj1));
/* notice how this causes the scales to always be updated
continuously when the scrollbar is moved */
gtk_range_set_update_policy (GTK_RANGE (scrollbar),
GTK_UPDATE_CONTINUOUS);
gtk_box_pack_start (GTK_BOX (box3), scrollbar, TRUE, TRUE, 0);
gtk_widget_show (scrollbar);
box2 = gtk_hbox_new (FALSE, 10);
gtk_container_border_width (GTK_CONTAINER (box2), 10);
gtk_box_pack_start (GTK_BOX (box1), box2, TRUE, TRUE, 0);
gtk_widget_show (box2);
/* a checkbutton to control whether the value is displayed or not */
button = gtk_check_button_new_with_label
("Display value on scale widgets");
gtk_toggle_button_set_state (GTK_TOGGLE_BUTTON (button), TRUE);
gtk_signal_connect (GTK_OBJECT (button), "toggled", GTK_SIGNAL_FUNC
(cb_draw_value), NULL);
gtk_box_pack_start (GTK_BOX (box2), button, TRUE, TRUE, 0);
gtk_widget_show (button);
box2 = gtk_hbox_new (FALSE, 10);
gtk_container_border_width (GTK_CONTAINER (box2), 10);
/* an option menu to change the position of the value */
label = gtk_label_new ("Scale Value Position:");
gtk_box_pack_start (GTK_BOX (box2), label, FALSE, FALSE, 0);
gtk_widget_show (label);
opt = gtk_option_menu_new();
menu = gtk_menu_new();
item = make_menu_item ("Top", GTK_SIGNAL_FUNC (cb_pos_menu_select),
GINT_TO_POINTER (GTK_POS_TOP));
gtk_menu_append (GTK_MENU (menu), item);
item = make_menu_item ("Bottom", GTK_SIGNAL_FUNC (cb_pos_menu_select),
GINT_TO_POINTER (GTK_POS_BOTTOM));
gtk_menu_append (GTK_MENU (menu), item);
item = make_menu_item ("Left", GTK_SIGNAL_FUNC (cb_pos_menu_select),
GINT_TO_POINTER (GTK_POS_LEFT));
gtk_menu_append (GTK_MENU (menu), item);
item = make_menu_item ("Right", GTK_SIGNAL_FUNC (cb_pos_menu_select),
GINT_TO_POINTER (GTK_POS_RIGHT));
gtk_menu_append (GTK_MENU (menu), item);
gtk_option_menu_set_menu (GTK_OPTION_MENU (opt), menu);
gtk_box_pack_start (GTK_BOX (box2), opt, TRUE, TRUE, 0);
gtk_widget_show (opt);
gtk_box_pack_start (GTK_BOX (box1), box2, TRUE, TRUE, 0);
gtk_widget_show (box2);
box2 = gtk_hbox_new (FALSE, 10);
gtk_container_border_width (GTK_CONTAINER (box2), 10);
/* yet another option menu, this time for the update policy of the
scale widgets */
label = gtk_label_new ("Scale Update Policy:");
gtk_box_pack_start (GTK_BOX (box2), label, FALSE, FALSE, 0);
gtk_widget_show (label);
opt = gtk_option_menu_new();
menu = gtk_menu_new();
item = make_menu_item ("Continuous",
GTK_SIGNAL_FUNC (cb_update_menu_select),
GINT_TO_POINTER (GTK_UPDATE_CONTINUOUS));
gtk_menu_append (GTK_MENU (menu), item);
item = make_menu_item ("Discontinuous",
GTK_SIGNAL_FUNC (cb_update_menu_select),
GINT_TO_POINTER (GTK_UPDATE_DISCONTINUOUS));
gtk_menu_append (GTK_MENU (menu), item);
item = make_menu_item ("Delayed",
GTK_SIGNAL_FUNC (cb_update_menu_select),
GINT_TO_POINTER (GTK_UPDATE_DELAYED));
gtk_menu_append (GTK_MENU (menu), item);
gtk_option_menu_set_menu (GTK_OPTION_MENU (opt), menu);
gtk_box_pack_start (GTK_BOX (box2), opt, TRUE, TRUE, 0);
gtk_widget_show (opt);
gtk_box_pack_start (GTK_BOX (box1), box2, TRUE, TRUE, 0);
gtk_widget_show (box2);
box2 = gtk_hbox_new (FALSE, 10);
gtk_container_border_width (GTK_CONTAINER (box2), 10);
/* a GtkHScale widget for adjusting the number of digits on the
sample scales. */
label = gtk_label_new ("Scale Digits:");
gtk_box_pack_start (GTK_BOX (box2), label, FALSE, FALSE, 0);
gtk_widget_show (label);
adj2 = gtk_adjustment_new (1.0, 0.0, 5.0, 1.0, 1.0, 0.0);
gtk_signal_connect (GTK_OBJECT (adj2), "value_changed",
GTK_SIGNAL_FUNC (cb_digits_scale), NULL);
scale = gtk_hscale_new (GTK_ADJUSTMENT (adj2));
gtk_scale_set_digits (GTK_SCALE (scale), 0);
gtk_box_pack_start (GTK_BOX (box2), scale, TRUE, TRUE, 0);
gtk_widget_show (scale);
gtk_box_pack_start (GTK_BOX (box1), box2, TRUE, TRUE, 0);
gtk_widget_show (box2);
box2 = gtk_hbox_new (FALSE, 10);
gtk_container_border_width (GTK_CONTAINER (box2), 10);
/* And, one last GtkHScale widget for adjusting the page size of the
scrollbar. */
label = gtk_label_new ("Scrollbar Page Size:");
gtk_box_pack_start (GTK_BOX (box2), label, FALSE, FALSE, 0);
gtk_widget_show (label);
adj2 = gtk_adjustment_new (1.0, 1.0, 101.0, 1.0, 1.0, 0.0);
gtk_signal_connect (GTK_OBJECT (adj2), "value_changed",
GTK_SIGNAL_FUNC (cb_page_size), adj1);
scale = gtk_hscale_new (GTK_ADJUSTMENT (adj2));
gtk_scale_set_digits (GTK_SCALE (scale), 0);
gtk_box_pack_start (GTK_BOX (box2), scale, TRUE, TRUE, 0);
gtk_widget_show (scale);
gtk_box_pack_start (GTK_BOX (box1), box2, TRUE, TRUE, 0);
gtk_widget_show (box2);
separator = gtk_hseparator_new ();
gtk_box_pack_start (GTK_BOX (box1), separator, FALSE, TRUE, 0);
gtk_widget_show (separator);
box2 = gtk_vbox_new (FALSE, 10);
gtk_container_border_width (GTK_CONTAINER (box2), 10);
gtk_box_pack_start (GTK_BOX (box1), box2, FALSE, TRUE, 0);
gtk_widget_show (box2);
button = gtk_button_new_with_label ("Quit");
gtk_signal_connect_object (GTK_OBJECT (button), "clicked",
GTK_SIGNAL_FUNC(gtk_main_quit),
NULL);
gtk_box_pack_start (GTK_BOX (box2), button, TRUE, TRUE, 0);
GTK_WIDGET_SET_FLAGS (button, GTK_CAN_DEFAULT);
gtk_widget_grab_default (button);
gtk_widget_show (button);
gtk_widget_show (window);
}
int main (int argc, char *argv[])
{
gtk_init(&argc, &argv);
create_range_controls();
gtk_main();
return 0;
}
/* example-end */