Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / multimedia / GDI+

Anatomy of a UserControl (SliderControl)

4.87/5 (41 votes)
5 Oct 2013CPOL15 min read 57.6K   3.3K  
This article presents a step-by-step guide for the implementation of a UserControl named SliderControl.

Introduction [^]

Slider Control

This article presents a step-by-step guide for the implementation of a UserControl named SliderControl.

In the following discussions, properties that are specified by the developer are displayed in BoldMixedCase text. Variables, used internally by the software are displayed in italicized_lowercase text.

User Perspective

The control allows the user to drag the arrow (to the right of the tube) and move it up or down. Vertical motion is limited to the top and the bottom of the tube. The arrow's fill color changes so as to match the color in the tube where the arrow is pointing. The text to the right of the arrow changes to indicate the current value as the user moves the arrow.

Developer Perspective

To the developer, the slider control can be incorporated into the Visual Studio ToolBox and placed on the surface of a Windows Form. The most important property of the control is this.Height, known internally as control_height. All other dimensions are dervived from this dimension.

The event SliderValueChanged is raised whenever the user changes the current value. Note that this is not the same as detecting the movement of the arrow. current_value is an integer. So small changes in the position of the arrow may result in a fractional value. That fractional value is rounded to the nearest integer (using Round half up) and is compared against the earlier current_value. If, and only if, they differ, is the SliderValueChanged event raised. The new current_value is returned in the SliderValueEventArgs.

Developing the control

Throughout the process of developing a control, it is crucial to follow some form of requirements. In the case of SliderControl, this was accomplished though a Microsoft PowerPoint presentation. Slides from that presentation are incorporated as PNG images in this article. The complete presentation is included as a downloadable file.

If the reader does not have Microsoft PowerPoint installed, a free PowerPoint Viewer, is available for download.

Table of Contents

The symbol [^] returns the reader to the top of the Table of Contents.

Visual Properties [^]

Slider Control Properties

The slider control has a number of properties that form the user's visual image. In the drawing to the right, the control's boundary is drawn in red.

The developer specifies the control's top-left corner by dragging the control from the ToolBox to a position on the form. This position becomes ( 0, 0 ) in the control's graphic environment. The developer specifies the control_height by dragging the control's resizing handles. The control_width is computed as:

control_width = ( int ) ( ( float ) control_height / 4.0F + 0.5F );

Note that the control_width is determined by the control_height. The only way to change both is to use the resizing handles.

There is a border between the control's border and each of the control's components. This border is computed from control_height as:

offset = ( int ) ( ( float ) control_height / 100.0F + 0.5F );

Once offset has been computed, tube_height can be computed as:

tube_height = control_height - 2 * offset;

The width of the tube may be either specified or computed.

  1. Specified.

    For the purpose of a common visual presentation with multiple slider controls on a form, the developer may wish to bypass the computation of tube_width (such as in the Slider Control Demo). This is accomplished by setting ForceTubeWidth to true and supplying a value for TubeWidth. Specifying a value for TubeWidth is ignored if ForceTubeWidth is not set to true.

  2. Computed.

    If ForceTubeWidth is not set to true, the value of tube_width is computed. To compute the tube_width, the constant HEIGHT_TO_WIDTH is used

    HEIGHT_TO_WIDTH = 0.030612244897959;

    and tube_width is computed as:

    tube_width = ( int ) ( ( double ) tube_height * HEIGHT_TO_WIDTH + 0.5 );

The tube is colored using a linear gradient brush using a color blend specified by MaximumColor, MidpointColor, and MinimumColor.

To the left of the colored tube, is a vertical group of labels, ranging from the MaximumValue to the MinimumValue. Label values are separated by Increment. The labels are separated from the control's left boundary and from the colored tube by offset. The MaximumValue must be greater than MinimumValue and MinimumValue must be greater than or equal to zero.

Implementation [^]

Slider Control Components

The slider control is a User-Drawn Control. Parts of the control are drawn when the control's OnPaint method is invoked.

The control's graphic, whose outine is drawn in red, is made up of two distinct graphic images: the background graphic, outlined in blue and filled with a blue pattern and the indicator graphic, outlined in green and filled with a green pattern.

