Introduction [^]
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 [^]
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.
- 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.
- 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 [^]
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 [^]
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:
- Update the tube dimensions.
- Update the background labels.
- Update the background geometry.
Although slightly misnamed, update_tube_dimensions is:
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.
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 );
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.
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:
- Fill the control with the
ControlBackgroundColor.
- Draw the background labels.
- Draw the end point circles
(MaximumValue then
MinimumValue).
- Draw the colored tube.
This sequence is found in
draw_background_graphic:
void draw_background_graphic ( Graphics graphics )
{
Brush brush;
Rectangle end_point_rectangle =
new Rectangle ( );
LinearGradientBrush linear_gradient_brush;
Rectangle rectangle;
draw_background_labels ( graphics );
end_point_rectangle.Size = new Size ( tube_width,
tube_width );
brush = new SolidBrush ( MaximumColor );
end_point_rectangle.Location = P0;
graphics.FillEllipse ( brush, end_point_rectangle );
brush.Dispose ( );
brush = new SolidBrush ( MinimumColor );
end_point_rectangle.Location = P4;
graphics.FillEllipse ( brush, end_point_rectangle );
brush.Dispose ( );
rectangle = new Rectangle ( new Point ( P0.X, P2.Y ),
new Size ( tube_width,
P3.Y - P2.Y ) );
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
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
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 [^]
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.
The indicator graphic is drawn by
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.
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 ( );
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.
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.
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 = new Region ( arrow_path );
graphics.DrawPolygon ( pen, 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.
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.
public class SliderValueChangedEventArgs : EventArgs
{
public int SliderValue;
public SliderValueChangedEventArgs ( int slider_value )
{
SliderValue = slider_value;
}
}
SliderValueChangedEventArgs
returns the current
SliderValue. Whenever
SliderControl detects a change in the value of
current_value it invokes
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
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:
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:
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 [^]
protected override void OnMouseDown ( MouseEventArgs e )
{
base.OnMouseDown(e);
if ( arrow_region.IsVisible ( new Point ( e.X, e.Y ) ) )
{
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.
- Call the base class's
OnMouseDown method so
that all registered delegates receive the event.
- Test to insure that the cursor is in the
arrow_region, defined
in the draw_arrow method.
- 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.
- If current_value_changed
returns true,
trigger_slider_value_changed_event
is invoked and the control repaints itself.
current_value_changed is
implemented as:
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 [^]
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 [^]
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 [^]
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
- Invokes the base class's
OnPaint method so
that all registered delegates receive the event.
- Completely clears the control of existing graphics.
- 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.
- 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.
- Draws the background to the screen.
- Creates and redraws the indicator graphic.
- 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:
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
graphics = Graphics.FromImage ( bitmap );
and height and
width contain the
bitmap dimenions.
Constructor [^]
public GraphicsBuffer ( )
{
width = 0;
height = 0;
}
The GraphicsBuffer constructor creates a new empty GraphicsBuffer
object. For example:
background = new GraphicsBuffer ( );
or
indicator = new GraphicsBuffer ( );
Methods [^]
Once a GraphicsBuffer has been constructed, methods and a property
are available to refine and use it.
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 [^]
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:
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.
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.
void memory_cleanup ( object sender,
EventArgs e )
{
if ( arrow_region != null )
{
arrow_region.Dispose ( );
arrow_region = 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.
Application.ApplicationExit +=
new EventHandler ( memory_cleanup );
memory_cleanup ( this, EventArgs.Empty );
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.
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 [^]
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 [^]
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 [^]
public bool GraphicsBufferExists
{
get
{
return ( graphics != null );
}
}
GraphicsBufferExists returns
true if the graphics object
exists; false, otherwise.
Properties [^]
GraphicsBuffer has only one property.
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 [^]
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
|