Winform控件优化之圆角按钮【各种实现中的推荐做法】

news/2024/10/3 23:27:28/文章来源:https://www.cnblogs.com/soliang/p/18442270
简介: Windows 11下所有控件已经默认采用圆角,其效果更好、相对有着更好的优化...尝试介绍很常见的圆角效果,通过重写控件的OnPaint方法实现绘制,并在后面进一步探索对应的优化和可能的问题
Windows 11下所有控件已经默认采用圆角,其效果更好、相对有着更好的优化,只是这是默认的行为,无法进一步自定义。

圆角按钮实现【重写OnPaint实现圆角绘制】

控件自定义绘制的关键在于:重写OnPaint方法,其参数提供了用于GDI+绘图的Graphics对象,由此实现绘制需要的图形效果。

为了更好的显示绘制的图形,通常必须设置控件背景透明(图形外的控件区域透明,便于正确显示绘制的图形),虽然Winform的背景透明有着一定缺陷,但总体来说这是必须的。

此外,Paint事件方法中,也可以进行控件的绘制(重绘),与继承重写OnPaint没有本质区别。

代码主要关键点或思路、优化

  1. 半径Radius、Color、TextAlign属性的赋值,都调用 Invalidate() 方法使控件画面无效并重绘控件。
  2. 添加文本位置的属性TextAlign,并在属性赋值时调用Invalidate()重绘控件,实现修改文本位置的布局。
  3. 有一个bug问题,就是在点击按钮鼠标抬起方法OnMouseUp中,实现了修改鼠标状态,对应的背景颜色值也修改了,控件重绘时也修改了颜色(debug),但绝大多数情况下,鼠标抬起背景颜色未变化。原因在重写的OnMouseUp(MouseEventArgs e)中先调用的控件基类base.OnMouseUp(e);,后修改的状态颜色,将base.OnMouseUp(e);改为最后调用即可。
  4. 修改和优化圆角部分圆弧的绘制,原实现圆弧半径处理不合理。
  5. 【尽可能高质量绘制】图形部分的几个模式必须指定,怎么明显看出显示的文本、边框等不清晰、锯齿验证等问题。
  6. 其他一些小修改和调整,比如抗锯齿、高质量绘图、使用控件字体、文本颜色默认白色、设置字体方向等
  7. Radius 属性修改边角半径大小(即圆角的大小、圆弧的大小)
  8. NormalColor、HoverColor、PressedColor 属性设置按钮正常状态、鼠标悬停、鼠标按下时的背景颜色,通常设置为一致即可。
  9. 指定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;//Hoverbase.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);}//重写OnPaintprotected 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); // 填充路径,而不是DrawPathusing (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);}}
}
参考 C# Winform实现圆角无锯齿按钮

可以改进和实现的

  1. 添加Border,实现Border颜色和宽度的指定(目前的一个思路时利用路径在外层填充一个圆角矩形,在内层再填充一个圆角矩形,形成有Border的效果;另一个思路时,画路径时,绘制内层路径和圆环路径,Border部分是一个圆角的圆环路径,而后分别填充颜色;还有就是绘制路径线条,线条作为Border。)
  2. 通过百分比实现圆角
  3. 完全扩展Button,通过标志位启动圆角和修改圆角,做到圆角和普通Button共存。
  4. 修改使用RectangleF对象,使用浮点数绘制矩形和路径
  5. 圆角半径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;

圆角按钮实现的进一步优化【最终实现,兼容默认按钮】

主要功能【圆角方面】

结合前面两部分介绍和代码,最终优化实现ButtonPro按钮(继承自Button),既提供Button原生功能,又提供扩展功能,除了圆角以外,还实现了圆形、圆角矩形的脚尖效果、边框大小和颜色、背景渐变颜色、圆角的绘制模式和创建Region模式、常用按钮图标快速使用(此项暂未实现)等其他功能。

