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.
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.
This was accomplished in the
Custom_Button constructor by
preventing the button from drawing its own borders.
public Custom_Button ( ) : base ( )
{
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.
Color contrast_color ( Color color )
{
double a = 0.0;
int d = 0;
a = 1.0 - ( 0.299 * color.R +
0.587 * color.G +
0.114 * color.B ) / 255.0;
if ( a < 0.5 )
{
d = 0;
}
else
{
d = 255;
}
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.
protected override void OnPaint ( PaintEventArgs e )
{
base.OnPaint ( e );
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.
To accomplish this end, two new methods,
ExaggerateButton and
RestoreButton,
were added.
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 );
}
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.
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.
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.
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.
[ 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 )
{
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;
}
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.
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.
protected override void OnPaint ( PaintEventArgs e )
{
base.OnPaint ( e );
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 ( );
}
}
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.
void initialize_starts_and_ends ( )
{
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.
void create_moving_border_graphic ( )
{
if ( moving_border_graphic != null )
{
moving_border_graphic = moving_border_graphic.
DeleteGraphicsBuffer ( );
}
moving_border_graphic = new GraphicsBuffer ( );
moving_border_graphic.InitializeGraphicsBuffer (
"Moving",
this.Width,
this.Height );
moving_border_graphic.Graphic.SmoothingMode =
SmoothingMode.HighQuality;
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.
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.