Once drawn, the background graphic does not need to be redrawn unless ControlBackgroundColor, ForceTubeWidth and TubeWidth, MaximumColor, MidpointColor, MinimumColor, MaximumValue, MinimumValue, or Increment are changed or the control is resized. The indicator graphic must be redrawn when either the background graphic is redrawn or when the user changes the position of the current value arrow.

Background Graphic [^]

Slider Control Background Graphic

The background graphic is made up of the colored tube, and the group of labels to the left of the tube. The tube, itself, is made up of two, equal sized, end point circles joined by a rectangle.

Note that end point circles' radii are tube_width / 2. Once drawn, the end point circles are filled with the MaximumColor and MinimumColor, appropriately.

The rectangle is filled with a linear gradient brush defined using a color blend from MaximumColor to MinimumColor passing through the MidpointColor.

Labels are derived from the values of MaximumValue, MinimumValue, and Increment. Once the labels are generated, label_size, the maximum size of all the labels, is computed using MeasureText.

There are five points P0, P1, P2, P3, and P4 that are fixed, based upon property values. These points fully define the contents of the background graphic. Recall from earlier, the origin of all graphic objects is the control's top left corner.

P0   Upper left corner of the tube and the upper left corner of the bounding rectangle for the MaximumValue end point circle.
P1   Bottom left corner of the tube.
P2   Upper center of the colored rectangle.
P3   Lower center of the colored rectangle.
P4   Upper left corner of the bounding rectangle for the MinimumValue end point circle.

The most important of these points is P0, since all the others are derived from it. The sequence that is used to compute these values is:

  1. Update the tube dimensions.
  2. Update the background labels.
  3. Update the background geometry.

Although slightly misnamed, update_tube_dimensions is:

C#
// ************************************ update_tube_dimensions

void update_tube_dimensions ( )
    {

    offset = round ( ( float ) control_height / 100.0F );

    tube_height = control_height - 2 * offset;
    if ( !force_tube_width )
        {
        tube_width = round ( ( double ) tube_height *
                             HEIGHT_TO_WIDTH );
        }
    arrow_width = round ( 1.5 * ( double ) tube_width );
    }

The helper function round returns the integer value of its argument, rounded to the nearest integer, using Round half up. round is overloaded to accept arguments of float and double.

The background labels must be updated when one of the properties that define the background changes.

C#
// ********************************** update_background_labels

void update_background_labels ( )
    {
    int     available_height = 0;
    float   font_size = FONT_SIZE;
    Font    new_font = new Font ( FONT_FAMILY, FONT_SIZE );
    int     needed_height = 0;

    new_font = new Font ( FONT_FAMILY, font_size );
    labels = create_background_labels ( MaximumValue,
                                        MinimumValue,
                                        Increment);
    label_size = determine_maximum_label_size ( labels,
                                                new_font );
                                // force vertical fit
    update_tube_dimensions ( );
    available_height = control_height -
                       ( 2 * offset ) - tube_width;
    needed_height = labels.Length * label_size.Height;
    while ( needed_height > available_height )
        {
        font_size -= 0.1F;
        new_font = new Font ( FONT_FAMILY, font_size );
        label_size = determine_maximum_label_size (
                                                labels,
                                                new_font );
        needed_height = labels.Length * label_size.Height;
        }
    label_font = new_font;
    }

The two helper functions create_background_labels and determine_maximum_label_size perform the functions that their names imply. Once that the labels have been created and initially sized, update_background_labels continues testing to insure that the labels will fit vertically within the available_height. If the labels will not fit, the size of the font is reduced until the labels will fit,

Finally, the points P0, P1, P2, P3, and P4 are computed.

C#
// ******************************** update_background_geometry

void update_background_geometry ( )
    {

    update_tube_dimensions ( );

    P0.X = offset + label_size.Width + offset;
    P0.Y = offset;

    P1.X = P0.X;
    P1.Y = control_height - offset;

    P2.X = P0.X + round ( ( double ) tube_width / 2.0 );
    P2.Y = P0.Y + round ( ( double ) tube_width / 2.0 );

    P3.X = P2.X;
    P3.Y = P1.Y - round ( ( double ) tube_width / 2.0 );

    P4.X = P0.X;
    P4.Y = P1.Y - tube_width;
    }

At this point, the geometry of the background graphic is defined. The sequence used to draw the background is:

  1. Fill the control with the ControlBackgroundColor.
  2. Draw the background labels.
  3. Draw the end point circles (MaximumValue then MinimumValue).
  4. Draw the colored tube.

