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

圆形按钮 Windows 控件 - 不断缩小的圆

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (97投票s)

2006年9月27日

CPOL

6分钟阅读

viewsIcon

401685

downloadIcon

13608

一个 C# 中的 Windows 圆形按钮控件,带有设计器支持。

Round buttons

引言

很久以前,我试图找一个漂亮的圆形按钮控件。我没找到,于是就按照传统的做法,决定自己写一个。我“几乎”完成了它,但由于各种原因,它被归入了“以后再来做”的类别。按照它特有的风格,“以后”终于到来,我武装了我闪亮的新版Microsoft Visual C# 2005 Express Edition,决定尝试完成它。

虽然我自己这么说,但我认为这些按钮看起来不错——你得自己判断!它们在“实体”中看起来更好,而不是像这篇文章中的 JPEG 图那样。

背景

在我寻找圆形按钮控件的过程中,我偶然看到了几篇文章(包括伟大的 Chris Maunder 本人的一篇!),在我看来,我那微不足道的头脑认为里面有太多复杂的数学知识。而且,我一直在学习 C# 中的图形,并对 PathGradientBrush 这样的酷炫事物进行了大量实验,从非常出色的 Bob Powell 网站汲取了很多灵感。可能,偶然地,我不记得具体是如何的,我偶然发现了将渐变填充的、逐渐缩小的圆层叠在一起,并通过 LinearGradientBrushPathGradientBrush 来构建一个可观的 3D 按钮的想法。下面的图片说明了这一点。

将鼠标悬停在上面可查看描述。

The Button recess onlyThe Button recess with an edgeThe Button recess with an edge and an outer bevel

The Button recess with an edge and both bevelsThe Button recess with an edge, both bevels and a flat topThe Button recess with an edge, both bevels and a domed top

工作原理

是的,本质上,将很多圆一个叠一个地放在一起就是它的工作方式。该控件继承自 Button 类,并重写了 OnPaint 方法,所有的绘制都在这里完成。我添加了一些新属性:

  • RecessDepth - 按钮在包含的表面上凹陷的深度
  • BevelHeight - 按钮顶部“外部”斜角的尺寸
  • BevelDepth - “内部”斜角的尺寸
  • Dome - 按钮是否有“圆顶”顶部

这些属性都通过添加相应的属性装饰,被添加到了属性面板的 Button Appearance 类别中。此外,我还为 RecessDepth 属性编写了一个自定义的下拉式 UITypeEditor。如果没有 Chris Sells 的优秀著作 Windows Forms Programming in C#,我绝不可能做到这一点,我强烈推荐它——我不会尝试解释 UITypeEditor 的工作原理,因为它在在线章节中有详细介绍,该章节讨论了设计时 IDE 集成的所有方面(虽然我也拥有实体书!)。

注意:为了让 ToolboxBitmap 属性正常工作,我不得不添加这个占位符类,正如 Bob Powell 那位仁兄在本文中建议的那样:ToolboxBitmap

internal class resfinder
{
    // Trick from Bob Powell
}
.
.
.
.
[Description("Round (Elliptical) Button Control"),
    ToolboxBitmap(typeof(resfinder), "RoundButton.Images.RoundButton.bmp")]
    public class RoundButton : System.Windows.Forms.Button

值得注意的代码段

这是重写的 OnPaint 方法。没什么特别令人兴奋的,但我将其包含在此以供参考。

protected override void OnPaint(PaintEventArgs e)
{
    buttonColor = this.BackColor;
    edgeColor1 = ControlPaint.Light(buttonColor);
    edgeColor2 = ControlPaint.Dark(buttonColor);

    Graphics g = e.Graphics;
    g.SmoothingMode = SmoothingMode.AntiAlias;

    Rectangle buttonRect = this.ClientRectangle;
    edgeWidth = GetEdgeWidth(buttonRect);

    FillBackground(g, buttonRect);

    if (RecessDepth > 0)
    {
        DrawRecess(ref g, ref buttonRect);
    }

    DrawEdges(g, ref buttonRect);

    ShrinkShape(ref g, ref buttonRect, edgeWidth);

    DrawButton(g, buttonRect);

    DrawText(g, buttonRect);

    SetClickableRegion();

}

接下来是 DrawRecess 方法,它创造了按钮嵌入窗体表面的错觉。Blend 对象允许你指定在矩形的哪些点以及多少比例下,LinearGradientBrush 的两种颜色会被混合。我通过反复试验找到了这些参数,直到它们在我看来看起来合适,因此它们纯粹是主观的。ControlPaint.DarkControlPaint.Light 在这里非常有用,因为它们可以创建父背景颜色的更浅和更深的色调。当然,这假设我们要创造的错觉是窗体由一块实色的材料制成,而不是仍然是灰色但被涂成了另一种颜色。如果你更喜欢后者,只需将 Parent.BackColor 更改为 Color.FromKnownColor(KnownColor.Control)

我在这里发现的有趣之处在于“使用这个第二个较小的矩形……”部分。我在 BuildGraphicsPath 方法中再次使用了相同的技术,虽然它创建了更平滑的曲线,但我不知道它是如何以及为何能工作的。但话说回来,我们有多少人真的知道电视是如何工作的……?

protected virtual void DrawRecess(ref Graphics g, ref Rectangle recessRect)
{

    LinearGradientBrush recessBrush = new LinearGradientBrush(recessRect,
                                      ControlPaint.Dark(Parent.BackColor),
                                      ControlPaint.LightLight(Parent.BackColor),
                                      GetLightAngle(Angle.Up));
    // Blend colours for realism
    Blend recessBlend = new Blend();
    recessBlend.Positions = new float[] {0.0f,.2f,.4f,.6f,.8f,1.0f};
    recessBlend.Factors = new float[] {.2f,.2f,.4f,.4f,1f,1f};
    recessBrush.Blend = recessBlend;

    // Using this second smaller rectangle
    // smooths the edges - don't know why...?
    Rectangle rect2 = recessRect;
    ShrinkShape(ref g, ref rect2, 1);
    FillShape(g, recessBrush, rect2);

    ShrinkShape(ref g, ref recessRect, recessDepth); //orig

}