圆角按钮控件相关属性和实现:

  • RoundRadius:圆角半径,>=0时启用圆角按钮,等于0为直角(但可使用背景色等所有Round圆角相关属性),<0时使用默认Button样式。
  • RegionNewModel:创建新Region的模式,使用'绘制范围'创建新的Region,实现控件区域贴合绘制范围,实现图形外的部分"正确的透明",但相对会有些锯齿。
  • ShowCusp:是否显示尖角,默认不显示,当启用Radius圆角(RoundRadius>=0)时才有效。
  • CuspAlign:(三角)尖角的显示位置,当启用圆角按钮(RoundRadius>=0),且显示尖角时有效。
  • RoundBorderWidth:启用Radius圆角(RoundRadius>=0)时边框宽度,默认0。
  • RoundBorderColor:启用Radius圆角(RoundRadius>=0)时边框颜色,默认黑色。
  • EnableBGGradient:启用渐变背景色(需要RoundRadius>=0),启用后RoundNormalColor、RoundHoverColor、RoundPressedColor颜色无效。
  • GradientModel:线性渐变的模式,默认垂直渐变。
  • BGColorBegin:渐变开始色。
  • BGColorEnd:渐变结束色。
  • RoundNormalColor:启用Radius圆角(RoundRadius>=0)时按钮标准颜色。
  • RoundHoverColor:启用Radius圆角(RoundRadius>=0)鼠标位于按钮上时的按钮颜色。
  • RoundPressedColor:启用Radius圆角(RoundRadius>=0)鼠标按下时的按钮颜色。
  • 圆形按钮,长宽一样,圆角半径为长宽的一半,可实现圆形按钮。
  • 扩展控件属性分类修改为“高级”,使用CategoryAttribute特性。
注意:

borderPen绘制线条的对齐方式:borderPen.Alignment = PenAlignment.Inset;。但是指定Inset绘制边框也有小问题,如果是重新创建Region,则绘制边框后内部变为直角矩形(先填充再绘制边框线条);如果使用就有Region(不新建),也会变成内部直角,并且如果不指定Inset则会外部绘制的Border线条变成直接(由原本直角的Region承载圆角之外的部分)。总之都有些问题,可自行测试。

在绘制边框时,不推荐使用PenAlignment.Inset,通过计算减少Rectangle的范围为半个Border的路径范围(宽高-RoundBorderWidth),绘制时在路径内外正好有一个Border的大小来实现。而且这样不会发生上面介绍的内或外直角而非圆角的情况

OnPaint方法中不要使用e.ClipRectangle

应该使用控件的宽高(Width、Height)计算Rectangle矩形,或者使用ClientRectangle属性。

相互影响的问题,复制的或拖拽的ButtonPro控件,会被其他控件影响,即调整某个控件,会导致ButtonPro的一个或多个也会牵连变化(很杂乱的关联变化),如何解决?应该是独立的才对!且在设计器中没法通过撤销操作还原效果

与SetStyle设置无关,后面测试和重新指定Region有关,在后续测试发现最终导致此类问题的原因,在于使用了OnPaint参数e.ClipRectangle作为控件绘制范围绘制产生的。【比如rect = e.Graphics.DrawRoundRectAndCusp(e.ClipRectangle, roundRadius, baseColor, showCusp, cuspAlign)

直接使用控件的WidthHeight定义绘制绘制范围,替换到e.ClipRectangle

OnPaint方法中不要使用e.ClipRectangle作为控件绘制范围

以下问题均是由于OnPaint方法中使用e.ClipRectangle来绘制绘制范围导致的(它是个自动被影响的值,应该使用Width、Height创建)

不重新赋值Region,拖动或调整按钮,会重写显示相互影响:

如果赋值新的Region,当调整或移动控件时,就有可能影响显示的布局或大小,且无法通过撤销还原

重新创建Region的锯齿问题和优势【写错代码实现圆形导致的错误探索,但也很有意义】

重新赋值Region一个最大的确实是会产生一些锯齿,即使使用抗锯齿和最好质量绘制,要想绘制圆形效果,必须重新赋值Region,否则控件只会是圆角。因此,提供了RoundCircleModel属性,用于是否启用可绘制圆形按钮的模式,会有些锯齿,默认不启用,如果需要时启用即可。

需要记住的几点:

  • Region定义的是控件的区域,通过GDI+绘制可以实现一个自定义的区域,从而解除默认的宽高矩形区域控件的限制。
  • 重新定义和赋值Region的缺点是会产生一定锯齿,这个锯齿是Region产生的,而不是GDI+绘制填充产生的。
  • 无法消除创建的Region锯齿,至少没提供相关API,因此实际重绘控件时,通常不要创建新的Region
  • 由于设置中使用了Winform透明父控件的样式,因此要注意其正确的父控件设置。
  • 直接使用绘制后的正确的绘制范围创建新的Region区域,则没有透明父控件的问题,可以实现“正确的透明”,具体可参加下图所示【新建Region绘制图形后圆角边缘部分出现的1像素的控件颜色可以通过调整创建Region时和绘制时的范围消除(多余的1像素白边问题无法简单的通过调整Region和绘制范围解决,具体可自行测试)】。