This sequence is found in draw_background_graphic:

C#
// *********************************** draw_background_graphic

void draw_background_graphic ( Graphics  graphics )
    {
    Brush               brush;
    Rectangle           end_point_rectangle =
                            new Rectangle ( );
    LinearGradientBrush linear_gradient_brush;
    Rectangle           rectangle;

                            // background labels
    draw_background_labels ( graphics );
                            // endpoints
    end_point_rectangle.Size = new Size ( tube_width,
                                          tube_width );
                            // maximum endpoint
    brush = new SolidBrush ( MaximumColor );
    end_point_rectangle.Location = P0;
    graphics.FillEllipse ( brush, end_point_rectangle );
    brush.Dispose ( );
                            // minimum endpoint
    brush = new SolidBrush ( MinimumColor );
    end_point_rectangle.Location = P4;
    graphics.FillEllipse ( brush, end_point_rectangle );
    brush.Dispose ( );
                            // gradient tube
    rectangle = new Rectangle ( new Point ( P0.X, P2.Y ),
                                new Size ( tube_width,
                                           P3.Y - P2.Y ) );
                            // inflate to account for the
                            // right and bottom off by one
                            // value in Rectangles
    rectangle.Inflate ( 1, 1 );
    linear_gradient_brush = create_linear_gradient_brush (
                                    MaximumColor,
                                    MidpointColor,
                                    MinimumColor );
    graphics.FillRectangle ( linear_gradient_brush,
                             rectangle );
    linear_gradient_brush.Dispose ( );
    }

The helper function draw_background_labels is

C#
// ************************************ draw_background_labels

void draw_background_labels ( Graphics graphics )
    {
    Point           location = new Point ( 0, 0 );
    TextFormatFlags text_format_flags;
    int             vertical_offset = 0;

    text_format_flags = ( TextFormatFlags.Right |
                          TextFormatFlags.VerticalCenter );

    vertical_offset = ( P3.Y - P2.Y ) / ( labels.Length - 1 );

    location.X = P0.X - ( offset + label_size.Width );
    location.Y = P2.Y - ( label_size.Height / 2 );

    foreach ( string label in labels )
        {
        Rectangle   rectangle = new Rectangle ( location,
                                                label_size );
        TextRenderer.DrawText ( graphics,
                                label,
                                label_font,
                                rectangle,
                                Color.Black,
                                text_format_flags );
        location.Y += vertical_offset;
        }
    }

The helper function create_linear_gradient_brush is

C#
// ****************************** create_linear_gradient_brush

LinearGradientBrush create_linear_gradient_brush (
                                    Color  maximum_color,
                                    Color  midpoint_color,
                                    Color  minimum_color )
    {
    LinearGradientBrush  brush;
    ColorBlend           color_blend = new ColorBlend ( );
    Rectangle            rectangle;

    rectangle = new Rectangle ( new Point ( P0.X, P2.Y ),
                                new Size ( tube_width,
                                           tube_height ) );
    rectangle.Inflate ( 1, 1 );
    brush = new LinearGradientBrush (
                            this.ClientRectangle,
                            maximum_color,
                            minimum_color,
                            LinearGradientMode.Vertical );

    color_blend.Positions = new float [ ]
                                {
                                0.0F,
                                0.5F,
                                1.0F
                                };

    color_blend.Colors = new Color [ ]
                            {
                            maximum_color,
                            midpoint_color,
                            minimum_color
                            };

    brush.InterpolationColors = color_blend;

    return ( brush );
    }

At this point, the background graphic has been drawn and may be transferred into the Graphics object supplied in the OnPaint PaintEventArgs. Until a background graphic property changes, we do not need to perform the background graphic computations.

Indicator Graphic [^]

Slider Control Indicator Graphic

The indicator graphic is made up of the current value arrow and a label to the right of the arrow that contains the current_value. The value of P2 in the background graphic anchors the left side of the indicator graphic.

The user interacts with the arrow by placing the cursor within the arrow's boundaries, pressing and holding down one of the the mouse buttons, dragging the cursor (and thus the arrow) up or down, and finally releasing the mouse button. If the cursor is located within the arrow, the control reacts to these user actions by responding to the OnMouseDown, OnMouseMove, and OnMouseUp events.

Slider Control Arrow Geometry

The indicator graphic is drawn by draw_indicator_graphic:

