Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Anatomy of Hover-Click Controls

4.97/5 (19 votes)
3 Dec 2013CPOL22 min read 29.9K   1.4K  
This article presents a template for the implementation of user drawn Hover-Click controls.

Introduction TOC

Settings Control

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

Favorites Control Visual Properties

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
Settings Control Cursor Location

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.

C#
// ******************************** control delegate and event

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.

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

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

C#
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.

Graphics Device Interface (GDI) [^] TOC

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.
Settings Control Calculations.png
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.
Settings Control Rotate Translate

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.

Constructing Home and Favorites

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:

C#
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:

C#
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.

C#
// ************************************* create_control_region

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:

  1. Sets the height and width of the control to the value passed as new_height.
  2. Calculates the scale that will be used to scale the graphics object and places the result in the transformation matrix.
  3. Adds a polygon, defined by the points array parameter, to the path.
  4. Applies the transformation matrix.
  5. Deletes any existing control region and creates a new control region.
  6. 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.

Settings Control Construction

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.

C#
// ************************************* create_annulus_region

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 );
                                // define outer region
    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 );
                                // define inner region
    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 );
                                // exclude inner from outer
    outer_region.Exclude ( inner_region );
                                // dispose of intermediates
    inner_path.Dispose ( );
    inner_region.Dispose ( );
    outer_path.Dispose ( );
    transform.Dispose ( );

    return ( outer_region );
    }

This method performs the following:

  1. 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).
  2. Creates the outer path that contains the outer circle.
  3. Scales the outer path and places it into the outer region.
  4. Creates the inner path that contains the inner circle.
  5. Scales the inner path and places it into the inner region.
  6. Excludes the inner region from the outer region, effectively opening a hole in the outer region.
  7. Returns the revised outer region, which is the annulus region.

The helper method round takes the forms:

C#
// ***************************************************** round

// http://en.wikipedia.org/wiki/Rounding

public static int round ( float parameter )
    {

    return ( ( int ) ( parameter + 0.5F ) );
    }

// ***************************************************** round

// http://en.wikipedia.org/wiki/Rounding

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.

C#
// *************************************** create_teeth_region

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 ( );       // must do this!!

    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++ )
        {
                                // do not move transform or
                                // path outside this loop
        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:

  1. 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.
  2. Invokes MakeEmpty [^] to initialize the target region to an empty interior. See Lessons Learned.
  3. 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.
  4. Computes tooth_height and tooth_width based on the computed value of d.
  5. 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.
  6. After all teeth are created, returns the region.

The helper new_gear_tooth is

C#
// ******************************************** new_gear_tooth

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:

C#
// ************************************************* deg_2_rad

public static double deg_2_rad ( int degrees )
    {

    return ( ( ( double ) degrees / 180.0 ) *
             System.Math.PI );
    }

// *************************************************** cos_deg

public static float cos_deg ( int  degrees )
    {

    return ( ( float ) System.Math.Cos ( deg_2_rad (
                                             degrees ) ) );
    }

// *************************************************** sin_deg

public static float sin_deg ( int  degrees )
    {

    return ( ( float ) System.Math.Sin ( deg_2_rad (
                                             degrees ) ) );
    }
Unclipped Control Region

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.

C#
// ************************************ create_clipping_region

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:

  1. Calculates the scale that will scale the clipping region and places the computed value into the transformation matrix.
  2. Adds a clipping region to the path.
  3. Applies the transformation matrix to the path.
  4. Creates a region adding the path to the region.
  5. Returns the region.
Clipped Control 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.

C#
// ************************************* create_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:

  1. Sets the height and width of the control to the value passed as new_height.
  2. Creates an annulus_region.
  3. Creates a teeth_region.
  4. Creates a clipping_region.
  5. Deletes any existing control_region and creates a new one.
  6. Invokes MakeEmpty [^] to initialize the control_region to an empty interior. See Lessons Learned.
  7. Forms the union [^] of the annulus_region with the teeth_region resulting in a preliminary control_region.
  8. Forms the intersection [^] between the preliminary control_region and the clipping_region thereby creating the final control_region.
  9. Returns the final control_region.

Define the Class Constructor TOC

The constructor for all Hover-Click controls takes the form

C#
// ********************************************** <control-name>

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    
C#
control_region = create_control_region ( CONTROL_HEIGHT,
                                         star,
                                         control_region );
Home
C#
control_region = create_control_region ( CONTROL_HEIGHT,
                                         home,
                                         control_region );
Settings
C#
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.

C#
// ********************************************** OnMouseClick

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:

  1. Raises the base class's OnMouseClick method so that all registered delegates receive the event.
  2. Insures that the control_region exists.
  3. Uses IsVisible [^] to determine if the mouse was in the control_region when the click occurred.
  4. Tests to insure that there are subscribers to the UserClicked event.
  5. 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.

C#
settings.UserClicked +=
    new Settings.Settings.UserClickedHandler (
                                settings_UserClicked );

The settings_UserClicked method is declared as:

C#
// ************************************** settings_UserClicked

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 )
        {
        // retrieve settings
        }
    else
        {
        // do something with Cancel
        }
    }

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.

C#
// ********************************************** OnMouseLeave

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:

  1. Raises the base class's OnMouseLeave method so that all registered delegates receive the event.
  2. 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.

C#
// *********************************************** OnMouseMove

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:

  1. Raises the base class's OnMouseMove method so that all registered delegates receive the event.
  2. Tests to insure that the control_region exists.
  3. If the control_region exists, uses IsVisible [^] to determine if the mouse was in the control_region when the click occurred.
  4. 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.
C#
// *************************************************** OnPaint

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:

  1. Calls the base class's OnPaint method so that all registered delegates receive the event.
  2. Fills the control_region using the control_brush.
  3. 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.

C#
// ************************************************** OnResize

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:

  1. Calls the base class's OnResize method so that all registered delegates receive the event.
  2. Recreates the control_region.
  3. 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
C#
// ********************************************* Control_Color

[ 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
C#
// ******************************************** Control_Height

[ 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
C#
// *********************************************** Hover_Color

[ 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
C#
// *************************************************** Outline

[ 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:

C#
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:

en-us
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:

  1. Use the SetClip [^] method to apply the clipping region to the Graphics [^] object passed through the PaintEventArgs [^].
  2. Use the Region.Intersect [^] method to combine an existing region with the clipping region.
Control Region With Clipping Framed

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.

Desired Control Region

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

Hover-Click Controls Demonstration

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.

License

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