【若是消除新建Region的锯齿问题,将会非常完美】

若是能实现直接绘制无锯齿的圆角Region区域,则,直接在Paint事件中实现控件的圆角Region即可,无需在额外重新绘制背景和文字。【更简单、更完美的方案】

目前所知,无法对Region进行抗锯齿,即使使用使用GDI+的API CreateRoundRectRgn方法。相关介绍参见 c# How to make smooth arc region using graphics path、 Winforms: Smooth the rounded edges for panel

控件上重绘可以使用Graphics对象在背景透明的Region区域控件上实现,其背后至少一个父控件(或顶层的Form窗体)。

但是,对于一个Form,要想实现圆角或多边形窗体,则必须重新生成Region,但创建Region在不规则形状时边缘锯齿无法解决(如果有大牛,应该可以应用消除锯齿的算法),后面会介绍一种取巧或者Win32窗体推荐的一种方式,即,使用Layered Windows。

代码具体实现

将绘制方法精简为扩展方法后,扩展控件的全部源代码如下:

using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;namespace CMControls
{public class ButtonPro : Button{private int roundRadius;//半径 private bool showCusp = false;//显示尖角private RectangleAlign cuspAlign = RectangleAlign.RightTop;//三角尖角位置private Color roundBorderColor = Color.Black;//边框颜色private int roundBorderWidth = 0;//边框宽度private Color roundHoverColor = Color.FromArgb(220, 80, 80);//鼠标位于控件时颜色private Color roundNormalColor = Color.FromArgb(51, 161, 224);//基颜色private Color roundPressedColor = Color.FromArgb(251, 161, 0);//鼠标按下控件时基颜色// 鼠标相对控件的状态位置,对应上面不同颜色private MouseControlState mouseControlState = MouseControlState.Normal;private bool regionNewModel = false; // 创建新Region的模式,使用"绘制范围"创建新的Region,实现控件区域贴合绘制范围,实现图形外的部分"正确的透明",但相对会有些锯齿private Color beginBGColor; //= Color.FromArgb(251, 161, 0);//渐变开始色private Color endBGColor; //= Color.FromArgb(251, 161, 0);//渐变结束色private bool enableBGGradient = false; //使用渐变色private LinearGradientMode gradientModel = LinearGradientMode.Vertical; //线性渐变的模式private Region originRegion;/// <summary>/// 圆形按钮的半径属性/// </summary>[CategoryAttribute("高级"), DefaultValue(20), Description("圆角半径,>=0时启用圆角按钮,等于0为直角(但可使用背景色等所有Round圆角相关属性),<0时使用默认Button样式")]public int RoundRadius{set{roundRadius = value;// 使控件的整个画面无效并重绘控件this.Invalidate();}get{return roundRadius;}}/// <summary>/// 圆角下创建新Region模式/// </summary>[CategoryAttribute("高级"), DefaultValue(false), Description("创建新Region的模式,使用'绘制范围'创建新的Region,实现控件区域贴合绘制范围,实现'正确的透明',但相对会有些的锯齿")]public bool RegionNewModel{set{regionNewModel = value;this.Invalidate();}get{return regionNewModel;}}/// <summary>/// 三角尖角位置,当启用圆角/// </summary>[CategoryAttribute("高级"), Description("(三角)尖角的显示位置,当启用圆角按钮(RoundRadius>=0),且显示尖角时有效"), DefaultValue(RectangleAlign.RightTop)]public RectangleAlign CuspAlign{set{cuspAlign = value;this.Invalidate();}get{return cuspAlign;}}[CategoryAttribute("高级"), Description("是否显示尖角,默认不显示,当启用Radius圆角(RoundRadius>=0)时才有效"), DefaultValue(false)]public bool ShowCusp{set{showCusp = value;this.Invalidate();}get{return showCusp;}}[CategoryAttribute("高级"), DefaultValue(0), Description("启用Radius圆角(RoundRadius>=0)时边框宽度,默认0")]public int RoundBorderWidth{set{roundBorderWidth = value;this.Invalidate();}get{return roundBorderWidth;}}[CategoryAttribute("高级"), DefaultValue(typeof(Color), "0, 0, 0"), Description("启用Radius圆角(RoundRadius>=0)时边框颜色,默认黑色")]public Color RoundBorderColor{get{return this.roundBorderColor;}set{this.roundBorderColor = value;this.Invalidate();}}/// <summary>/// 是否启用背景渐变色,启用后RoundNormalColor、RoundHoverColor、RoundPressedColor颜色无效/// </summary>[CategoryAttribute("高级"), DefaultValue(false), Description("启用渐变背景色(需要RoundRadius>=0),启用后RoundNormalColor、RoundHoverColor、RoundPressedColor颜色无效")]public bool EnableBGGradient{get{return this.enableBGGradient;}set{this.enableBGGradient = value;this.Invalidate();}}/// <summary>/// 线性渐变的模式,默认垂直渐变/// </summary>[CategoryAttribute("高级"), DefaultValue(LinearGradientMode.Vertical), Description("线性渐变的模式,默认垂直渐变")]public LinearGradientMode GradientModel{get{return this.gradientModel;}set{this.gradientModel = value;this.Invalidate();}}/// <summary>/// 背景渐变色/// </summary>[CategoryAttribute("高级"), DefaultValue(typeof(Color), "0, 122, 204"), Description("渐变开始色")]public Color BGColorBegin{get{return this.beginBGColor;}set{this.beginBGColor = value;this.Invalidate();}}/// <summary>/// 背景渐变色/// </summary>[CategoryAttribute("高级"), DefaultValue(typeof(Color), "8, 39, 57"), Description("渐变结束色")]public Color BGColorEnd{get{return this.endBGColor;}set{this.endBGColor = value;this.Invalidate();}}[CategoryAttribute("高级"), DefaultValue(typeof(Color), "51, 161, 224"), Description("启用Radius圆角(RoundRadius>=0)时按钮标准颜色")]public Color RoundNormalColor{get{return this.roundNormalColor;}set{this.roundNormalColor = value;this.Invalidate();}}[CategoryAttribute("高级"), DefaultValue(typeof(Color), "220, 80, 80"), Description("启用Radius圆角(RoundRadius>=0)鼠标位于按钮上时的按钮颜色")]public Color RoundHoverColor{get{return this.roundHoverColor;}set{this.roundHoverColor = value;this.Invalidate();}}[CategoryAttribute("高级"), DefaultValue(typeof(Color), "251, 161, 0"), Description("启用Radius圆角(RoundRadius>=0)鼠标按下时的按钮颜色")]public Color RoundPressedColor{get{return this.roundPressedColor;}set{this.roundPressedColor = value;this.Invalidate();}}protected override void OnMouseEnter(EventArgs e)//鼠标进入时{mouseControlState = MouseControlState.Hover;//Hoverbase.OnMouseEnter(e);}protected override void OnMouseLeave(EventArgs e)//鼠标离开{mouseControlState = MouseControlState.Normal;//正常base.OnMouseLeave(e);}protected override void OnMouseDown(MouseEventArgs e)//鼠标按下{if (e.Button == MouseButtons.Left && e.Clicks == 1)//鼠标左键且点击次数为1{mouseControlState = MouseControlState.Pressed;//按下的状态}base.OnMouseDown(e);}protected override void OnMouseUp(MouseEventArgs e)//鼠标弹起{if (e.Button == MouseButtons.Left && e.Clicks == 1){if (ClientRectangle.Contains(e.Location))//控件区域包含鼠标的位置{mouseControlState = MouseControlState.Hover;}else{mouseControlState = MouseControlState.Normal;}}base.OnMouseUp(e);}public ButtonPro(){ForeColor = Color.White;this.FlatStyle = FlatStyle.Flat;this.FlatAppearance.BorderSize = 0;FlatAppearance.MouseDownBackColor = Color.Transparent;FlatAppearance.MouseOverBackColor = Color.Transparent;FlatAppearance.CheckedBackColor = Color.Transparent;RoundRadius = 20; // 似乎当值为默认20时重新生成设计器或者重新打开项目后,此属性就会变为0,必须在构造函数中指定20来解决this.mouseControlState = MouseControlState.Normal;// 原始RegionoriginRegion = Region;}public override void NotifyDefault(bool value){base.NotifyDefault(false); // 去除窗体失去焦点时最新激活的按钮边框外观样式}//重写OnPaintprotected override void OnPaint(PaintEventArgs e){base.OnPaint(e);//base.OnPaintBackground(e);// 不能使用 e.ClipRectangle.GetRoundedRectPath(_radius) 计算控件全部的Region区域,e.ClipRectangle 似乎是变化的,必须使用固定的Width和Height,包括下面的绘制也不能使用e.ClipRectangle// 在Paint事件中也不推荐使用e.ClipRectangle时没问题的Rectangle controlRect = new Rectangle(0, 0, this.Width, this.Height);// roundRadius 修改回来是要还原if (roundRadius >= 0 && regionNewModel) // 圆角下创建新Region模式,使用自定义Region{var controlPath = controlRect.GetRoundedRectPath(roundRadius);// 要在绘制之前指定Region,否则无效this.Region = new Region(controlPath);}else // 修改对应调整{//this.Region = new Region(controlRect);//也属于重新修改this.Region = originRegion;}if (roundRadius >= 0){Rectangle rect;if (enableBGGradient){rect = e.Graphics.DrawRoundRectAndCusp(controlRect, roundRadius, beginBGColor, endBGColor, true, CuspAlign, gradientModel, roundBorderWidth > 0 ? new Pen(roundBorderColor, roundBorderWidth) : null);}else{Color baseColor;switch (mouseControlState){case MouseControlState.Hover:baseColor = this.roundHoverColor;break;case MouseControlState.Pressed:baseColor = this.roundPressedColor;break;case MouseControlState.Normal:baseColor = this.roundNormalColor;break;default:baseColor = this.roundNormalColor;break;}rect = e.Graphics.DrawRoundRectAndCusp(controlRect, roundRadius, baseColor, showCusp, cuspAlign, roundBorderWidth > 0 ? new Pen(roundBorderColor, roundBorderWidth) : null);}// 使用合适的区域e.Graphics.DrawText(rect, Text, ForeColor, Font, TextAlign);}}}
}

测试扩展按钮控件ButtonPro

通过拖拽ButtonPro按钮控件,调整各个参数,查看不同样式的按钮效果。

文本垂直居中偏上的问题及文字大小不正确【推荐使用TextRenderer.DrawText绘制文本】

所有的一切都非常好,但是,目前还有一个小问题,就是绘制垂直居中的文本时,可以明显看到偏上方。是的由此产生“瑕疵”。

目前没有找到很好的解决办法,更换字体、字体大小为偶数会有一定效果,但并不能完全解决。

使用StringFormat.GenericTypographic

后面经过花木兰控件库的大佬提醒,使用StringFormat.GenericTypographic作为文本绘制的格式对象,可以看到偏上的问题有了明显改善。

        using (StringFormat strF = StringFormat.GenericTypographic){// 文字布局switch (_textAlign){//...}g.DrawString(text, font, brush, rect, strF);}

虽然如此,但是还是有一点点不完全垂直。而且对比同样字体情况下,DrawString绘制出来的文本明显和原生Button时显示的文字有很大差别(大小、清晰度)

仅仅重写OnPaintBackground【无效果】

后面由于文字绘制的问题,想着直接重写OnPaintBackground,文字交由Winform自己绘制,应该可以达到很好的效果。

但是,但是重写OnPaintBackground后背景没有任何效果,仅仅是设置的透明背景,无法实现圆角等各种绘制。

目前暂时不知道该如何正确的处理OnPaintBackground方法。

使用TextRenderer.DrawText绘制文本【不推荐Graphics.DrawString】

后来几乎要放弃了,因为最终绘制的文字确实很不理想,和原生Button对比起来差好多。。。

然后想着测试下TextRenderer.DrawText()绘制文本的效果如何,最终发现文字绘制效果非常好(大小正确、清晰),重点是文字位置的水平和垂直居中没有任何问题,基本和原生Button的文字效果一致。

// ...
TextRenderer.DrawText(g, text, font, rect, color, formatFlags);

注:本文转自Winform控件优化之圆角按钮【各种实现中的推荐做法】

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/806281.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Windows 中的硬链接、目录联接(软链接)、符号链接、快捷方式

在Linux文件系统中经常提及硬链接(Hard Link)和符号链接(Symbolic Link),Windows中也可以创建链接,但由于丰富的图形界面操作,很少提及链接。Windows 的 NTFS 文件系统支持三种链接:硬链接(Hard Link)、符号链接(Symbolic Link)和目录链接(junction point),此外还有一个大…

ehviewer绿色版1.9.5.2最新2024ios苹果安卓

随着科技的不断发展,手机已经成为我们生活中不可或缺的一部分。在这个数字化时代,人们对于娱乐方式的需求也在逐渐改变。其中,漫画作为一种受欢迎的阅读形式,已经从传统的纸质书籍转变为数字版。而今天,我要为大家介绍的这款软件——ehviewer绿色版1.9.5.2,正是为了满足广…

建筑中的文化表达与地方特色:演绎地域之魂

在浩瀚的城市风貌中,每一座建筑都是文化的载体,无声地讲述着地域的故事与精神。建筑不仅需要满足功能需求,更应成为文化传承与创新的舞台。本文旨在深度剖析建筑设计如何在尊重与弘扬地方文化的基础上,巧妙融合现代元素,创造出既有时代感又不失根源性的建筑作品。 1. 深入…

确保 PbootCMS 网站能够正常运行,并且成功安装和授权模板

准备 PHP 环境确认 PHP 版本使用命令行或 SSH 登录服务器,运行以下命令检查 PHP 版本:shphp -v确认版本为 5.3+。上传 PbootCMS 文件使用 FTP 客户端使用 FTP 客户端(如 FileZilla、WinSCP 等)连接到服务器。 将 PbootCMS 的所有文件上传到服务器的根目录(通常是 public_h…

解决 PbootCMS 网站程序提示“执行 SQL 发生错误

步骤一:清理缓存文件打开 FTP 客户端使用常用的 FTP 客户端(如 FileZilla、WinSCP 等)连接到服务器。找到 runtime 文件夹在 FTP 客户端中找到 PbootCMS 的安装目录,通常是在 public_html 或 www 目录下。删除 runtime 文件夹中的内容进入 runtime 文件夹,删除其中的所有文…

有效地解决 PbootCMS 网站程序提示“执行 SQL 发生错误!错误:DISK I/O ERROR”的问题,并确保系统的稳定运行

打开 FTP 客户端使用 FTP 客户端连接到服务器。找到 runtime 文件夹在 FTP 客户端中找到 PbootCMS 的安装目录,例如: /var/www/html/pbootcms删除 runtime 文件夹中的内容进入 runtime 文件夹,删除其中的所有文件和子文件夹。升级程序备份现有数据使用 FTP 客户端备份整个网…

FME Desktop/Form for Linux快速安装

太长不看:FME Flexnet Server许可证管理器快速安装包,适用于FME Desktop/Form/Engine/Server:com.safe.fme-flexnet-bbs-20240923.zip小问题1:由于ArcGIS的一些函数库是Windows下专有的,所以ArcGIS的一些专有格式,FME for Linux是无法处理的。小问题2:FME for Linux没有…

项目部署一:前期准备(本地上传、gitignore、ssh连接、密钥)

项目部署想要将django项目部署在服务器上,本质上需要三大部分:将代码上传到服务器 在服务器上 获取代码、安装服务、配置环境 启动服务1.代码同步 上传代码的方式有很多种,例如:FTP工具、scp命令、rsync服务、svn等,不过目前公司主流的都是使用git+代码托管平台。本地电脑…

高级语言程序设计第二个作业

属于课程:https://edu.cnblogs.com/campus/fzu/2024C/homework/13282 要求在:https://edu.cnblogs.com/campus/fzu/2024C/homework/13282 学号:102400117 姓名:廖逸轩以上是习题。这几个顺序是随机的,因为我最后编序号忘了哪个是哪个了...... 问题:printf里面输入引号里面…

高级语言程序设计课程第二次个人作业

这个作业属于哪个课程:https://edu.cnblogs.com/campus/fzu/2024C/ 这个作业要求在哪里:https://edu.cnblogs.com/campus/fzu/2024C/homework/13282 学号:102400227 姓名:谭培![](h ttps://img2024.cnblogs.com/blog/3525132/202409/3525132-20240930170319149-356045001.p…

vue2实现字体修改(全局/局部字体引入修改)/添加文字渐变色样式

1.创建一个全局 CSS 文件 创建一个单独的 CSS 文件,例如 fonts.css,然后在 main.js中引入。 fonts.css 文件内容: @font-face {font-family: youshebiaotihei;src: url(../../fonts/youshebiaotihei.ttf) format(truetype); /* 引用字体,但非全局使用 */font-weight: norma…