C#
// ************************************ draw_indicator_graphic

void draw_indicator_graphic ( Graphics  graphics )
    {

    update_indicator_geometry ( );

    draw_current_value_string ( graphics );
    draw_arrow ( graphics );
    }

Prior to drawing the indicator graphic, the indicator graphic geometry must be updated.

C#
// ********************************* update_indicator_geometry

void update_indicator_geometry ( )
    {
    int     dy = 0;
    float   percent = 0.0F;
    float   percent_down = 0.0F;
    int     pixels = 0;
    int     pixels_down = 0;
    double  theta = INTERIOR_ANGLE * Math.PI / 180.0;

    update_tube_dimensions ( );
                               // solve initialization problem
    if ( CurrentValue < MinimumValue )
        {
        CurrentValue = MaximumValue - Increment;
        }

    P5.X = P2.X + round ( ( double ) tube_width / 2.0 ) +
                  offset;
    pixels = round ( ( float ) ( P3.Y - P2.Y ) );
    percent = Math.Abs ( ( float ) ( current_value -
                                     MinimumValue ) /
                         ( float ) ( MaximumValue -
                                     MinimumValue ) );
    percent_down = 1.0F - percent;
    pixels_down = round ( ( percent_down *
                          ( float ) pixels ) );
    P5.Y = P2.Y + pixels_down;

    dy = round ( ( double ) arrow_width *
                 Math.Sin ( theta ) );

    P6.X = P5.X + arrow_width;
    P6.Y = P5.Y - dy;

    P7.X = P6.X;
    P7.Y = P5.Y + dy;
    }

The value of the constant INTERIOR_ANGLE (20 degrees) was obtained experimentally. One of the first things I did was to draw the arrow. Over time, I found that 20 degrees was balanced, pleasing to the eye, and of sufficient size to allow the user to capture the arrow (for dragging).

Having the revised the indicator geometry, the label to the right of the arrow is drawn.

C#
// ********************************* draw_current_value_string

void draw_current_value_string ( Graphics graphics )
    {
    Brush           brush;
    string          current_value_string;
    TextFormatFlags flags;
    Point           location;
    Rectangle       rectangle;

    brush = new SolidBrush ( Color.Black );
    current_value_string = current_value.ToString ( );
    flags = ( TextFormatFlags.Left |
              TextFormatFlags.VerticalCenter );
    location = new Point ( P6.X + offset,
                           P5.Y - ( label_size.Height / 2 ) );

    rectangle = new Rectangle ( location, label_size );
    TextRenderer.DrawText ( graphics,
                            current_value_string,
                            label_font,
                            rectangle,
                            Color.Black,
                            flags );
    }

TextRenderer and TextFormatFlags are used to draw the label to the left side of and centered vertically within the bounding rectangle.

Finally we can draw the arrow.

C#
// ************************************************ draw_arrow

void draw_arrow ( Graphics graphics )
    {
    Point [ ]       arrow_outline = new Point [ 3 ];
    GraphicsPath    arrow_path = null;
    Brush           brush;
    int             i = 0;
    Pen             pen;
    Color           value_color;

    update_geometry ( );

    value_color = background.ColorAtLocation (
                      new Point ( P5.X - ( tube_width / 2 ),
                                  P5.Y ) );
    brush = new SolidBrush ( value_color );
    pen = new Pen ( Color.Black, 1.0F );

    arrow_outline [ i++ ] = P5;
    arrow_outline [ i++ ] = P6;
    arrow_outline [ i++ ] = P7;

    arrow_path = new GraphicsPath ( FillMode.Alternate );
    arrow_path.AddLines ( arrow_outline );
    arrow_path.CloseFigure ( );
                                // arrow_region is used for
                                // hit testing
    arrow_region = new Region ( arrow_path );
                                // draw arrow outline
    graphics.DrawPolygon ( pen, arrow_outline );
                                // fill arrow outline
    graphics.FillPolygon ( brush, arrow_outline );

    arrow_path.Dispose ( );
    brush.Dispose ( );
    pen.Dispose ( );
    }

The outine of the arrow is defined by P5, P6, and P7. The array arrow_outline stores the outline. From this array, a GraphicsPath is defined and from this path, a Region is defined. The IsVisible method of the region will be used for hit testing. The outline of the arrow is drawn using DrawPolygon.

