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

Moving Border Buttons

4.54/5 (9 votes)
14 May 2015CPOL3 min read 13.2K   871  
Presents how to create a button with a moving border

The Need

Occasionally, it is of interest to distinguish a specific button from a group of surrounding buttons. For example, in the case of the Known Colors Palette Tool, when a color button is chosen, either programmatically or by a user action, it needs to be distinguished from the other non-chosen button. Currently, the tool contains no method of distinguishing a chosen button.

Not Distinguished

Part of the difficulty in choosing a distinguishing technique is the large number of methods available. Because I was revising the tool, I had a test framework available to assist me.

The color buttons that appear in the tool are instances of the class Custom_Button. The first method I chose was simply to place a border around the chosen color button.

Highlighted Border

This was accomplished in the Custom_Button constructor by preventing the button from drawing its own borders.

C#
// ********************************************* Custom_Button

public Custom_Button ( ) : base ( )
    {
                                // prevent button from drawing
                                // its own border
    FlatAppearance.BorderSize = 0;
    FlatAppearance.BorderColor = Color.Black;
    FlatStyle = System.Windows.Forms.FlatStyle.Flat;

    border_width = 1;
    }

The actual border color was derived from the Custom_Button's BackGround color.

C#
// ******************************************** contrast_color

// http://stackoverflow.com/questions/1855884/
//     determine-font-color-based-on-background-color

Color contrast_color ( Color color )
    {
    double  a = 0.0;
    int     d = 0;
                                // counting the perceptive
                                // luminance; human eye favors
                                // green color...
    a = 1.0 - ( 0.299 * color.R +
                0.587 * color.G +
                0.114 * color.B ) / 255.0;

    if ( a < 0.5 )
        {
        d = 0;                  // bright color - black font
        }
    else
        {
        d = 255;                // dark color; white font
        }

    return ( Color.FromArgb ( d, d, d ) );
    }

The color returned by contrast_color is either Color.Black or Color.White. The actual drawing of the border occurs in the OnPaint event handler.

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

protected override void OnPaint ( PaintEventArgs e )
    {
                                // have base class paint the
                                // button normally
    base.OnPaint ( e );
                                // draw border using given
                                // color and width
    e.Graphics.DrawRectangle (
        new Pen ( FlatAppearance.BorderColor,
                  border_width ),
        new Rectangle ( 0,
                        0,
                        Size.Width - 1,
                        Size.Height - 1 ) );
    }

I was not pleased with this result as I did not think that it differentiated the chosen button enough. So the next attempt was to size the button larger and provide the same border as above.

Button Sized Larger

To accomplish this end, two new methods, ExaggerateButton and RestoreButton, were added.

C#
// ****************************************** ExaggerateButton

public void ExaggerateButton ( )
    {
    int     added_size = 0;
    Point   location;
    Size    size;

    added_size = Form_Constants.COLOR_SQUARE_SEPARATION;
    location = new Point ( this.Location.X - added_size,
                           this.Location.Y - added_size );
    size = new Size ( this.Size.Width + 2 * added_size,
                      this.Size.Height + 2 * added_size );
    this.Location = location;
    this.Size = size;

    this.Parent.Controls.SetChildIndex (
                    this,
                    ElevatedZOrder );
    CurrentZOrder = ElevatedZOrder;

    border_width = 5;
    FlatAppearance.BorderColor =
        contrast_color ( this.Custom_Button_Color.color );
    }

// ********************************************* RestoreButton

public void RestoreButton ( )
    {
    int     added_size = 0;
    Point   location;
    Size    size;

    added_size = Form_Constants.COLOR_SQUARE_SEPARATION;
    location = new Point ( this.Location.X + added_size,
                           this.Location.Y + added_size );
    size = new Size ( this.Size.Width - 2 * added_size,
                      this.Size.Height - 2 * added_size );
    this.Location = location;
    this.Size = size;

    this.Parent.Controls.SetChildIndex (
                    this,
                    BaseZOrder );
    CurrentZOrder = BaseZOrder;

    border_width = 1;
    FlatAppearance.BorderColor = Color.Black;
    }

The actual drawing of the border occurs in the OnPaint event handler, as above. Again, I was not pleased with the result and decided to further enlarge the chosen button.

Button Sized Much Larger

Moving Border Implementation

At this point, it became apparent that increasing the size of the button was just causing a distraction. This realization led me to the thought that I could place a moving border around the chosen button.