你会注意到源代码中有大量的 ShrinkShape(ref g, ref edgeRect, 1); 语句。这是创建“逐渐缩小的圆”的方法。我使用了一个 ref 参数,以便所讨论的矩形不断缩小。

要绘制圆顶顶部,我只需在 DrawButton 方法中使用这段代码。cColor 的默认值是 White,所以如果我们想要一个圆顶顶部,我们将 CenterColor 设置为白色,并根据按钮的大小计算 CenterPoint

pgb.CenterColor = buttonColor;

if (dome)
{
    pgb.CenterColor = cColor;
    pgb.CenterPoint =
        new PointF(buttonRect.X + buttonRect.Width / 8 + buttonPressOffset,
                   buttonRect.Y + buttonRect.Height / 8 + buttonPressOffset);
}

FillShape(g, pgb, buttonRect);

在按钮上绘制文本是通过 DrawText 方法完成的,如下所示。它使用了从基类 Button 继承的 FontForeColor 属性。我使用了我的 VerticalString 类来编写垂直文本,如果按钮的高度是其宽度的两倍以上。VerticalString 是之前 CodeProject 文章 这里 的主题,我已将源代码包含在项目下载中以供完整性。我还必须确保,在可能的情况下,按钮文本保持在按钮边界内。作为此过程的一部分,我不得不将文本的对齐方式从 ContentAlignment 转换为 StringAlignment。最后,我检查按钮是否被禁用,如果被禁用,我就将文本“灰化”。

protected void DrawText(Graphics g, Rectangle textRect)
{
    labelStrFmt = new StringFormat();
    labelBrush = new SolidBrush(this.ForeColor);
    labelFont = this.Font;    // Get the caller-specified font

    vs = new VerticalString();
    vs.TextSpread = .75;

    // Check for tall button, and write text vertically if necessary
    bool verticalText = false;
    if (textRect.Height > textRect.Width * 2)
    {
        verticalText = true;
    }

    // Convert the text alignment from
    // ContentAlignment to StringAlignment
    labelStrFmt.Alignment = ConvertToHorAlign(this.TextAlign);
    labelStrFmt.LineAlignment = ConvertToVertAlign(this.TextAlign);

    // If horizontal text is not horizontally centred,
    // or vertical text is not vertically centred,
    // shrink the rectangle so that the text doesn't stray outside the ellipse
    if ((!verticalText & (labelStrFmt.LineAlignment != StringAlignment.Center)) |
        (verticalText & (labelStrFmt.Alignment != StringAlignment.Center)))
    {
        textRect.Inflate(-(int)(textRect.Width/7.5), 
                         -(int)(textRect.Height/7.5));
    }

    textRect.Offset(buttonPressOffset, buttonPressOffset);
    // Apply the offset if we've been clicked

    // If button is not enabled, "grey out" the text.
    if (!this.Enabled)
    {
        //Write the white "embossing effect" text at an offset
        textRect.Offset(1, 1);
        labelBrush.Color = ControlPaint.LightLight(buttonColor);
        WriteString(verticalText, g, textRect);

        //Restore original text pos, and set text colour to grey.
        textRect.Offset(-1, -1);
        labelBrush.Color = Color.Gray;
    }

    //Write the text
    WriteString(verticalText, g, textRect);
}

按钮被按下时的错觉是通过下面的两个小方法实现的。当用户按下按钮时,buttonPressOffset 变量被设置为 1,并且虚拟光线角度被改变,使得按钮的左上角变暗,右下角变亮,从而产生按钮已凹入表面中的印象。当按钮释放时,值恢复正常。

protected void buttonDown()
{
    lightAngle = Angle.Down;
    buttonPressOffset = 1;
    this.Invalidate();
}

protected void buttonUp()
{
    lightAngle = Angle.Up;
    buttonPressOffset = 0;
    this.Invalidate();
}

最后,有几点要说明…

RoundButton 控件仅支持 FlatStyle.Standard。我为 FlatStyle.FlatFlatStyle.Popup 编写了一些代码,它们工作得还可以,但我对代码和结果都不完全满意,所以我把它删除了。

如果你查看源代码,你可能会注意到一个名为 Overrideable shape-specific methodsRegion,其中包含像这些这样不起眼的方法:

protected virtual void AddShape(GraphicsPath gpath, Rectangle rect)
{
    gpath.AddEllipse(rect);
}


protected virtual void DrawShape(Graphics g, Pen pen, Rectangle rect)
{
    g.DrawEllipse(pen, rect);
}

为什么不直接调用 AddEllipse,而是调用 AddShape?好吧,我还编写了其他一些类,例如 TriangleButtonDiamondButton,它们显然不使用 AddEllipse 或任何与椭圆相关的其他内容,因此我想覆盖其他形状代码中的方法。我没有在这里包含其他形状,部分原因是我认为有些代码已经变得有点混乱,需要比我现在时间所能做的更多的返工,并且因为坦率地说它们看起来不如圆形的好看!

要在另一个项目中使用的按钮,只需添加对 RoundButton.dll 的引用,RoundButton 图标就会出现在工具箱中。(你可能需要执行 工具 -> 选择工具箱项 来手动添加它。)

文章到此结束。我希望你觉得它很有趣,并且喜欢这些按钮!

© . All rights reserved.