The arrow is filled using FillPolygon.

At this point, the indicator graphic has been drawn and may be transferred into the Graphics object supplied in the OnPaint PaintEventArgs. The indicator graphic is redrawn when either the background graphic is redrawn or when the user moves the arrow.

Handling Events [^]

One of the more obscure features of most programming languages is the manner in which actions of interest are signalled. Many languages have this mechanism. But since SliderControl is implemented in C#, this discussion will be limited to that language.

SliderValueChanged [^]

The SliderControl would be useless unless it could tell (signal) its parent that the user made a change to current_value (by dragging the arrow up or down). To signal this event, SliderControl contains the declaration of the SliderValueChanged event.

cd
// ******************************** control delegate and event

public delegate void SliderValueChangedHandler (
                        Object                      sender,
                        SliderValueChangedEventArgs e );

public event SliderValueChangedHandler SliderValueChanged;

The delegate SliderValueChangedHandler defines the signature of a method that will be invoked by the SliderValueChanged event. The event has the two arguments: sender and a custom EventArgs, SliderValueChangedEventArgs.

C#
// ***************************** class SliderValueChangedEventArgs

public class SliderValueChangedEventArgs : EventArgs
    {
    public int  SliderValue;

    // ******************************* SliderValueChangedEventArgs

    public SliderValueChangedEventArgs ( int slider_value )
        {

        SliderValue = slider_value;
        }

    } // class SliderValueChangedEventArgs

SliderValueChangedEventArgs returns the current SliderValue. Whenever SliderControl detects a change in the value of current_value it invokes trigger_slider_value_changed_event.

C#
// ************************ trigger_slider_value_changed_event

void trigger_slider_value_changed_event ( int  current_value )
    {

    if ( SliderValueChanged != null )
        {
        SliderValueChanged ( this,
                             new SliderValueChangedEventArgs (
                                     current_value ) );
        }
    }

The SliderValueChanged event may have zero or more subscribers. The test

C#
if ( SliderValueChanged != null )

is made to insure that there is at least one subscriber to the SliderValueChanged event. Failure to make this test could cause an exception, something to be avoided in a user control.

If a class wishes to be notified of a change in the value of the SliderControl's current_value, it must register an event handler. In the TestSliderControl demonstration project, there are three SliderControl instances. Each instance is wired to the event handler. This is accomplished by first declaring that the method slider_SC_SliderValueChanged is to be used to capture the event:

C#
heater_AC_SC.SliderValueChanged +=
    new SliderControl.SliderControl.
            SliderValueChangedHandler (
                SliderValueChanged );

spa_SC.SliderValueChanged +=
    new SliderControl.SliderControl.
            SliderValueChangedHandler (
                SliderValueChanged );

cruise_control_SC.SliderValueChanged +=
    new SliderControl.SliderControl.
            SliderValueChangedHandler (
                SliderValueChanged );

The slider_SC_SliderValueChanged method is declared as:

C#
// ****************************** slider_SC_SliderValueChanged

void SliderValueChanged (
            object                                    sender,
            SliderControl.SliderValueChangedEventArgs e )
    {
    string                      new_value;
    SliderControl.SliderControl slider_control;

    slider_control = ( SliderControl.SliderControl ) sender;
    new_value = e.SliderValue.ToString ( );

    switch ( slider_control.Tag.ToString ( ).ToLower ( ) )
        {
        case "heater_ac":
            heater_ac_value_TB.Text = new_value;
            break;

        case "spa":
            spa_value_TB.Text = new_value;
            break;

        case "cruise_control":
            cruise_control_value_TB.Text = new_value;
            break;

        default:

            break;
        }
    }

At design time, each instance of SliderControl is given a unique Tag value. The slider_SC_SliderValueChanged method uses this value to discriminate between the multiple instances of SliderControl. In this example, the new value of SliderValue is placed into a TextBox. Any number of other operations could have been performed using the value returned in SliderValue.

Sliding the Arrow [^]

The user interacts with the SliderControl by moving the arrow up and down as described earlier. The user's interactions are detected by the event handlers for the OnMouseDown, OnMouseMove, and OnMouseUp events.

OnMouseDown Event Handler [^]
C#
// *********************************************** OnMouseDown

