65.9K
CodeProject 正在变化。 阅读更多。
Home

移动边框按钮

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.54/5 (9投票s)

2015年5月14日

CPOL

3分钟阅读

viewsIcon

13741

downloadIcon

971

介绍如何创建带有移动边框的按钮

需求

有时,区分一组周围按钮中的特定按钮很有意思。例如,在“已知颜色调色板工具”中,当通过编程或用户操作选择颜色按钮时,需要将其与其他未选择的按钮区分开来。目前,该工具没有区分已选按钮的方法。

Not Distinguished

选择区分技术的部分难点在于可用方法众多。因为我正在修订该工具,所以我有一个测试框架来协助我。

工具中出现的颜色按钮是 Custom_Button 类的实例。我选择的第一个方法是简单地在选定的颜色按钮周围放置边框。

Highlighted Border

这在 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 ) );
        }

我对这个结果并不满意,因为我认为它没有充分区分选定的按钮。所以,接下来的尝试是增大按钮的尺寸,并提供与上面相同的边框。

Button Sized Larger

为了达到这个目的,添加了两个新方法:ExaggerateButtonRestoreButton

    // ****************************************** 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 事件处理程序中,如 上文 所述。再次,我对结果不满意,并决定进一步放大选定的按钮。

Button Sized Much Larger

移动边框实现

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

Moving Border

虽然,由于此图的静态性质,读者无法看到边框的移动,但它确实存在。通过下载演示程序,读者可以亲眼看到移动边框确实很突出。

经过一些试验,我决定通过用虚线笔绘制边框来实现移动边框(之前我试过使用多边形)。笔的创建方式如下。

    // ********************************** 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 属性。笔的宽度和虚线长度由 PenWidthDashLength 属性指定。当 DashLengthPenWidth 属性发生变化时,在初始化或每次都会调用 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 ( );
            }
        }
Calculate Start End

创建移动边框图形时,必须计算每个边(顶部、左侧、底部和右侧)的起始和重置位置。在左边的图中,绿色的正方形代表起始笔位置,红色的正方形是笔重置回起始位置的位置。

开始和结束位置由 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 );
            }
        }

结论

我相信移动边框可以将选定的按钮与周围的按钮区分开来。因此,我将在已知颜色调色板工具的修订版中添加移动边框按钮。

© . All rights reserved.