Windows 11下所有控件已经默认采用圆角,其效果更好、相对有着更好的优化,只是这是默认的行为,无法进一步自定义。
圆角按钮实现【重写OnPaint实现圆角绘制】
控件自定义绘制的关键在于:重写OnPaint
方法,其参数提供了用于GDI+绘图的Graphics
对象,由此实现绘制需要的图形效果。
为了更好的显示绘制的图形,通常必须设置控件背景透明(图形外的控件区域透明,便于正确显示绘制的图形),虽然Winform的背景透明有着一定缺陷,但总体来说这是必须的。
此外,Paint
事件方法中,也可以进行控件的绘制(重绘),与继承重写OnPaint
没有本质区别。
代码主要关键点或思路、优化
- 半径Radius、Color、TextAlign属性的赋值,都调用
Invalidate()
方法使控件画面无效并重绘控件。 - 添加文本位置的属性TextAlign,并在属性赋值时调用
Invalidate()
重绘控件,实现修改文本位置的布局。 - 有一个bug问题,就是在点击按钮鼠标抬起方法
OnMouseUp
中,实现了修改鼠标状态,对应的背景颜色值也修改了,控件重绘时也修改了颜色(debug),但绝大多数情况下,鼠标抬起背景颜色未变化。原因在重写的OnMouseUp(MouseEventArgs e)
中先调用的控件基类base.OnMouseUp(e);
,后修改的状态颜色,将base.OnMouseUp(e);
改为最后调用即可。 - 修改和优化圆角部分圆弧的绘制,原实现圆弧半径处理不合理。
- 【尽可能高质量绘制】图形部分的几个模式必须指定,怎么明显看出显示的文本、边框等不清晰、锯齿验证等问题。
- 其他一些小修改和调整,比如抗锯齿、高质量绘图、使用控件字体、文本颜色默认白色、设置字体方向等
- Radius 属性修改边角半径大小(即圆角的大小、圆弧的大小)
- NormalColor、HoverColor、PressedColor 属性设置按钮正常状态、鼠标悬停、鼠标按下时的背景颜色,通常设置为一致即可。
指定Size的Width、Height的大小相同,Radius为正方向边长的一半,可以实现圆形按钮。
StringFormat
对象,可以提供对字符串文本的颜色、布局、方向等各种格式的设置,用于渲染文本效果。
Control.DesignMode
属性可以判断当前代码的执行环境是否是设计器模式,在某些条件下可以通过此判断,决定是否在设计器下执行某段代码(如果不涉及样式效果,就可以不需要在设计器下执行)
使用圆角按钮
编译后,直接从工具箱中拖动RoundButtons
圆角按钮控件到窗体即可。
代码如下,关键部分都有相关注释,可以直接过一遍代码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace CMControls.RoundButtons
{
public enum ControlState { Hover, Normal, Pressed }
public class RoundButton : Button
{
private int radius;//半径
//private Color _borderColor = Color.FromArgb(51, 161, 224);//边框颜色
private Color _hoverColor = Color.FromArgb(220, 80, 80);//基颜色
private Color _normalColor = Color.FromArgb(51, 161, 224);//基颜色
private Color _pressedColor = Color.FromArgb(251, 161, 0);//基颜色
private ContentAlignment _textAlign = ContentAlignment.MiddleCenter;
public override ContentAlignment TextAlign
{
set
{
_textAlign = value;
this.Invalidate();
}
get
{
return _textAlign;
}
}
/// <summary>
/// 圆角按钮的半径属性
/// </summary>
[CategoryAttribute("Layout"), BrowsableAttribute(true), ReadOnlyAttribute(false)]
public int Radius
{
set
{
radius = value;
// 使控件的整个画面无效并重绘控件
this.Invalidate();
}
get
{
return radius;
}
}
[CategoryAttribute("Appearance"), DefaultValue(typeof(Color), "51, 161, 224")]
public Color NormalColor
{
get
{
return this._normalColor;
}
set
{
this._normalColor = value;
this.Invalidate();
}
}
[CategoryAttribute("Appearance"), DefaultValue(typeof(Color), "220, 80, 80")]
public Color HoverColor
{
get
{
return this._hoverColor;
}
set
{
this._hoverColor = value;
this.Invalidate();
}
}
[CategoryAttribute("Appearance"), DefaultValue(typeof(Color), "251, 161, 0")]
public Color PressedColor
{
get
{
return this._pressedColor;
}
set
{
this._pressedColor = value;
this.Invalidate();
}
}
public ControlState ControlState { get; set; }
protected override void OnMouseEnter(EventArgs e)//鼠标进入时
{
ControlState = ControlState.Hover;//Hover
base.OnMouseEnter(e);
}
protected override void OnMouseLeave(EventArgs e)//鼠标离开
{
ControlState = ControlState.Normal;//正常
base.OnMouseLeave(e);
}
protected override void OnMouseDown(MouseEventArgs e)//鼠标按下
{
if (e.Button == MouseButtons.Left && e.Clicks == 1)//鼠标左键且点击次数为1
{
ControlState = ControlState.Pressed;//按下的状态
}
base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseEventArgs e)//鼠标弹起
{
if (e.Button == MouseButtons.Left && e.Clicks == 1)
{
if (ClientRectangle.Contains(e.Location))//控件区域包含鼠标的位置
{
ControlState = ControlState.Hover;
}
else
{
ControlState = ControlState.Normal;
}
}
base.OnMouseUp(e);
}
public RoundButton()
{
ForeColor = Color.White;
Radius = 20;
this.FlatStyle = FlatStyle.Flat;
this.FlatAppearance.BorderSize = 0;
this.ControlState = ControlState.Normal;
this.SetStyle(
ControlStyles.UserPaint | //控件自行绘制,而不使用操作系统的绘制
ControlStyles.AllPaintingInWmPaint | //忽略背景擦除的Windows消息,减少闪烁,只有UserPaint设为true时才能使用。
ControlStyles.OptimizedDoubleBuffer |//在缓冲区上绘制,不直接绘制到屏幕上,减少闪烁。
ControlStyles.ResizeRedraw | //控件大小发生变化时,重绘。
ControlStyles.SupportsTransparentBackColor, //支持透明背景颜色
true);
}
//重写OnPaint
protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
{
base.OnPaint(e);
// base.OnPaintBackground(e);
// 尽可能高质量绘制
e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
e.Graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.CompositingQuality = CompositingQuality.HighQuality;
e.Graphics.InterpolationMode = InterpolationMode.HighQualityBilinear;
Rectangle rect = new Rectangle(0, 0, this.Width, this.Height);
var path = GetRoundedRectPath(rect, radius);
this.Region = new Region(path);
Color baseColor;
//Color borderColor;
//Color innerBorderColor = this._baseColor;//Color.FromArgb(200, 255, 255, 255); ;
switch (ControlState)
{
case ControlState.Hover:
baseColor = this._hoverColor;
break;
case ControlState.Pressed:
baseColor = this._pressedColor;
break;
case ControlState.Normal:
baseColor = this._normalColor;
break;
default:
baseColor = this._normalColor;
break;
}
using (SolidBrush b = new SolidBrush(baseColor))
{
e.Graphics.FillPath(b, path); // 填充路径,而不是DrawPath
using (Brush brush = new SolidBrush(this.ForeColor))
{
// 文本布局对象
using (StringFormat gs = new StringFormat())
{
// 文字布局
switch (_textAlign)
{
case ContentAlignment.TopLeft:
gs.Alignment = StringAlignment.Near;
gs.LineAlignment = StringAlignment.Near;
break;
case ContentAlignment.TopCenter:
gs.Alignment = StringAlignment.Center;
gs.LineAlignment = StringAlignment.Near;
break;
case ContentAlignment.TopRight:
gs.Alignment = StringAlignment.Far;
gs.LineAlignment = StringAlignment.Near;
break;
case ContentAlignment.MiddleLeft:
gs.Alignment = StringAlignment.Near;
gs.LineAlignment = StringAlignment.Center;
break;
case ContentAlignment.MiddleCenter:
gs.Alignment = StringAlignment.Center; //居中
gs.LineAlignment = StringAlignment.Center;//垂直居中
break;
case ContentAlignment.MiddleRight:
gs.Alignment = StringAlignment.Far;
gs.LineAlignment = StringAlignment.Center;
break;
case ContentAlignment.BottomLeft:
gs.Alignment = StringAlignment.Near;
gs.LineAlignment = StringAlignment.Far;
break;
case ContentAlignment.BottomCenter:
gs.Alignment = StringAlignment.Center;
gs.LineAlignment = StringAlignment.Far;
break;
case ContentAlignment.BottomRight:
gs.Alignment = StringAlignment.Far;
gs.LineAlignment = StringAlignment.Far;
break;
default:
gs.Alignment = StringAlignment.Center; //居中
gs.LineAlignment = StringAlignment.Center;//垂直居中
break;
}
// if (this.RightToLeft== RightToLeft.Yes)
// {
// gs.FormatFlags = StringFormatFlags.DirectionRightToLeft;
// }
e.Graphics.DrawString(this.Text, this.Font, brush, rect, gs);
}
}
}
}
/// <summary>
/// 根据矩形区域rect,计算呈现radius圆角的Graphics路径
/// </summary>
/// <param name="rect"></param>
/// <param name="radius"></param>
/// <returns></returns>
private GraphicsPath GetRoundedRectPath(Rectangle rect, int radius)
{
#region 正确绘制圆角矩形区域
int R = radius*2;
Rectangle arcRect = new Rectangle(rect.Location, new Size(R, R));
GraphicsPath path = new GraphicsPath();
// 左上圆弧 左手坐标系,顺时针为正 从180开始,转90度
path.AddArc(arcRect, 180, 90);
// 右上圆弧
arcRect.X = rect.Right - R;
path.AddArc(arcRect, 270, 90);
// 右下圆弧
arcRect.Y = rect.Bottom - R;
path.AddArc(arcRect, 0, 90);
// 左下圆弧
arcRect.X = rect.Left;
path.AddArc(arcRect, 90, 90);
path.CloseFigure();
return path;
#endregion
}
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
}
}
}
可以改进和实现的
- 添加Border,实现Border颜色和宽度的指定(目前的一个思路时利用路径在外层填充一个圆角矩形,在内层再填充一个圆角矩形,形成有Border的效果;另一个思路时,画路径时,绘制内层路径和圆环路径,Border部分是一个圆角的圆环路径,而后分别填充颜色;还有就是绘制路径线条,线条作为Border。)
- 通过百分比实现圆角
- 完全扩展Button,通过标志位启动圆角和修改圆角,做到圆角和普通Button共存。
- 修改使用
RectangleF
对象,使用浮点数绘制矩形和路径 - 圆角半径radius指定为0的处理
Rectangle.Round(RectangleF)
将RectangleF对象转换为Rectangle,通过舍入最近的数。
利用填充内外两层圆角矩形路径形成Border【有着致命缺陷(随后介绍了正确处理的方案)】
控件的Region
区域一定指定,并且要包含全部的Graphics绘制的内容,否则显示不全,包含在Region内才能全部显示出来。
Region
区域指定的是控件的区域,表示的是控件的范围
如下,通过Border大小 _borderWidth 计算不同的路径,指定Region。
矩形区域长宽不同,无法按照等比的方式计算长宽方向上固定边框宽度的比例;因此,内部的内层圆角半径也无法准确计算,理论采用比例较小的比较合适
// 外层圆角矩形
Rectangle outRect = new Rectangle(0, 0, this.Width, this.Height);
var outPath = outRect.GetRoundedRectPath(_radius);
// 计算内存圆角矩形,不严谨
Rectangle rect = new Rectangle(_borderWidth, _borderWidth, this.Width - _borderWidth*2, this.Height - _borderWidth*2);
var path = rect.GetRoundedRectPath(_radius);
//this.Region = new Region(path);
// 必须正确指定外层路径outPath的全部区域,否则无法显示完全填充的全部
this.Region = new Region(outPath);
然后分别填充两个路径:
using (SolidBrush borderB = new SolidBrush(_borderColor))
{
e.Graphics.FillPath(borderB, outPath);
}
using (SolidBrush b = new SolidBrush(baseColor))
{
e.Graphics.FillPath(b, path); // 填充路径,而不是DrawPath
}
通过缩放实现正确的内外两层圆角矩形路径
通过缩放实现正确Border的原理主要如下图所示,长宽缩小BorderSize大小,圆角半径同样缩小BorderSize,两个内外层圆角矩形的圆角在共同半径下绘制圆角弧线。
Rectangle.Inflate()
方法用于返回Rectangle结构的放大副本,第二三个参数表示x、y方向放大或缩小的量。
var innerRect = Rectangle.Inflate(outRect, -borderSize, -borderSize);
则对应得到内层圆角路径为:
GraphicsPath innerPath = innerRect.GetRoundedRectPath(borderRadius - borderSize)
从这里可以看出,需要保证borderSize
小于borderRadius
。
CDI+路径的填充模式
GraphicsPath
的填充模式FillMode默认是FillMode.Alternate
,所以替代填充可以实现内外两层的填充实现Border效果。
填充模式另一个选项为FillMode.Winding
,可实现环绕效果,它们都是应用在路径发生重叠(overlap)时,不同的填充效果。可具体测试不同效果
GraphicsPath gp = new GraphicsPath(FillMode.Winding);
直接绘制路径作为边框【推荐】**
通过DrawPath
直接绘制边框,注意宽度的处理。
// 绘制边框
using (Pen pen = new Pen(_borderColor,_borderWidth*2))
{
e.Graphics.DrawPath(pen, path);
// 绘制路径上,会有一半位于路径外层,即Region外面,无法显示出来。因此设置为双borderWidth
}
记得同时修改下文字绘制的区域范围问题,边框宽度占据了区域的一部分。否则,在空间很小时,文字会位于边框上。
查看效果如下:
最好的处理不要使用
_borderWidth*2
,而是使用原本大小,绘制的Path缩小在半个_borderWith范围内。比如:new Rectangle(rect.X + _borderWidth / 2, rect.Y + _borderWidth / 2, rect.Width - _borderWidth, rect.Height - _borderWidth)
在Paint事件中重绘控件为圆角
除了继承控件(如上面Button)通过重写OnPaint
方法,实现圆角的绘制,还可以直接在原生控件的Paint
事件方法中,实现重绘控件为圆角。
后面文章中也介绍了,可以发现在
Paint
事件方法中重绘比完全用户绘制控件,圆角和各个部分有着更少的锯齿,几乎没有,看起来相对更好一些,也因此较为推荐在Paint
事件中实现圆角。【可以直接对比两者效果】
比如,对于Button设置如下样式,并添加Paint事件方法
button1.Paint += Button1_Paint;
button1.FlatStyle = FlatStyle.Flat;
button1.FlatAppearance.BorderSize = 0;
button1.FlatAppearance.MouseDownBackColor = Color.Transparent;
button1.FlatAppearance.MouseOverBackColor = Color.Transparent;
button1.FlatAppearance.CheckedBackColor = Color.Transparent;