protected override void OnMouseDown ( MouseEventArgs e )
    {

    base.OnMouseDown(e);

    if ( arrow_region.IsVisible ( new Point ( e.X, e.Y ) ) )
        {
                            // cursor is in the arrow
        arrow_being_dragged = true;
        if ( current_value_changed ( e.Y ) )
            {
            trigger_slider_value_changed_event (
                                            current_value );
            this.Invalidate ( );
            }
        }
    }

The SliderControl OnMouseDown event handler is classic.

  1. Call the base class's OnMouseDown method so that all registered delegates receive the event.
  2. Test to insure that the cursor is in the arrow_region, defined in the draw_arrow method.
  3. If the cursor is in the arrow_region, set the variable arrow_being_dragged to true and test to insure that the value, associated with the arrow, differs from the current_value. The test to determine if the new value differs from the the current_value is performed in the current_value_changed method.
  4. If current_value_changed returns true, trigger_slider_value_changed_event is invoked and the control repaints itself.

current_value_changed is implemented as:

C#
// ************************************* current_value_changed

bool current_value_changed ( int y )
    {
    int     old_current_value = current_value;
    float   percent;
    float   value_down;
    int     pixels;
    int     pixels_down;
    bool    value_changed = false;

    if ( P5.Y != y )
        {
        if ( y > P3.Y )
            {
            y = P3.Y;
            }
        if ( y < P2.Y )
            {
            y = P2.Y;
            }
        pixels = P3.Y - P2.Y;
        pixels_down = y - P2.Y;
        percent = ( float ) pixels_down / ( float ) pixels;
        value_down = percent * ( float ) ( MaximumValue -
                                           MinimumValue );
        current_value = round ( ( float ) MaximumValue -
                                value_down );
        value_changed = ( old_current_value !=
                          current_value );
        }

    return ( value_changed );
    }

Note that current_value is revised in this method.

OnMouseMove Event Handler [^]
C#
// *********************************************** OnMouseMove

protected override void OnMouseMove ( MouseEventArgs e )
    {

    base.OnMouseMove ( e );

    if ( arrow_being_dragged )
        {
        if ( current_value_changed ( e.Y ) )
            {
            trigger_slider_value_changed_event (
                                            current_value );
            this.Invalidate ( );
            }
        }
    }

The SliderControl OnMouseMove event handler is straight-forward. If arrow_being_dragged is true, it means that the user still has the mouse button down and is dragging the arrow up or down. In that case, current_value_changed is invoked and, if it returns true, trigger_slider_value_changed_event is invoked and the control repaints itself.

OnMouseUp Event Handler [^]
C#
// ************************************************* OnMouseUp

protected override void OnMouseUp ( MouseEventArgs e )
    {

    base.OnMouseUp ( e );

    arrow_being_dragged = false;
    }

When the user releases the mouse button, the OnMouseUp event handler is invoked. In turn, this handler simply sets arrow_being_dragged to false. This causes all future mouse movement to be ignored until the user again presses a mouse button. Note that the SliderControl need not be redrawn.

OnPaint Event Handler [^]

C#
// *************************************************** OnPaint

protected override void OnPaint ( PaintEventArgs e )
    {

    base.OnPaint ( e );

    e.Graphics.Clear ( ControlBackgroundColor );

    if ( ( this.Height != control_height ) ||
         ( this.Width != control_width ) )
        {
        int  width;

        revise_background_graphic = true;
        control_height = this.Height;
        control_width = round ( ( float ) control_height /
                                4.0F );
        update_geometry ( );
        width = offset + label_size.Width + offset +
                tube_width + offset + arrow_width + offset +
                label_size.Width + offset;
        if ( width > control_width )
            {
            control_width = width;
            }
        this.Size = new Size ( control_width,
                               control_height );
        }

    if ( ( background == null ) || revise_background_graphic )
        {
        if ( revise_background_graphic )
            {
            revise_background_graphic = false;
            }
        create_background_graphic ( );
        draw_background_graphic ( background.Graphic );
        }
    background.RenderGraphicsBuffer ( e.Graphics );

    create_indicator_graphic ( );
    draw_indicator_graphic ( indicator.Graphic );

    indicator.RenderGraphicsBuffer ( e.Graphics );
    }

Whenever the system recognizes that a control needs to be repainted, it notifies the control by raising the OnPaint event. There are any number of reasons that the OnPaint event might be raised, some of which include:

  • The cursor moves across the face of a control.
  • The control is resized.
  • A hidden area of the control becomes visible.
  • The control itself requests it.

