移动边框按钮






4.54/5 (9投票s)
介绍如何创建带有移动边框的按钮
需求
有时,区分一组周围按钮中的特定按钮很有意思。例如,在“已知颜色调色板工具”中,当通过编程或用户操作选择颜色按钮时,需要将其与其他未选择的按钮区分开来。目前,该工具没有区分已选按钮的方法。

选择区分技术的部分难点在于可用方法众多。因为我正在修订该工具,所以我有一个测试框架来协助我。
工具中出现的颜色按钮是 Custom_Button 类的实例。我选择的第一个方法是简单地在选定的颜色按钮周围放置边框。

这在 Custom_Button 构造函数中完成,通过阻止按钮绘制自己的边框。
// ********************************************* 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;
}
实际的边框颜色源自 Custom_Button 的 BackGround 颜色。
// ******************************************** 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 ) );
}
contrast_color 返回的颜色是 Color.Black 或 Color.White。边框的实际绘制发生在 OnPaint 事件处理程序中。
// *************************************************** 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 ) );
}
我对这个结果并不满意,因为我认为它没有充分区分选定的按钮。所以,接下来的尝试是增大按钮的尺寸,并提供与上面相同的边框。

为了达到这个目的,添加了两个新方法:ExaggerateButton 和 RestoreButton。
// ****************************************** 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;
}
边框的实际绘制发生在 OnPaint 事件处理程序中,如 上文 所述。再次,我对结果不满意,并决定进一步放大选定的按钮。

移动边框实现
在这一点上,很明显,增加按钮的大小只会造成干扰。这个想法促使我想到可以在选定的按钮周围放置一个移动边框。

虽然,由于此图的静态性质,读者无法看到边框的移动,但它确实存在。通过下载演示程序,读者可以亲眼看到移动边框确实很突出。
经过一些试验,我决定通过用虚线笔绘制边框来实现移动边框(之前我试过使用多边形)。笔的创建方式如下。
// ********************************** 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;
}
contrasting_color 源自按钮的 BackColor 属性。笔的宽度和虚线长度由 PenWidth 和 DashLength 属性指定。当 DashLength 或 PenWidth 属性发生变化时,在初始化或每次都会调用 create_moving_border_pen。
因为我们谈论的是一个移动对象,也就是一个动画对象,所以我们需要一个计时器。移动边框的计时器在 MoveButtonBorder 属性代码中启动和停止。
// ****************************************** 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;
}
}
}
计时器每次在 TimerInterval 属性中指定的时间间隔到期时,都会触发 tick 事件处理程序。 tick 事件处理程序如下所示。
// ****************************************************** 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
{
}
}
tick 事件处理程序调用 Refresh,这反过来又导致 OnPaint 事件被引发。OnPaint 事件处理程序如下所示。
// *************************************************** 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 ( );
}
}

创建移动边框图形时,必须计算每个边(顶部、左侧、底部和右侧)的起始和重置位置。在左边的图中,绿色的正方形代表起始笔位置,红色的正方形是笔重置回起始位置的位置。
开始和结束位置由 initialize_starts_and_ends 计算。
// ******************************** 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;
}
每次计时器的经过时间间隔到期时,OnPaint 事件处理程序都会调用 create_moving_border_graphic,该函数创建移动边框图形。
// ***************************** 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 ] );
}
}
当边框已渲染到 OnPaint PaintEventArgs 中传递的 Graphic 对象上时,必须修改每个边(顶部、左侧、底部和右侧)的起始位置。 revise_start_ats 执行此操作,并在必要时将起始值重置为其初始化状态。
// ****************************************** 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 );
}
}
结论
我相信移动边框可以将选定的按钮与周围的按钮区分开来。因此,我将在已知颜色调色板工具的修订版中添加移动边框按钮。