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






4.89/5 (97投票s)
一个 C# 中的 Windows 圆形按钮控件,
引言
很久以前,我试图找一个漂亮的圆形按钮控件。我没找到,于是就按照传统的做法,决定自己写一个。我“几乎”完成了它,但由于各种原因,它被归入了“以后再来做”的类别。按照它特有的风格,“以后”终于到来,我武装了我闪亮的新版Microsoft Visual C# 2005 Express Edition,决定尝试完成它。
虽然我自己这么说,但我认为这些按钮看起来不错——你得自己判断!它们在“实体”中看起来更好,而不是像这篇文章中的 JPEG 图那样。
背景
在我寻找圆形按钮控件的过程中,我偶然看到了几篇文章(包括伟大的 Chris Maunder 本人的一篇!),在我看来,我那微不足道的头脑认为里面有太多复杂的数学知识。而且,我一直在学习 C# 中的图形,并对 PathGradientBrush
这样的酷炫事物进行了大量实验,从非常出色的 Bob Powell 网站汲取了很多灵感。可能,偶然地,我不记得具体是如何的,我偶然发现了将渐变填充的、逐渐缩小的圆层叠在一起,并通过 LinearGradientBrush
和 PathGradientBrush
来构建一个可观的 3D 按钮的想法。下面的图片说明了这一点。
将鼠标悬停在上面可查看描述。
工作原理
是的,本质上,将很多圆一个叠一个地放在一起就是它的工作方式。该控件继承自 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.Dark
和 ControlPaint.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
继承的 Font
和 ForeColor
属性。我使用了我的 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.Flat
和 FlatStyle.Popup
编写了一些代码,它们工作得还可以,但我对代码和结果都不完全满意,所以我把它删除了。
如果你查看源代码,你可能会注意到一个名为 Overrideable shape-specific methods
的 Region
,其中包含像这些这样不起眼的方法:
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
?好吧,我还编写了其他一些类,例如 TriangleButton
和 DiamondButton
,它们显然不使用 AddEllipse
或任何与椭圆相关的其他内容,因此我想覆盖其他形状代码中的方法。我没有在这里包含其他形状,部分原因是我认为有些代码已经变得有点混乱,需要比我现在时间所能做的更多的返工,并且因为坦率地说它们看起来不如圆形的好看!
要在另一个项目中使用的按钮,只需添加对 RoundButton.dll 的引用,RoundButton
图标就会出现在工具箱中。(你可能需要执行 工具 -> 选择工具箱项 来手动添加它。)
文章到此结束。我希望你觉得它很有趣,并且喜欢这些按钮!