For example, both the OnMouseDown and OnMouseMove event handlers trigger the redrawing of the control by invoking Invalidate. Also Invalidate is invoked when any of ControlBackgroundColor, CurrentValue, ForceTubeWidth, Increment MaximumColor, MaximumValue, MidpointColor, MinimumColor, MinimumValue, or TubeWidth are changed.

When invoked, the SliderControl OnPaint event handler

  1. Invokes the base class's OnPaint method so that all registered delegates receive the event.
  2. Completely clears the control of existing graphics.
  3. Determines if the height or width of the control has changed. If so, first sets revise_background_graphic to true, thereby forcing the background graphic to be redrawn, and then computes and sets the new size of the control.
  4. If the background is null or revise_background_graphic is true, the background is recreated and redrawn. Note that if neither of these conditions are true, the background remains untouched.
  5. Draws the background to the screen.
  6. Creates and redraws the indicator graphic.
  7. Draws the indicator to the screen.

Graphics Buffer [^]

The GraphicsBuffer class contains an off-screen bitmap used to draw graphic objects without flicker. Although .Net provides a Double Buffered Graphics capability, it is overkill for the SliderControl.

Normally I include GraphicsBuffer within the control I am implementing. However, because the class is useful in more than just the SliderControl software, I have included it as a separate source file (GraphicsBuffer.cs) in the downloads.

Members [^]

The GraphicsBuffer's members are:

C#
Bitmap      bitmap;
Graphics    graphics;
int         height;
int         width;

bitmap is the off-screen object that holds the image that will eventually be displayed on the screen. Drawing is performed through graphics, defined internally within GraphicsBuffer as

C#
graphics = Graphics.FromImage ( bitmap );

and height and width contain the bitmap dimenions.

Constructor [^]

C#
// ******************************************** GraphicsBuffer

public GraphicsBuffer ( )
    {

    width = 0;
    height = 0;
    }

The GraphicsBuffer constructor creates a new empty GraphicsBuffer object. For example:

C#
background = new GraphicsBuffer ( );

or

C#
indicator = new GraphicsBuffer ( );

Methods [^]

Once a GraphicsBuffer has been constructed, methods and a property are available to refine and use it.

CreateGraphicsBuffer [^]
C#
// ************************************** CreateGraphicsBuffer

public bool CreateGraphicsBuffer ( int width,
                                   int height )
    {
    bool  success = true;

    if ( graphics != null )
        {
        graphics.Dispose ( );
        graphics = null;
        }

    if ( bitmap != null )
        {
        bitmap.Dispose ( );
        bitmap = null;
        }

    this.width = 0;
    this.height = 0;

    if ( ( width == 0 ) || ( height == 0 ) )
        {
        success = false;
        }
    else
        {
        this.width = width;
        this.height = height;

        bitmap = new Bitmap ( this.width, this.height );
        graphics = Graphics.FromImage ( bitmap );

        success = true;
        }

    return ( success );
    }

CreateGraphicsBuffer is the first method to be invoked after the GraphicsBuffer constructor. This method completes the creation process by deleting any remnants of an earlier invocation of CreateGraphicsBuffer; creates the in-memory bitmap from the specified height and width; and associates a graphics with the bitmap.

DeleteGraphicsBuffer [^]
C#
// ************************************** DeleteGraphicsBuffer

public GraphicsBuffer DeleteGraphicsBuffer ( )
    {

    if ( graphics != null )
        {
        graphics.Dispose ( );
        graphics = null;
        }

    if ( bitmap != null )
        {
        bitmap.Dispose ( );
        bitmap = null;
        }

    width = 0;
    height = 0;

    return ( null );
    }

In the SliderControl, whenever the background graphic is to be redrawn, the following code is executed:

C#
// ********************************* create_background_graphic

void create_background_graphic ( )
    {

    if ( background != null )
        {
        background = background.DeleteGraphicsBuffer ( );
        }
    background = new GraphicsBuffer ( );
    background.CreateGraphicsBuffer ( control_width,
                                      control_height );
    background.Graphic.SmoothingMode = SmoothingMode.
                                       HighQuality;
    background.ClearGraphics ( ControlBackgroundColor );
    }

Similar code is executed to redraw the indicator graphic.

C#
// ********************************** create_indicator_graphic