Moving Border

Although, because of the static nature of this figure, readers cannot see the border motion, it is there. By downloading the demonstration, readers can see for themselves that a moving border does, in fact, stand out.

After some experimentation, I decided to implement a moving border by drawing the border with a dash-dot pen (earlier I tried polygons). The pen is created as follows.

C#
// ********************************** create_moving_border_pen

/// <summary>
/// creates the pen that will be used to draw the moving
/// border
/// </summary>
void create_moving_border_pen ( )
    {

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

    moving_border_pen =
        new Pen ( contrasting_color ( BackColor ),
                  PenWidth );

    dash_pattern = new float [ ]
                        {
                        DashLength / PenWidth,
                        DashLength / PenWidth
                        };

    moving_border_pen.DashPattern = dash_pattern;
    moving_border_pen.DashOffset = 0.0F;
    moving_border_pen.DashStyle = DashStyle.Custom;
    moving_border_pen.EndCap = LineCap.Flat;
    moving_border_pen.StartCap = LineCap.Flat;
    }

The contrasting_color is derived from the button's BackColor property. The pen's width and dash-dot length are specified by the PenWidth and DashLength properties. create_moving_border_pen is invoked on initialization or whenever either the DashLength or PenWidth properties change.

Because we are talking about a moving object, that is an animated object, we need a timer. The timer for the moving border is started and stopped in the MoveButtonBorder property code.

C#
// ****************************************** MoveButtonBorder

[ Category ( "Appearance" ),
  Description ( "Specifies if button border should move" ),
  DefaultValue ( typeof ( bool ), "false" ),
  Bindable ( true ) ] public bool MoveButtonBorder
    {

    get
        {
        return ( move_button_border );
        }

    set
        {
        move_button_border = value;
        if ( move_button_border )
            {
                                // prevent button from drawing
                                // its own border
            FlatAppearance.BorderSize = 0;
            FlatStyle = FlatStyle.Flat;

            if ( timer == null )
                {
                timer = new System.Timers.Timer ( );
                timer.Elapsed +=
                    new ElapsedEventHandler ( tick );
                timer.Interval = timer_interval;
                timer.Start ( );
                }
            }
        else
            {
            if ( timer != null )
                {
                if ( timer.Enabled )
                    {
                    timer.Elapsed -=
                        new ElapsedEventHandler ( tick );
                    timer.Stop ( );
                    }
                timer = null;
                }
                                // allow button to draw its
                                // own border
            FlatAppearance.BorderSize = 1;
            FlatStyle = FlatStyle.Standard;
            }
        }
    }

The timer triggers the tick event handler each time that the interval, specified in the TimerInterval property, expires. The tick event handler follows.

C#
// ****************************************************** tick

/// <summary>
/// handles the timer's elapsed time event
/// </summary>
/// <note>
/// this event handler executes in a thread separate from the
/// user interface thread and therefore needs to use Invoke
/// </note>
void tick ( object           source,
            ElapsedEventArgs e )
    {

    try
        {
        if ( this.InvokeRequired )
            {
            this.Invoke (
                new EventHandler (
                    delegate
                        {
                        this.Refresh ( );
                        }
                    )
                );
            }
        else
            {
            this.Refresh ( );
            }
        }
    catch
        {

        }
    }

The tick event handler invokes Refresh that, in turn, causes the OnPaint event to be raised. The OnPaint event handler follows.

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

/// <summary>
/// the Paint event handler
/// </summary>
/// <note>
/// the button is drawn in the usual manner by the base
/// method; then a border is added if MoveButtonBorder is
/// true; note too that MoveButtonBorder makes appropriate
/// changes to FlatAppearance and FlatStyle
/// </note>
protected override void OnPaint ( PaintEventArgs e )
    {
                                // have base class paint the
                                // button normally
    base.OnPaint ( e );
                                // add the moving border only
                                // if border movement was
                                // specified
    if ( MoveButtonBorder )
        {
        if ( !initialized )
            {
            initialize_starts_and_ends ( );
            create_moving_border_pen ( );
            }

        create_moving_border_graphic ( );
        moving_border_graphic.RenderGraphicsBuffer (
                                e.Graphics );
        revise_start_ats ( );
        }
    }
Calculate Start End

When creating the moving border graphic, the starting and reset position for each edge (top, left, bottom, and right) must be computed. In the figure to the left, the green square represents the starting pen position and the red square is the position at which the pen is reset back to the starting position.

