Introduction
TOC
This article presents a rather extensive description of a
template for the implementation of user drawn Hover-Click
Controls
[^].
Hover-Click controls are found in many desktop applications. For
example, in the upper right corner of Internet Explorer there
appear three images: a house (for Home), a star (for Favorites),
and a gear (for Tools). When the mouse hovers over one of these
images, the image is filled with a different color. And if the
mouse is clicked as it is hovering, a submenu or a new page may
appear.
The template presented in this article is unusual in that:
- The Hover-Click control's graphic image is persisted between
Windows
OnPaint
[^]
events, thus not requiring that it be recreated between these
events.
- The template uses
Transformation Matrixes
[^]
to translate, rotate and scale the Hover-Click control's
graphic image.
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
TOC
Each Hover-Click control presents an image that changes its fill
color when the user moves the mouse pointer over the image. While
the mouse pointer is hovering over the image, the control will accept a
user's mouse click and take some appropriate action.
Developer Perspective
TOC
To the developer, Hover-Click controls can be
incorporated into the Visual Studio ToolBox
[^]
and placed on the surface of a Windows Form. The most important
property of a Hover-Click control is
this.Height, known internally
as control_height and
externally as the
Control_Height
property. All other dimensions are derived from this dimension.
The event
UserClicked
is
raised
[^]
when the user clicks on a Hover-Click control's image. There are no
values supplied in
EventArgs
[^].
Only the fact that the user clicked on the Hover-Click control's
image is communicated by this event.
Developing the control
TOC
During the process of developing these controls, it was crucial to
follow some form of requirements, accomplished though a Microsoft
PowerPoint presentation. Some of the 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.
Naming Convention
TOC
Throughout this article a number of Hover-Click controls will be
presented. To avoid repeating the phrase "Hover-Click control," it
will be eliminated where its presence is implied by context.
Table of Contents
The symbol
TOC
returns the reader to the top of the Table of Contents.
Visual Properties
TOC
Each Hover-Click control has four properties that form the
user's visual image. In the figure to the right, using the
Favorites control as an example, the control's boundary is drawn
using green dashed lines.
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 or by specifying its
value in the control's property dialog. The width of the
control is set equal to
control_height. There is
a two pixel border between the control's border and the edge of
the graphics image.
The developer may specify the
Control_Color, the
control's fill color when the mouse pointer
is not
over the control, and the
Hover_Color, the
control's fill color when the mouse pointer
is
over the control.
Outline specifies whether
or not the control's border is framed.
The background of a Hover-Click control is transparent. No
way to change this property is provided. This results in a control
where only the control's graphic image is visible.
Note
|
|
In the figure to the left, the mouse pointer
hot spots
[^]
are located at the red dots.
The mouse pointer must be over the control's graphic image
for hovering to be recognized. In the figure to the left,
the mouse pointer labeled
A
will be recognized as hovering over the control while the
mouse pointer labeled
B
will not, even though it is within the control's
boundaries. Note too that, in this case, the inner-most
circle is not in the hovering region.
|
Control Properties
TOC
Hover-Click controls have the following properties available
to the developer:
Name
| |
Description
|
Control_Color
| | Gets or sets the fill color of the control when the
mouse pointer
is not within
the control's graphic image. The default value is
Color.White.
|
Control_Height
| | Gets or sets the height of the control. Any change to this
property causes other internal values for the control to be
changed. The default value is 30 pixels.
|
Hover_Color
| | Gets or sets the fill color of the control when the
mouse pointer
is within
the control's graphic image. The default value is
Color.LightSkyBlue for the
Settings and Home controls and
Color.Gold for the Favorites
control.
|
Outline
| | Gets or sets whether or not the control's graphic image is drawn
with a black outline. The default value is
true.
|
Implementation
TOC
When implementing a Hover-Click control, there are common tasks that
must be performed.
- Define the Control Delegate and Event
- Perform Memory Cleanup
- Create the Control's Graphic Image
- Define the Class Constructor
- Handle Events
- OnMouseClick
- OnMouseLeave
- OnMouseMove
- OnPaint
- OnResize
- Get/Set Control Properties
- Control_Color
- Control_Height
- Hover_Color
- Outline
With the exception of the creation of the control graphic image, all
of these tasks are the same from one Hover-Click control to another.
Define the Control Delegate and Event
TOC
All Hover-Click controls signal that the user clicked on its
image. To signal this event, the control contains the declaration
of an event, triggered when the mouse is clicked over the
control's graphic image.
public delegate void UserClickedHandler ( Object sender,
EventArgs e );
public event UserClickedHandler UserClicked;
The
delegate
[^]
UserClickedHandler
defines the signature of a method that will be invoked by the
UserClicked event.
The event has the two arguments: sender and EventArgs. EventArgs
will always be empty.
Perform Memory Cleanup
TOC
In this template, there are two
Graphics Device Interface (GDI)
[^]
objects,
control_brush
and
control_region,
that persist between invocations of the
OnPaint
[^]
method. When the control is disposed, both
must be disposed. Failure to dispose of GDI objects when
the control is disposed will cause a
memory leak
[^]. The method
memory_cleanup
performs this task.
void memory_cleanup ( object sender,
EventArgs e )
{
if ( control_brush != null )
{
control_brush.Dispose ( );
control_brush = null;
}
if ( control_region != null )
{
control_region.MakeEmpty ( );
control_region.Dispose ( );
control_region = null;
}
}
In the control's constructor appears
this.Disposed += new EventHandler ( memory_cleanup );
This line
registers
[^] the
memory_cleanup method as the
event handler
[^]
for when the Hover-Click control itself is disposed.
Create the Control's Graphic Image
TOC
The Home and Favorites control graphic images are
simple and have much in common. The Settings control
graphic image is significantly more complex.
Before we go into the particulars of drawing the Hover-Click
controls, it might be useful to recall some of the features of the
Graphics Device Interface, also known as the GDI. This Microsoft
product is a portion of Microsoft's operating systems and provides
drawing and string formatting for all Windows components. Through
its API, the GDI extends its support to the developer.
Of particular interest is the ability of the GDI to perform
rotation
[^],
scaling
[^]
and
translation
[^]
of GDI objects.
Within this control template,
all drawing is performed using values derived from a fixed
CONTROL_HEIGHT
of 200 pixels. The resultant drawing is more accurate than if the
drawing was performed using the current
control_height (normally
much smaller). By drawing in this larger size and then taking
advantage of the GDI scaling methods, we can obtain a scaled image
that does not suffer from interpolation errors.
For example, the Settings control's graphic image
contains gear teeth arranged circularly around the control's
center. There were two options to draw these teeth:
Option 1.
|
| For each tooth, compute the location of each of the four
corners of the tooth (points p0, p1, p2, and p3 in the
preceding figure); create a
GraphicsPath
[^]
from the polygon that is defined by these four points;
add the GraphicsPath to a region. After all teeth are
processed, return the region as the tooth region. It is
the responsibility of the programmer to compute the
translated, rotated, and scaled image.
|
|
| |
Option 2.
|
|
The point o is the
origin of the control, the point
p is the location
of the gear tooth after drawing it. The angle of
rotation, in this case, is 135°. So all that is
needed is to draw a tooth at the origin and then apply
a
transformation matrix
[^].
For each tooth, compute the offsets of the upper-left
corner of the tooth,
p,
before rotation; compute the angle of rotation;
compute a scaling factor that will result in an image
of the proper size; fill a transformation matrix with
the offsets, rotation, and scaling; add the image of a
tooth
drawn at the origin of the control
to a GraphicsPath; allow the transformation matrix to
place the image at its final position and orientation
in the path; add the path to the tooth region. After
all teeth are processed, return the region as the
tooth region. It is the responsibility of the GDI to
scale, rotate, and translate the image.
|
|
So what's the advantage of using the GDI to scale, rotate, and
translate? Without question the GDI performs more efficiently and
more accurately than any code I could write. Even when the
Control_Height is set to 30
pixels, all the teeth appear correctly. In experiments with
computing the location and drawing a polygon, I found that some of
the teeth were poorly drawn or missing.
In the preceding discussions, each control used a GDI
Region
[^]
to contain its graphics image in a scaled, rotated, and
translated state.
A region describes the interior of a graphics shape composed of
rectangles and paths. It is scalable. Controls use regions to clip
the output of drawing operations. These regions are called clipping
regions. Controls also use regions in hit-testing operations, such
as checking whether or not a point is within a region. Controls can
fill a region by using a
Brush
[^]
object.
Although Microsoft is usually quite thorough in converting Win32 API
entry points into C# entry points, drawing a region (not filling)
appears to have been overlooked. To draw a border, it is necessary
to invoke the
FrameRgn
[^]
function of the Win32 API.
Home and Favorites Controls
TOC
The graphic images for the Home and Favorites controls were
initially created on a ten-by-ten grid in Microsoft PowerPoint. The
coordinates, in tenths, were collected clockwise, starting at the
lower left point.
Each coordinate value was multiplied by 200 to obtain the
coordinates of the end-points of the lines that would define the
images' polygons. For the Home control:
Point [ ] home = { new Point ( 20, 160 ),
new Point ( 20, 80 ),
new Point ( 0, 80 ),
new Point ( 20, 70 ),
new Point ( 20, 40 ),
new Point ( 40, 40 ),
new Point ( 40, 56 ),
new Point ( 100, 20 ),
new Point ( 200, 80 ),
new Point ( 180, 80 ),
new Point ( 180, 160 ),
new Point ( 120, 160 ),
new Point ( 120, 100 ),
new Point ( 80, 100 ),
new Point ( 80, 160 ),
new Point ( 20, 160 ) };
For the Favorites control:
Point [ ] star = { new Point ( 40, 200 ),
new Point ( 62, 125 ),
new Point ( 0, 80 ),
new Point ( 75, 80 ),
new Point ( 100, 0 ),
new Point ( 125, 80 ),
new Point ( 200, 80 ),
new Point ( 138, 125 ),
new Point ( 160, 200 ),
new Point ( 100, 155 ),
new Point ( 40, 200 ) };
Although not necessary, the last point duplicates the first point,
thereby closing the polygon. Closure could have been performed by
invoking the
CloseFigure
[^]
method.
With the end-points of the Home and Favorites polygons defined, the
control region may be created.
Region create_control_region ( int new_height,
Point [ ] points,
Region control_region )
{
GraphicsPath path = new GraphicsPath ( );
float scale = 0.0F;
Matrix transform = new Matrix ( );
control_height = new_height;
this.Height = control_height;
this.Width = control_height;
scale = ( float ) new_height / ( float ) CONTROL_HEIGHT;
transform.Scale ( scale, scale );
path.AddPolygon ( points );
path.Transform ( transform );
if ( control_region != null )
{
control_region.Dispose ( );
control_region = null;
}
control_region = new Region ( path );
transform.Dispose ( );
path.Dispose ( );
return ( control_region );
}
This method performs the following:
- Sets the height and width of the control to the value passed as
new_height.
- Calculates the scale that will be used to scale the
graphics object and places the result in the
transformation matrix.
- Adds a polygon, defined by the
points array parameter,
to the path.
- Applies the transformation matrix.
- Deletes any existing control region and creates a new control
region.
- Returns the newly created control region.
If the control being drawn is the Home control, the
points array parameter will
be the
home
array; if it is the Favorites control, the
points array parameter will
be the
star
array.
Settings Control
TOC
As mentioned earlier, the Settings graphic image is
complex.
The control's graphic is made up of two distinct
regions
[^]:
the annulus (doughnut) region and the teeth region. As seen
from the preceding figure, the annulus region is made up of an
outer region (solid colored circle) and an inner region (open
circle) which, when combined, form the annulus region. When the
annulus and teeth regions are combined, the control region is the
result.
The
annulus_region is created by
the
create_annulus_region
method.
Region create_annulus_region ( int control_height )
{
int diameter = 0;
GraphicsPath inner_path = new GraphicsPath ( );
Region inner_region = null;
int origin = 0;
GraphicsPath outer_path = new GraphicsPath ( );
Region outer_region = null;
Rectangle rectangle;
float scale = 0.0F;
Matrix transform = new Matrix ( );
scale = ( float ) control_height /
( float ) CONTROL_HEIGHT;
transform.Scale ( scale, scale );
origin = round ( OUTER_CIRCLE_MULTIPLIER *
( float ) CONTROL_HEIGHT ) +
OFFSET;
diameter = CONTROL_HEIGHT - 2 * origin;
rectangle = new Rectangle (
new Point ( origin,
origin ),
new Size ( ( diameter - 1 ),
( diameter - 1 ) ) );
outer_path.AddEllipse ( rectangle );
outer_path.Transform ( transform );
outer_region = new Region ( outer_path );
origin = round ( INNER_CIRCLE_MULTIPLIER *
( float ) CONTROL_HEIGHT ) +
OFFSET;;
diameter = CONTROL_HEIGHT - 2 * origin;
rectangle = new Rectangle (
new Point ( origin,
origin ),
new Size ( ( diameter - 1 ),
( diameter - 1 ) ) );
inner_path.AddEllipse ( rectangle );
inner_path.Transform ( transform );
inner_region = new Region ( inner_path );
outer_region.Exclude ( inner_region );
inner_path.Dispose ( );
inner_region.Dispose ( );
outer_path.Dispose ( );
transform.Dispose ( );
return ( outer_region );
}
This method performs the following:
- Calculates the scale that will be used
to scale the graphics object and places the calculated value
into the transformation matrix. The value calculated for
scale
is constant for both the outer and inner regions. This is the
only transformation used in the drawing of the annulus (i.e., no
rotation or translation).
- Creates the outer path that contains the outer circle.
- Scales the outer path and places it into the outer region.
- Creates the inner path that contains the inner circle.
- Scales the inner path and places it into the inner region.
- Excludes the inner region from the outer region, effectively
opening a hole in the outer region.
- Returns the revised outer region, which is the annulus region.
The helper method
round takes the forms:
public static int round ( float parameter )
{
return ( ( int ) ( parameter + 0.5F ) );
}
public static int round ( double parameter )
{
return ( ( int ) ( parameter + 0.5 ) );
}
The method is over-loaded so as to allow parameters of
float and
double.
The teeth region is created by the
create_teeth_region
that implements the
second option, described earlier.
Region create_teeth_region ( int control_height )
{
Point center;
int d;
double height_div_2;
double height_div_2_squared;
int radius = 0;
Region region = new Region ( );
float scale = 0.0F;
int tooth_height;
int tooth_width;
float tooth_width_div_2;
scale = ( float ) control_height /
( float ) CONTROL_HEIGHT;
region.MakeEmpty ( );
radius = round ( ( float ) CONTROL_HEIGHT / 2.0F ) -
OFFSET;
center = new Point ( radius, radius );
height_div_2 = ( double ) CONTROL_HEIGHT / 2.0 -
2.0 * ( double ) OFFSET;
height_div_2_squared = height_div_2 * height_div_2;
d = round ( Math.Sqrt ( height_div_2_squared +
height_div_2_squared ) );
tooth_height = round ( ( float ) d / 2.0F );
tooth_width = round ( 0.2F * ( float ) CONTROL_HEIGHT );
tooth_width_div_2 = ( float ) tooth_width / 2.0F;
for ( int i = 0; ( i < NUMBER_TEETH ); i++ )
{
int beta = 0;
Rectangle gear_tooth;
float offset_x = ( float ) OFFSET;
float offset_y = ( float ) OFFSET;
GraphicsPath path = new GraphicsPath ( );
float rotate = 0.0F;
Point t = new Point ( );
int theta = i * DEGREE_INCREMENT;
Matrix transform = new Matrix ( );
t.X = center.X + round ( ( float ) d *
cos_deg ( theta ) );
t.Y = center.Y + round ( ( float ) d *
sin_deg ( theta ) );
beta = theta - 90;
if ( beta < 0 )
{
beta = beta + 360;
}
offset_x = ( float ) t.X + ( float ) OFFSET +
( tooth_width_div_2 *
cos_deg ( beta ) );
offset_y = ( float ) t.Y + ( float ) OFFSET +
( tooth_width_div_2 *
sin_deg ( beta ) );
rotate = ( float ) theta + 90.0F;
if ( rotate > 360.0F )
{
rotate = rotate - 360.0F;
}
transform.Reset ( );
transform.Translate ( offset_x, offset_y );
transform.Rotate ( rotate );
transform.Scale ( scale, scale, MatrixOrder.Append );
gear_tooth = new_gear_tooth ( tooth_width,
tooth_height );
gear_tooth.Inflate ( -1, -1 );
path.Reset ( );
path.AddRectangle ( gear_tooth );
path.Transform ( transform );
region.Union ( path );
transform.Dispose ( );
path.Dispose ( );
}
return ( region );
}
This method performs the following:
- Calculates the scale that will be placed into the transformation
matrix to scale the graphics object. The value calculated for
scale is constant for
all teeth.
- Invokes
MakeEmpty
[^]
to initialize the target region to an empty interior. See
Lessons Learned.
- Computes
d,
the distance between the center of the control and
the center of the outside edge of each tooth, as depicted
above.
The calculated value is constant for all teeth.
- Computes
tooth_height and
tooth_width based on the
computed value of
d.
- For each tooth
- Computes the point at the center of the outer edge of
the tooth.
- Computes the translation offset in both the x- and
y-directions.
- Computes the rotation from the x-axis.
- Fills the transformation matrix with the computed values of
offset, rotation, and scale.
- Creates a gear tooth
at the origin of the control
and reduces its size by one pixel insuring that the bottom
and right sides will be drawn.
- Resets the path, adds the gear tooth to the path, and applies
the translation, rotation, and scaling to the path.
- Adds the path to the region.
- After all teeth are created, returns the region.
The helper
new_gear_tooth is
Rectangle new_gear_tooth ( int width,
int height )
{
return ( new Rectangle ( 0, 0, width, height ) );
}
As mentioned earlier, each tooth is initially drawn at the control's
origin and then
create_teeth_region
translates, rotates, and scales it to its final position and
orientation.
The three helper methods that allow computations to be performed on
integral degrees are:
public static double deg_2_rad ( int degrees )
{
return ( ( ( double ) degrees / 180.0 ) *
System.Math.PI );
}
public static float cos_deg ( int degrees )
{
return ( ( float ) System.Math.Cos ( deg_2_rad (
degrees ) ) );
}
public static float sin_deg ( int degrees )
{
return ( ( float ) System.Math.Sin ( deg_2_rad (
degrees ) ) );
}
If the
annulus_region and the
teeth_region were to be
combined, the result would look like the figure to the left. This is
not desired. It is necessary to apply a clipping region to the
Settings control.
This clipping region will be a circle with the same width and
height as the control itself. Being a circle, centered at the
center of the control, it will also have the effect of rounding the
outer edges of the teeth.
The
clipping_region
is created by the
create_clipping_region
method.
Region create_clipping_region ( int control_height )
{
int height = CONTROL_HEIGHT - 2 * OFFSET;
GraphicsPath path = new GraphicsPath ( );
Region region = null;
float scale = 0.0F;
Matrix transform = new Matrix ( );
scale = ( float ) control_height /
( float ) CONTROL_HEIGHT;
transform.Scale ( scale, scale );
path.AddEllipse ( new Rectangle (
new Point ( OFFSET, OFFSET ),
new Size ( ( height - 1 ),
( height - 1 ) ) ) );
path.Transform ( transform );
region = new Region ( path );
path.Dispose ( );
transform.Dispose ( );
return ( region );
}
This method performs the following:
- Calculates the scale that will scale the clipping region and
places the computed value into the transformation matrix.
- Adds a clipping region to the path.
- Applies the transformation matrix to the path.
- Creates a region adding the path to the region.
- Returns the region.
When the
clipping_region is applied
to the combined
annulus_region and
teeth_region,
the result would appear like the figure to the right. This is
the desired graphic image.
We now have all of the pieces that make up the Settings control
graphic image. The
create_control_region
collects the components and combines them into the
control_region.
Region create_control_region ( int new_height,
Region control_region )
{
Region annulus_region = null;
Region clipping_region = null;
Region teeth_region = null;
control_height = new_height;
this.Height = control_height;
this.Width = control_height;
annulus_region = create_annulus_region (
control_height );
teeth_region = create_teeth_region (
control_height );
clipping_region = create_clipping_region (
control_height );
if ( control_region != null )
{
control_region.Dispose ( );
}
control_region = new Region ( );
control_region.MakeEmpty ( );
control_region.Union ( annulus_region );
control_region.Union ( teeth_region );
control_region.Intersect ( clipping_region );
annulus_region.Dispose ( );
annulus_region = null;
clipping_region.Dispose ( );
clipping_region = null;
teeth_region.Dispose ( );
teeth_region = null;
return ( control_region );
}
This method:
- Sets the height and width of the control to the value passed as
new_height.
- Creates an
annulus_region.
- Creates a
teeth_region.
- Creates a
clipping_region.
- Deletes any existing
control_region and
creates a new one.
- Invokes
MakeEmpty
[^]
to initialize the
control_region to an
empty interior. See
Lessons Learned.
- Forms the
union
[^]
of the
annulus_region
with the
teeth_region resulting in
a preliminary
control_region.
- Forms the
intersection
[^]
between the preliminary
control_region
and the
clipping_region thereby
creating the final
control_region.
- Returns the final
control_region.
Define the Class Constructor
TOC
The constructor for all Hover-Click controls takes the form
public <control-name> ( )
{
this.SetStyle ( ( ControlStyles.DoubleBuffer |
ControlStyles.UserPaint |
ControlStyles.AllPaintingInWmPaint ),
true );
this.UpdateStyles ( );
this.Disposed += new EventHandler ( memory_cleanup );
<create_control_region>;
}
<control-name> is the name of the Hover-Click control
(i.e., "Favorites", "Home", "Settings").
SetStyle
[^]
enables or disables the styles passed as its parameters. In this
case, the styles are being set. From
ControlStyles Enumeration
[^]:
DoubleBuffer
| | If true, drawing is performed in a buffer, and after it
completes, the result is output to the screen.
Double-buffering prevents flicker caused by the redrawing of
the control. If you set DoubleBuffer to true, you should also
set UserPaint and AllPaintingInWmPaint to true.
|
UserPaint
| | If true, the control paints itself rather than the operating
system doing so. If false, the Paint event is not raised. This
style only applies to classes derived from Control.
|
AllPaintingInWmPaint
| | If true, the control ignores the window message WM_ERASEBKGND
to reduce flicker. This style should only be applied if the
UserPaint bit is set to true.
|
UpdateStyles
[^]
forces the new styles to be applied to the control.
Disposed
[^]
identifies the method that handles the disposing of the control. See
Perform Memory Cleanup, above.
<create_control_region> creates the control region with
appropriate parameters:
Favorites | |
control_region = create_control_region ( CONTROL_HEIGHT,
star,
control_region );
|
Home | |
control_region = create_control_region ( CONTROL_HEIGHT,
home,
control_region );
|
Settings | |
control_region = create_control_region ( CONTROL_HEIGHT,
control_region );
|
Handle Events
TOC
Once instantiated, all Hover-Click controls are event driven. This
means that events, external to the control (e.g., hovering over the
control, clicking on the control, etc.), cause the controls to
react. Reaction to external events occurs through the controls'
event handlers.
OnMouseClick Event Handler
TOC
When the user clicks the mouse over any part of the control, the
MouseClick
[^]
event is raised. However, just because the mouse has been clicked
does not mean that the click occurred within the control's graphic
image. To react to the user's click on the control, we will use the
OnMouseClick
[^]
method to be notified of the click and use the
control_region
to perform the hit test required to insure that the click occurred
on the control's graphic image.
protected override void OnMouseClick ( MouseEventArgs e )
{
base.OnMouseClick ( e );
if ( control_region != null )
{
if ( control_region.IsVisible (
new Point ( e.X, e.Y ) ) )
{
if ( UserClicked != null )
{
UserClicked ( this, EventArgs.Empty );
}
}
}
}
The
OnMouseClick method
performs the following actions:
- Raises the base class's
OnMouseClick method so
that all registered delegates receive the event.
- Insures that the
control_region exists.
- Uses
IsVisible
[^]
to determine if the mouse was in the
control_region
when the click occurred.
- Tests to insure that there are
subscribers to the
UserClicked event.
- If there are subscribers to the
UserClicked event,
raises the
UserClicked event.
If a class wishes to be notified of a mouse click in a Hover-Click
control, it must
register
[^]
a UserClicked event handler.
In the
Hover-Click Demonstration
project, using the Settings control as an example, this is
accomplished by first declaring that the method
settings_UserClicked
be used to capture and process the event.
settings.UserClicked +=
new Settings.Settings.UserClickedHandler (
settings_UserClicked );
The
settings_UserClicked
method is declared as:
void settings_UserClicked ( object sender,
EventArgs e )
{
if ( settings_dialog == null )
{
settings_dialog = new SettingsDialog ( );
}
( ( SettingsDialog ) settings_dialog ).initialize_GUI ( );
if ( settings_dialog.ShowDialog ( ) == DialogResult.OK )
{
}
else
{
}
}
Note that the
settings_UserClicked
method opens a dialog box, presumably to collect settings.
OnMouseLeave Event Handler
TOC
When the mouse pointer is moved wholly out of the control
boundaries the
MouseLeave
[^]
event is raised. When that occurs, the fill
color for the control must be reset. The
OnMouseLeave
[^]
method performs that task.
protected override void OnMouseLeave ( EventArgs e )
{
base.OnMouseLeave ( e );
if ( hovering )
{
if ( control_brush != null )
{
control_brush.Dispose ( );
}
control_brush = new SolidBrush ( control_color );
this.Invalidate ( );
hovering = false;
}
}
OnMouseLeave method
performs the following actions:
- Raises the base class's
OnMouseLeave method so
that all registered delegates receive the event.
- If the mouse pointer was hovering (i.e., in the
control_region),
- If the
control_brush
exists, disposes of the
control_brush.
- Recreates
control_brush using
the
control_color.
- Invoke this.
Invalidate
[^]
to redraw the control.
- Resets hovering to false.
OnMouseMove Event Handler
TOC
As the mouse pointer is moved over the face of the control, the
MouseMove
[^]
event is raised. When that occurs, the fill
color for the control may need to be reset. The
OnMouseMove
[^]
method performs that task.
protected override void OnMouseMove ( MouseEventArgs e )
{
bool was_hovering = hovering;
base.OnMouseMove ( e );
hovering = false;
if ( control_region != null )
{
hovering = ( control_region.IsVisible (
new Point ( e.X, e.Y ) ) );
}
if ( was_hovering != hovering )
{
if ( control_brush != null )
{
control_brush.Dispose ( );
}
if ( hovering )
{
control_brush = new SolidBrush ( hover_color );
}
else
{
control_brush = new SolidBrush ( control_color );
}
this.Invalidate ( );
}
}
The coordinates of the mouse are contained in
the MouseEventArgs passed to
the
OnMouseMove method.
OnMouseMove method
performs the following actions:
- Raises the base class's
OnMouseMove method so
that all registered delegates receive the event.
- Tests to insure that the
control_region exists.
- If the
control_region exists,
uses
IsVisible
[^]
to determine if the mouse was in the
control_region
when the click occurred.
- If the earlier value of
hovering is not equal to
the new value of
hovering:
- If the
control_brush
exists, dispose of the
control_brush.
- Create a new
control_brush
that has the appropriate color.
- Invoke this.
Invalidate
[^]
to redraw the control.
OnPaint Event Handler
TOC
Whenever the system recognizes that a control needs to be repainted,
it raises the
Paint
[^]
event. There are any
number of reasons that the Paint event might be raised, some of
which include:
- The control is resized.
- A hidden area of the control becomes visible.
- The control itself requests it.
When the Paint event is raised, the control must be redrawn. The
OnPaint
[
^]
method performs that task.
protected override void OnPaint ( PaintEventArgs e )
{
base.OnPaint ( e );
e.Graphics.FillRegion ( control_brush, control_region );
if ( Outline )
{
Utilities.FrameRegion.frame_region ( e.Graphics,
control_region );
}
}
The
OnPaint method
performs the following actions:
- Calls the base class's
OnPaint method so
that all registered delegates receive the event.
- Fills the
control_region using the
control_brush.
- If the
Outline
property is
true,
frames (draws a border around) the
control_region.
OnResize Event Handler
TOC
Whenever the system recognizes that a control's size is changing,
it raises the
ReSize
[^]
event. When the ReSize event is raised, the control must be redrawn.
The
OnResize
[^]
method performs that task.
protected override void OnResize ( EventArgs e )
{
base.OnResize ( e );
<create_control_region>;
this.Invalidate ( );
}
where <create_control_region> creates the control region with
appropriate parameters, as above.
OnResize method
performs the following actions:
- Calls the base class's
OnResize method so
that all registered delegates receive the event.
- Recreates the
control_region.
- Invoke this.
Invalidate
[^]
to redraw the control.
Get/Set Control Properties
TOC
All Hover-Click controls have four properties (see
Control_Properties, above). Their
values are collected by classic Getter Setter methods. These methods
are depicted, without comment, in the following subsections.
Control_Color
TOC
[ Category ( "Appearance" ),
Description ( "Gets/Sets color of control" ),
DefaultValue ( typeof ( Color ), "White" ),
Bindable ( true ) ] public Color Control_Color
{
get
{
return ( control_color );
}
set
{
if ( value != control_color )
{
control_color = value;
this.Invalidate ( );
}
}
}
Control_Height
TOC
[ Category ( "Appearance" ),
Description ( "Gets/Sets control height" ),
DefaultValue ( typeof ( int ), "CONTROL_HEIGHT" ),
Bindable ( true ) ] public int Control_Height
{
get
{
return ( control_height );
}
set
{
if ( value != control_height )
{
control_region = create_control_region (
value,
control_region );
this.Invalidate ( );
}
}
}
Hover_Color
TOC
[ Category ( "Appearance" ),
Description ( "Gets/Sets color of control when mouse hovers" ),
DefaultValue ( typeof ( Color ), "LightSkyBlue" ),
Bindable ( true ) ] public Color Hover_Color
{
get
{
return ( hover_color );
}
set
{
if ( value != hover_color )
{
hover_color = value;
this.Invalidate ( );
}
}
}
Outline
TOC
[ Category ( "Appearance" ),
Description ( "Gets/Sets whether outline is to be drawn" ),
DefaultValue ( typeof ( bool ), "true" ),
Bindable ( true ) ] public bool Outline
{
get
{
return ( outline );
}
set
{
if ( value != outline )
{
outline = value;
this.Invalidate ( );
}
}
}
Lessons Learned
TOC
During the development of these Hover-Click controls, I uncovered
some peculiarities of regions that I had not encountered before. In
my past programming, I used regions for much the same purposes as I
used them in this article. However, the earlier regions were single
object regions, not combinations using unions.
Also I never framed the regions. The regions was always visible
against their backgrounds. Clearly for the Hover-Click controls, this
was not the case.
Region.MakeEmpty
TOC
The Region.Union method takes any of the following forms
Each updates the region to the union of itself and the GDI object
that is the parameter to the Region.Union method.
However, this is not the case in the following fragment:
Region final_region = null;
GraphicsPath first_path = null;
Region first_region = null;
GraphicsPath second_path = null;
Region second_region = null;
first_path = new GraphicsPath ( some polygon )
first_region = new Region ( first_path );
second_path = new GraphicsPath ( some other polygon )
second_region = new Region ( second_path );
final_region = new Region ( );
final_region.Union ( first_region );
final_region.Union ( second_region );
final_region fills the monitor. When queried, Microsoft responded:
Since the constructor creates an infinite region, try this:
final_region = new Region ( );
final_region.MakeEmpty ( );
final_region.Union ( first_region );
final_region.Union ( second_region );
Lesson
| |
In all cases where regions are to be combined using
Region.Union ( Region), add Region.MakeEmpty immediately after the
constructor of the target region
|
Applying Clipping Regions
TOC
There are two ways in which to apply clipping regions:
- Use the
SetClip
[^]
method to apply the clipping region to the
Graphics
[^] object passed through the
PaintEventArgs
[^].
- Use the
Region.Intersect
[^]
method to combine an existing region with the
clipping region.
If we use the first method, and then invoke
FrameRgn
[^], we obtain an image like the figure to
the left.
It appears that the clipping region established by SetClip method is
associated with the Graphics object and not with the
control_region.
Clipping appears to occur on the
control_region when the
image is painted. When the framing occurs, it appears to operate on
the
control_region as if clipping
had not been applied.
If we use the second method, and then invoke FrameRgn, we obtain an
image like the figure to the right.
So it appears that the
clipping_region
must be applied to the
control_region
before the FrameRgn method is invoked.
Lesson
| |
Combine the clipping region with the control region using
Region.Intersect before invoking FrameRgn.
|
Hover-Click Demonstration
TOC
The demonstration project allows developers to test the ways in
which the Hover-Click controls work. It also provides a framework to
test new Hover-Click controls.
Conclusion
TOC
This article has presented a template for the implementation of user
drawn Hover-Click controls.
References
TOC
Development Environment
TOC
Hover-Click controls were developed in the following environment:
|
IrfanView for Windows Version 4.36
[^]
|
| Microsoft Visual C# 2008 |
| Microsoft Visual Studio 2008 Professional |
| Microsoft .Net Framework Version 3.5 SP1 |
| Microsoft Windows 7 Professional SP1 |
| Microsoft Paint (part of Windows 7 Professional) |
| Microsoft Office PowerPoint 2003 SP3 |
History
TOC
11/26/2013
| | Original Article
|
12/04/2013
| | Revised the OnMouseLeave method; added borders to two PNG
images; repaired typos; added items to the development
environment.
|