void create_indicator_graphic ( )
    {

    if ( indicator != null )
        {
        indicator = indicator.DeleteGraphicsBuffer ( );
        }
    indicator = new GraphicsBuffer ( );
    indicator.CreateGraphicsBuffer ( control_width,
                                     control_height );
    indicator.Graphic.SmoothingMode = SmoothingMode.
                                      HighQuality;
    }

DeleteGraphicsBuffer is also executed when the SliderControl memory_cleanup event handler is invoked.

C#
// ******************************************** memory_cleanup

void memory_cleanup ( object    sender,
                      EventArgs e )
    {

    if ( arrow_region != null )
        {
        arrow_region.Dispose ( );
        arrow_region = null;
        }
                                // DeleteGraphicsBuffer
                                // returns null
    if ( background != null )
        {
        background = background.DeleteGraphicsBuffer ( );
        }

    if ( indicator != null )
        {
        indicator = indicator.DeleteGraphicsBuffer ( );
        }
    }

memory_cleanup is wired to the ApplicationExit event and executed when SliderControl is constructed.

C#
Application.ApplicationExit +=
                new EventHandler ( memory_cleanup );
memory_cleanup ( this, EventArgs.Empty );
RenderGraphicsBuffer [^]
C#
// ************************************** RenderGraphicsBuffer

public void RenderGraphicsBuffer ( Graphics graphic )
    {

    if ( bitmap != null )
        {
        graphic.DrawImage (
                    bitmap,
                    new Rectangle ( 0, 0, width, height ),
                    new Rectangle ( 0, 0, width, height ),
                    GraphicsUnit.Pixel );
        }
    }

RenderGraphicsBuffer draws the GraphicsBuffer bitmap to the graphic specified in its parameter. It uses the DrawImage method of the .Net Graphics class and is invoked for the background and indicator graphics in the SliderControl OnPaint event handler.

C#
background.RenderGraphicsBuffer ( e.Graphics );
:
:
indicator.RenderGraphicsBuffer ( e.Graphics );

Here e is the PaintEventArgs parameter to the OnPaint event handler. When RenderGraphicsBuffer is invoked, the screen image is updated.

ClearGraphics [^]
C#
// ********************************************* ClearGraphics

public void ClearGraphics ( Color background_color )
    {

    Graphic.Clear ( background_color );
    }

ClearGraphics clears the entire drawing surface and fills it with the specified background_color.

ColorAtLocation [^]
C#
// ******************************************* ColorAtLocation

public Color ColorAtLocation ( Point location )
    {
    Color  color = Color.Black;

    if ( ( location.X <= this.width ) &&
         ( location.Y <= this.height ) )
        {
        color = this.bitmap.GetPixel ( location.X,
                                       location.Y );
        }

    return ( color );
    }

ColorAtLocation returns the GDI Color at the given location in the graphics bitmap. If the location is outside the graphics bitmap, the color Black is returned.

GraphicsBufferExists [^]
C#
// ************************************** GraphicsBufferExists

public bool GraphicsBufferExists
    {

    get
        {
        return ( graphics != null );
        }
    }

GraphicsBufferExists returns true if the graphics object exists; false, otherwise.

Properties [^]

GraphicsBuffer has only one property.

Graphic [^]
C#
// *************************************************** Graphic

public Graphics Graphic
    {

    get
        {
        return ( graphics );
        }
    }

Graphic returns the current Graphics object. This object is used to draw graphics on the bitmap.

Slider Control Demo [^]

Test Slider Control

Recall from earlier that the the width of the tube may be either specified or computed.

In the figure to the right, the three tubes require that two of them have their tube widths specified. On the property pages for the spa and the cruise control SliderControls, ForceTubeWidth was set to true and TubeWidth was set to 16. That value (16) comes from the TubeWidth of the Heater/AC SliderControl. The effect of these settings is to insure that all three tubes have the same width when displayed on the form.

Conclusion [^]

This article presented a step-by-step guide for the implementation of a slider control.

References [^]

Development Environment [^]

SliderControl was developed in the following environment:

      Microsoft Windows 7 Professional Service Pack 1
      Microsoft Visual Studio 2008 Professional
      Microsoft .Net Framework Version 3.5 SP1
      Microsoft Visual C# 2008

History [^]

05/08/2013   Original Article
05/14/2013   Changed inheritance from UserControl to Control, version to 1.2

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)