The start and end positions are calculated by initialize_starts_and_ends.

C#
// ******************************** initialize_starts_and_ends

/// <summary>
/// performs the initialization of the TOP, RIGHT, BOTTOM, and
/// LEFT edges starting and ending points; initialization is
/// performed by the OnPaint event handler when the button's
/// size is known
/// </summary>
void initialize_starts_and_ends ( )
    {
                                // initialization is performed
                                // once during OnPaint when
                                // the button's size is known
    for ( int i = 0; ( i < EDGES ); i++ )
        {
        switch ( i )
            {
            case TOP:
                start_at [ TOP ] = new Point (
                    -( DashLength - 1 ),
                    ( PenWidth / 2 ) );
                end_at [ TOP ] = start_at [ TOP ];
                end_at [ TOP ].X = this.Width +
                                   DashLength;
                break;
            case RIGHT:
                start_at [ RIGHT ] = new Point (
                    this.Width - ( PenWidth / 2 ) - 1,
                    -( DashLength - 1 ) );
                end_at [ RIGHT ] = start_at [ RIGHT ];
                end_at [ RIGHT ].Y = this.Height +
                                     DashLength;
                break;

            case BOTTOM:
                start_at [ BOTTOM ] = new Point (
                    this.Width + ( DashLength - 1 ),
                    this.Height - ( PenWidth / 2 ) - 1 );
                end_at [ BOTTOM ] = start_at [ BOTTOM ];
                end_at [ BOTTOM ].X = -DashLength;
                break;

            case LEFT:
                start_at [ LEFT ] = new Point (
                    ( PenWidth / 2 ),
                    this.Height + ( DashLength - 1 ) );
                end_at [ LEFT ] = start_at [ LEFT ];
                end_at [ LEFT ].Y = -DashLength;
                break;

            default:
                break;
            }
        }

    initialized = true;
    }

Each time that the timer's elapsed interval expires, the OnPaint event handler invokes create_moving_border_graphic that creates the moving border graphic.

C#
// ***************************** create_moving_border_graphic

/// <summary>
/// creates the graphic image of the moving border that will be
/// rendered on the button's surface
/// </summary>
void create_moving_border_graphic ( )
    {
                                // delete existing buffer
    if ( moving_border_graphic != null )
        {
        moving_border_graphic = moving_border_graphic.
                               DeleteGraphicsBuffer ( );
        }
                                // create a new buffer
    moving_border_graphic = new GraphicsBuffer ( );
    moving_border_graphic.InitializeGraphicsBuffer (
                                              "Moving",
                                              this.Width,
                                              this.Height );
    moving_border_graphic.Graphic.SmoothingMode =
        SmoothingMode.HighQuality;
                                // draw the border edges
    for ( int i = 0; ( i < EDGES ); i++ )
        {
        moving_border_graphic.Graphic.DrawLine (
            moving_border_pen,
            start_at [ i ],
            end_at [ i ] );
        }
    }

When the borders have been rendered onto the Graphic object, passed in the OnPaint PaintEventArgs, the start positions for each edge (top, left, bottom, and right) must be revised. revise_start_ats performs this action and, if necessary resets the start values to their initialized state.

C#
// ****************************************** revise_start_ats

/// <summary>
/// revises the TOP, RIGHT, BOTTOM, and LEFT edges starting
/// point at each timer tick; revision is performed by the
/// OnPaint event handler
/// </summary>
void revise_start_ats ( )
    {

    start_at [ TOP ].X++;
    if ( start_at [ TOP ].X >= DashLength )
        {
        start_at [ TOP ].X = -( DashLength + 1 );
        }

    start_at [ RIGHT ].Y++;
    if ( start_at [ RIGHT ].Y >= DashLength )
        {
        start_at [ RIGHT ].Y = -( DashLength - 1 );
        }

    start_at [ BOTTOM ].X--;
    if ( start_at [ BOTTOM ].X <= this.Width - DashLength )
        {
        start_at [ BOTTOM ].X =
            this.Width + ( DashLength - 1 );
        }

    start_at [ LEFT ].Y--;
    if ( start_at [ LEFT ].Y <= this.Height - DashLength )
        {
        start_at [ LEFT ].Y =
            this.Height + ( DashLength - 1 );
        }
    }

Conclusion

I believe that the moving border distinguishes a chosen button from surrounding ones. As a result, I will add moving border buttons to the revision to the Known Colors Palette Tool.

License

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