漫谈Winform控件优化之圆角矩形边框处理

简介: 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;//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);
        }
    }
}
参考 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;//Hover
            base.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;

            // 原始Region
            originRegion = Region;
        }

        public override void NotifyDefault(bool value)
        {
            base.NotifyDefault(false); // 去除窗体失去焦点时最新激活的按钮边框外观样式
        }

        //重写OnPaint
        protected 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控件优化之圆角Panel【绘制时需要注意的几点和扩展】

 
简介: 圆角的实现(原理和绘制方法)之前基本都已经介绍,本篇主要是实现圆角Panel时介绍几点注意点和一些扩展。一是BackColor应始终为Transparent;二是Draw完全显示绘制出的线条...

Winform控件优化之圆角Panel【绘制时需要注意的几点和扩展】

[toc]

圆角的实现(原理和绘制方法)之前基本都已经介绍,本篇主要是实现圆角Panel时介绍几点注意点和一些扩展。

新建RoundPanel.cs类文件,继承自Panle

public class RoundPanel : Panel{}

由于之前已经介绍圆角的实现,此处不再重复,只列出主要的点。

添加新窗体CustomPanelTest.cs,用于测试RoundPanel控件的使用和效果。

不要使用原控件BackColor属性的坑(BackColor应始终为Transparent)

在继承控件OnPaint方法或Paint事件中,重绘控件时有一个比较大的坑,那就是不要使用原控件的BackColor作为重绘时的背景色。

因为重绘的控件通常相对原Region长方形区域是不规则的,会有多出来的重绘之外的区域,除非完全用重绘区域内的路径范围创建新Region【但这又会产生锯齿和微不可见的白边】

在不新建Region的前提下,最正确的做法是设置扩展控件的背景色透明、无边框,然后重新绘制新背景和文字

比如,使用下面的代码在OnPaint方法中,你将永远看不到圆角效果(因为绘制的圆角图形和圆角外的其他部分各自颜色没有任何区别,看不出绘制的图形,被一样的背景色覆盖了)。

e.Graphics.DrawRoundRectAndCusp(new System.Drawing.Rectangle(0, 0, Width, Height), 15, BackColor);
DrawRoundRectAndCusp 为之前介绍的扩展方法,15为圆角半径。

基于此,可以在开启圆角绘制的情况下,实现对BackColor设置的限制,只能为Color.Transparent

半角半径属性RoundRadius大于等于0时,背景色透明。

[Category("Appearance"), Description("背景颜色,当RoundRadius>=0时取值为Color.Transparent,RoundRadius<0才可设置其他颜色")]
public override Color BackColor
{
    get => base.BackColor;
    set
    {
        if (RoundRadius >= 0)
        {
            base.BackColor = Color.Transparent;
        }
        else
        {
            base.BackColor = value;
        }
    }
}

绘制时使用圆角时的颜色属性roundNormalColor

e.Graphics.DrawRoundRectAndCusp(new System.Drawing.Rectangle(0, 0, Width, Height), 15, roundNormalColor);

Panel扩展控件实现圆角(Draw完全显示绘制出的线条)

Panel实现圆角和之前Button等介绍实现圆角的原理或过程没有任何区别。相对来说其需要的控件背景更少、也没有文字处理,反而更简单,只绘制个圆角即可。

本次实现边框是通过线条绘制(Graphics.DrawPath绘制路径线条、Graphics.DrawPolygon绘制points点坐标组成的线条)实现,在绘制路径时,默认是以路径为线条的中心,也就是路径内外个一半线条。而之前默认实现的圆角矩形路径正好是控件的范围,这就是导致路径外的一半线条超出控件,不会显示出来,产生一些问题。

if (borderPen != null)
{
    g.DrawPath(borderPen, path);
}

因此需要重新指定路径的范围,使其往内缩小半个BorderSize,从而绘制的线条完全显示(也可以修改线条绘制模式,使其在路径内层绘制)

但由于陷入了上面介绍的使用BackColor绘制背景的误区,以及,关于绘制Border的处理,将绘制路径减少roundBorderSize/2大小实现Draw完全显示线条,因此也单独做个记录。

整体减少roundBorderSize,开始位置移动roundBorderSize / 2

new Rectangle(ClientRectangle.X + roundBorderSize / 2, ClientRectangle.Y + roundBorderSize / 2, ClientRectangle.Width - roundBorderSize, ClientRectangle.Height - roundBorderSize)
Border绘制不完全显示出来的情况下,会导致不规格的圆角效果。

RoundPanel代码如下,关于扩展方法DrawRoundRectAndCusp、实现尖角效果等,可以查看之前的介绍。

using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
namespace CMControls
{
    public class RoundPanel : Panel
    {
        private Color roundNormalColor = SystemColors.Control;
        private int roundBorderSize = 0;
        private Color roundBorderColor = Color.MediumSlateBlue;
        private bool underlinedStyle = false;
        private int roundRadius = 20;

        /// <summary>
        /// 圆角按钮的半径属性
        /// </summary>
        [Category("Layout"), DefaultValue(20), Description("圆角半径,>=0时启用圆角按钮,等于0为直角,<0时使用默认控件样式")]
        public int RoundRadius
        {
            get { return roundRadius; }
            set
            {
                    roundRadius = value;
                    this.Invalidate();//Redraw control
            }
        }
        public override Color BackColor
        {
            get => base.BackColor;
            set
            {
                if (RoundRadius >= 0)
                {
                    base.BackColor = Color.Transparent;
                }
                else
                {
                    base.BackColor = value;
                }
            }
        }
        [CategoryAttribute("Appearance"),DefaultValue(typeof(Color), "SystemColors.Control"), Description("启用Radius圆角(RoundRadius>=0)时按钮标准颜色")]
        public Color RoundNormalColor
        {
            get { return roundNormalColor; }
            set
            {
                roundNormalColor = value;
                this.Invalidate();
            }
        }
        [CategoryAttribute("Appearance"), DefaultValue(0), Description("启用Radius圆角(RoundRadius>=0)时边框宽度,默认0")]
        public int RoundBorderWidth
        {
            get { return roundBorderSize; }
            set
            {
                roundBorderSize = value;
                this.Invalidate();

            }
        }
        [CategoryAttribute("Appearance"), DefaultValue(typeof(Color), "Color.MediumSlateBlue"), Description("启用Radius圆角(RoundRadius>=0)时边框颜色")]
        public Color RoundBorderColor
        {
            get => roundBorderColor;
            set
            {
                roundBorderColor = value;
                this.Invalidate();
            }
        }
        [CategoryAttribute("Appearance"), DefaultValue(false), Description("圆角边框宽度>0时,是否启用Underline,默认false")]
        public bool UnderlinedStyle
        {
            get { return underlinedStyle; }
            set
            {
                underlinedStyle = value;
                this.Invalidate();
            }
        }
        public RoundPanel()
        {
            BackColor = Color.Transparent;
        }
        //重写OnPaint
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            if (RoundRadius >= 0)
            {
                if (roundBorderSize > 0)
                {
                    using (var borderPen = new Pen(roundBorderColor, roundBorderSize))
                    {
                        if (underlinedStyle) //Line Style
                        {
                            e.Graphics.DrawLine(borderPen, 0, this.Height - roundBorderSize/2, this.Width, this.Height - roundBorderSize/2);
                        }
                        else
                        {
                            //borderPen.Alignment = PenAlignment.Inset;
                            e.Graphics.DrawRoundRectAndCusp(new Rectangle(ClientRectangle.X+ roundBorderSize / 2, ClientRectangle.Y+ roundBorderSize / 2, ClientRectangle.Width- roundBorderSize, ClientRectangle.Height - roundBorderSize), RoundRadius, roundNormalColor, borderPen: borderPen);
                        }
                    }
                }
                else
                {
                    e.Graphics.DrawRoundRectAndCusp(ClientRectangle, RoundRadius, roundNormalColor);
                }
            }
        }
    }
}

拖拽RoundPanel控件到窗体中,调整不同样式查看效果:

圆角模式,指定四个圆角的位置及组合

后面增加RoundMode属性,可以指定四个圆角的位置及其组合。

如下圆角Panel中的小圆角RoundPanel分别变换不同的圆角位置:

 

Winform控件优化之Paint事件实现圆角组件(提取绘制圆角的扩展方法)

 
简介: Paint事件方法中实现圆角控件不要通过事件参数`e.ClipRectangle`获取控件区域范围,原因见最后介绍;注意设置控件背景透明(参见[Winform控件优化之背景透明那些事2...

Windows 11下所有控件已经默认采用圆角,其效果更好、相对有着更好的优化,只是这是默认的行为,无法进一步自定义。

注意两点:

  • Paint事件方法中实现圆角控件不要通过事件参数e.ClipRectangle获取控件区域范围,原因见最后介绍的Paint事件中绘制控件的问题
  • 注意设置控件背景透明(虽然Winform中的透明不是完全透明,参见Winform控件优化之背景透明那些事

实现圆角控件比较好的一个思路是在控件的Paint事件中进行绘制,不太好的地方在于每次实现时都要添加事件处理,且不能在设计器中实时看到效果。

只要有Paint事件的组件都实现绘制圆角【如果没有,也可以通过将控件放在Panel中,借助Panel的圆角实现】。

Paint事件中实现圆角控件

注意:由于控件在Paint事件方法中进行了重新绘制,因此文本也被覆盖了,需要处理文本的绘制显示

新建项目 CustomControlRound,窗体中添加两个Panel、三个Button、一个Label控件。button设置Flat样式、边框为0。

然后在Paint事件方法中调用绘制圆角区域的方法(具体代码参见下一部分的扩展方法),具体如下:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        panel1.Paint += Panel1_Paint;
        panel2.Paint += Panel2_Paint;

        panel1.BackColor = panel2.BackColor = Color.Transparent;

        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;


        button2.Paint += Button1_Paint;
        button2.FlatStyle = FlatStyle.Flat;
        button2.FlatAppearance.BorderSize = 0;
        //button2.FlatAppearance.BorderColor = SystemColors.ButtonFace;
        button2.FlatAppearance.MouseDownBackColor = Color.Transparent;
        button2.FlatAppearance.MouseOverBackColor = Color.Transparent;
        button2.FlatAppearance.CheckedBackColor = Color.Transparent;

        button3.Paint += Button1_Paint;
        button3.FlatStyle = FlatStyle.Flat;
        button3.FlatAppearance.BorderSize = 0;
        //button3.FlatAppearance.BorderColor = SystemColors.ButtonFace;
        button3.FlatAppearance.MouseDownBackColor = Color.Transparent;
        button3.FlatAppearance.MouseOverBackColor = Color.Transparent;
        button3.FlatAppearance.CheckedBackColor = Color.Transparent;

        label1.Paint += Label1_Paint;
        label1.BackColor = Color.Transparent;
    }

    private void Label1_Paint(object sender, PaintEventArgs e)
    {
        var l = (Label)sender;
        // e.Graphics.DrawRoundRectAndCusp(e.ClipRectangle,  18, Color.FromArgb(180, 200, 210), Color.FromArgb(120, 120, 100)); // 不推荐
        e.Graphics.DrawRoundRectAndCusp(new Rectangle(0,0, l.Width,l.Height),  18, Color.FromArgb(180, 200, 210), Color.FromArgb(120, 120, 100));
    }

    private void Button1_Paint(object sender, PaintEventArgs e)
    {
        var btn = (Button)sender;
        e.Graphics.DrawRoundRectAndCusp(new Rectangle(0, 0, btn.Width, btn.Height), 18, Color.FromArgb(0, 122, 204), Color.FromArgb(8, 39, 57));

        ((Button)sender).NotifyDefault(false); // 去除窗体失去焦点时最新激活的按钮边框外观样式
    }

    private void Panel1_Paint(object sender, PaintEventArgs e)
    {
        var panel = sender as Panel;
        var rect=e.Graphics.DrawRoundRectAndCusp(new Rectangle(0, 0, panel.Width, panel.Height), 18, Color.FromArgb(90, 143, 0), Color.FromArgb(41, 67, 0),true, rectAlign);
    }

    private void Panel2_Paint(object sender, PaintEventArgs e)
    {
        var panel = sender as Panel;
        var rect = e.Graphics.DrawRoundRectAndCusp(new Rectangle(0, 0, panel.Width, panel.Height), 18, Color.FromArgb(113, 113, 113), Color.FromArgb(0, 0, 0));
    }
}

查看效果:

在Paint事件中不需要调用 base.OnPaint(e);,只有在继承控件重写 OnPaint方法时,才应该(在函数方法开始时)调用 base.OnPaint(e);

提取绘制圆角矩形和旁边小尖角的代码为扩展方法

介绍

下面是提取的扩展方法,对源代码进行了修改和优化。

1、可绘制渐变背景色、指定背景色、指定画刷绘制的背景;可指定渐变背景的方向
2、指定圆角半径的绘制,如果半径小于等于0,将绘制直角矩形。
3、指定三角小尖,默认不绘制;指定三角小尖的位置,可供八个位置选择
4、三角小尖的大小,目前感觉作为参数传递不够灵活,如果需要调整可直接从代码中修改,主要修改部分为:

// 尖角的大小 默认为 开始位置为_radius 底边为20,高度为13的等腰三角形
var cuspHemlineStart = _radius;
var cuspHemlineLength = 20;
var cuspHeight = 13;

扩展方法

扩展方法代码如下,将其放在namespace System.Drawing.Drawing2D下,可直接方便的从Graphics对象调用。

/// <summary>
/// 绘制可渐变的圆角矩形,并指定是否有三角小尖及其位置
/// </summary>
/// <param name="g"></param>
/// <param name="rectangle">矩形区域</param>
/// <param name="_radius">圆角半径</param>
/// <param name="begin_bgcolor">背景渐变开始色</param>
/// <param name="end_bgcolor">背景渐变结束色</param>
/// <param name="cusp">是否有三角小尖,默认无</param>
/// <param name="rectAlign">三角小尖的位置,默认右上</param>
/// <param name="gradientMode">渐变模式,默认垂直方向渐变</param>
public static void DrawRoundRectAndCusp(this Graphics g, Rectangle rectangle, int _radius, Color begin_bgcolor, Color end_bgcolor, bool cusp = false, RectangleAlign rectAlign= RectangleAlign.RightTop, LinearGradientMode gradientMode = LinearGradientMode.Vertical)
{
    ////抗锯齿 等模式在Graphics外层自定义设置
    //g.SmoothingMode = SmoothingMode.AntiAlias;
    //渐变填充
    LinearGradientBrush linearGradientBrush = new LinearGradientBrush(rectangle, begin_bgcolor, end_bgcolor, gradientMode);
    g.DrawRoundRectAndCusp(rectangle, _radius, linearGradientBrush, cusp, rectAlign);
}

/// <summary>
/// 绘制指定背景的圆角矩形,并指定是否有三角小尖及其位置
/// </summary>
/// <param name="g"></param>
/// <param name="rectangle">矩形区域</param>
/// <param name="_radius">圆角半径</param>
/// <param name="bgcolor">指定背景色</param>
/// <param name="cusp">是否有三角小尖,默认无</param>
/// <param name="rectAlign">三角小尖的位置,默认右上</param>
public static void DrawRoundRectAndCusp(this Graphics g, Rectangle rectangle, int _radius, Color bgcolor, bool cusp = false, RectangleAlign rectAlign = RectangleAlign.RightTop)
{
    ////抗锯齿 等模式在Graphics外层自定义设置
    //g.SmoothingMode = SmoothingMode.AntiAlias;
    //渐变填充
    var  brush = new SolidBrush(bgcolor);
    g.DrawRoundRectAndCusp(rectangle, _radius, brush, cusp, rectAlign);
}

/// <summary>
/// 绘制Brush画刷的圆角矩形,并指定是否有三角小尖及其位置
/// </summary>
/// <param name="g"></param>
/// <param name="rectangle">矩形区域</param>
/// <param name="_radius">圆角半径</param>
/// <param name="bgbrush">指定背景画刷</param>
/// <param name="cusp">是否有三角小尖,默认无</param>
/// <param name="rectAlign">三角小尖的位置,默认右上</param>
public static void DrawRoundRectAndCusp(this Graphics g, Rectangle rectangle, int _radius, Brush bgbrush, bool cusp = false, RectangleAlign rectAlign = RectangleAlign.RightTop)
{
    ////抗锯齿 尽可能高质量绘制
    g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
    g.PixelOffsetMode = PixelOffsetMode.HighQuality;
    g.SmoothingMode = SmoothingMode.AntiAlias; // SmoothingMode.HighQuality 
    g.CompositingQuality = CompositingQuality.HighQuality;
    g.InterpolationMode = InterpolationMode.HighQualityBilinear;

    var rect = rectangle;
    //画尖角 对应的变更rect区域
    if (cusp)
    {
        // 尖角的大小 默认为 开始位置为_radius 底边为20,高度为13的等腰三角形
        var cuspHemlineStart = _radius;
        var cuspHemlineLength = 20;
        var cuspHeight = 13;

        // 让位出来的间隔暂时为尖角高度-1
        var span = cuspHeight - 1;

        // 三角顶点
        PointF p1, p2, p3;

        switch (rectAlign)
        {
            case RectangleAlign.AboveLeft:
                p1 = new PointF(rectangle.X + cuspHemlineStart, rectangle.Y + cuspHeight);
                p2 = new PointF(rectangle.X + cuspHemlineStart + cuspHemlineLength, rectangle.Y + cuspHeight);
                p3 = new PointF(rectangle.X + cuspHemlineStart + cuspHemlineLength / 2, rectangle.Y);
                rect = new Rectangle(rectangle.X, rectangle.Y + span, rectangle.Width, rectangle.Height - span);
                break;
            case RectangleAlign.AboveRight:
                p1 = new PointF(rectangle.Right - cuspHemlineStart, rectangle.Y + cuspHeight);
                p2 = new PointF(rectangle.Right - cuspHemlineStart - cuspHemlineLength, rectangle.Y + cuspHeight);
                p3 = new PointF(rectangle.Right - cuspHemlineStart - cuspHemlineLength / 2, rectangle.Y);
                rect = new Rectangle(rectangle.X, rectangle.Y + span, rectangle.Width, rectangle.Height - span);
                break;
            case RectangleAlign.RightBottom:
                p1 = new PointF(rectangle.Right - cuspHeight, rectangle.Bottom - cuspHemlineStart);
                p2 = new PointF(rectangle.Right - cuspHeight, rectangle.Bottom - cuspHemlineStart - cuspHemlineLength);
                p3 = new PointF(rectangle.Right, rectangle.Bottom - cuspHemlineStart - cuspHemlineLength / 2);
                rect = new Rectangle(rectangle.X, rectangle.Y, rectangle.Width - span, rectangle.Height);
                break;
            case RectangleAlign.BelowRight:
                p1 = new PointF(rectangle.Right - cuspHemlineStart, rectangle.Bottom - cuspHeight);
                p2 = new PointF(rectangle.Right - cuspHemlineStart - cuspHemlineLength, rectangle.Bottom - cuspHeight);
                p3 = new PointF(rectangle.Right - cuspHemlineStart - cuspHemlineLength / 2, rectangle.Bottom);
                rect = new Rectangle(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height - span);
                break;
            case RectangleAlign.BelowLeft:
                p1 = new PointF(rectangle.X + cuspHemlineStart, rectangle.Bottom - cuspHeight);
                p2 = new PointF(rectangle.X + cuspHemlineStart + cuspHemlineLength, rectangle.Bottom - cuspHeight);
                p3 = new PointF(rectangle.X + cuspHemlineStart + cuspHemlineLength / 2, rectangle.Bottom);
                rect = new Rectangle(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height - span);
                break;
            case RectangleAlign.LeftBottom:
                p1 = new PointF(rectangle.X + cuspHeight, rectangle.Bottom - cuspHemlineStart);
                p2 = new PointF(rectangle.X + cuspHeight, rectangle.Bottom - cuspHemlineStart - cuspHemlineLength);
                p3 = new PointF(rectangle.X, rectangle.Bottom - cuspHemlineStart - cuspHemlineLength / 2);
                rect = new Rectangle(rectangle.X + span, rectangle.Y, rectangle.Width - span, rectangle.Height);
                break;
            case RectangleAlign.LeftTop:
                p1 = new PointF(rectangle.X + cuspHeight, rectangle.Y + cuspHemlineStart);
                p2 = new PointF(rectangle.X + cuspHeight, rectangle.Y + cuspHemlineStart + cuspHemlineLength);
                p3 = new PointF(rectangle.X, rectangle.Y + cuspHemlineStart + cuspHemlineLength / 2);
                rect = new Rectangle(rectangle.X + span, rectangle.Y, rectangle.Width - span, rectangle.Height);
                break;
            case RectangleAlign.RightTop:
            default:
                p1 = new PointF(rectangle.Right - cuspHeight, rectangle.Y + cuspHemlineStart);
                p2 = new PointF(rectangle.Right - cuspHeight, rectangle.Y + cuspHemlineStart + cuspHemlineLength);
                p3 = new PointF(rectangle.Right, rectangle.Y + cuspHemlineStart + cuspHemlineLength / 2);
                rect = new Rectangle(rectangle.X, rectangle.Y, rectangle.Width - span, rectangle.Height);
                break;
        }

        PointF[] ptsArray = new PointF[] { p1, p2, p3 };

        // 填充参数点所指定的多边形内部
        g.FillPolygon(bgbrush, ptsArray);
    }
    //填充
    g.FillPath(bgbrush, rect.GetRoundedRectPath(_radius));
}

/// <summary> 
/// 根据普通矩形得到圆角矩形的路径 【根据矩形区域rect,计算呈现radius圆角的Graphics路径】
/// </summary> 
/// <param name="rect">原始矩形</param> 
/// <param name="radius">半径</param> 
/// <returns>图形路径</returns> 
public static GraphicsPath GetRoundedRectPath(this Rectangle rect, int radius)
{
    #region 正确绘制圆角矩形区域
    int R = radius*2;
    Rectangle arcRect = new Rectangle(rect.Location, new Size(R, R));
    GraphicsPath path = new GraphicsPath();
    
    if (radius <= 0)
    {
        path.AddRectangle(rect);
    }
    else
    {
        // 左上圆弧 左手坐标系,顺时针为正 从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();
    // 闭合路径中所有开放图形,并形成新图形
    path.CloseAllFigures();
    return path;
    #endregion
}

/// <summary> 
/// 获取圆角矩形的路径 
/// </summary> 
/// <param name="rect">原始矩形</param> 
/// <param name="radius">半径</param> 
/// <returns>图形路径</returns> 
public static GraphicsPath GetRoundedRectPath(int x, int y, int width, int height, int radius)
{           
    Rectangle rect = new Rectangle(x, y, width, height);
    return rect.GetRoundedRectPath(radius);
}

测试尖角的显示位置

复制几个Panel,在Paint事件中分别处理尖角的位置

panel1.Paint += Panel1_Paint;
panel3.Paint += Panel1_Paint;
panel4.Paint += Panel1_Paint;
panel5.Paint += Panel1_Paint;
panel6.Paint += Panel1_Paint;
panel7.Paint += Panel1_Paint;
panel8.Paint += Panel1_Paint;
panel9.Paint += Panel1_Paint;

/// ......

private void Panel1_Paint(object sender, PaintEventArgs e)
{
    var panel = sender as Panel;
    var rectAlign = RectangleAlign.RightTop;
    switch (panel.Name)
    {
        case "panel3":
            rectAlign = RectangleAlign.AboveLeft;
            break;
        case "panel4":
            rectAlign = RectangleAlign.AboveRight;
            break;
        case "panel5":
            rectAlign = RectangleAlign.BelowLeft;
            break;
        case "panel6":
            rectAlign = RectangleAlign.BelowRight;
            break;
        case "panel7":
            rectAlign = RectangleAlign.LeftBottom;
            break;
        case "panel8":
            rectAlign = RectangleAlign.LeftTop;
            break;
        case "panel9":
            rectAlign = RectangleAlign.RightBottom;
            break;
        default:
            break;
    }

    var rect=e.Graphics.DrawRoundRectAndCusp(new Rectangle(0, 0, panel.Width, panel.Height), 18, Color.FromArgb(90, 143, 0), Color.FromArgb(41, 67, 0),true, rectAlign);

    e.Graphics.DrawText(rect, "这是一个Panel控件,非常适合显示消息", Color.White, panel.Font);
}

查看效果

重绘控件后文本的处理

由于重新绘制导致文本也被覆盖的情况,有比较常见的两种处理方法:

  • 重绘控件的同时,重绘文本文字
  • 通过添加Label控件实现文本处理

同时重绘文本

同样将绘制文本的方法提取为扩展方法,借助StringFormat字符串格式对象实现文字的布局处理。

不推荐使用 Graphics.DrawString绘制按钮控件(Rectangle区域)内的文字,当然DrawString也有一定有时比如文字方向的处理。
/// <summary>
/// 绘制(控件区域)文本内容
/// </summary>
/// <param name="g"></param>
/// <param name="rect"></param>
/// <param name="text"></param>
/// <param name="color"></param>
/// <param name="font"></param>
/// <param name="_textAlign">文字布局,默认居中。实际测试并未真正的居中,垂直方向偏上,改为通过计算rect的中心位置实现,使用微软雅黑还好点,字体大小最好偶数</param>
/// <param name="rtl">是否RightToLeft 无效果,不推荐使用</param>
public static void DrawText(this Graphics g, Rectangle rect, string text, Color color, Font font, ContentAlignment _textAlign = ContentAlignment.MiddleCenter, bool rtl=false)
{             
        var formatFlags = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter; // 默认居中
        switch (_textAlign)
        {
            case ContentAlignment.TopLeft:
                formatFlags = TextFormatFlags.Top | TextFormatFlags.Left;
                break;
            case ContentAlignment.TopCenter:
                formatFlags = TextFormatFlags.Top | TextFormatFlags.HorizontalCenter;
                break;
            case ContentAlignment.TopRight:
                formatFlags = TextFormatFlags.Top | TextFormatFlags.Right;
                break;
            case ContentAlignment.MiddleLeft:
                formatFlags = TextFormatFlags.VerticalCenter | TextFormatFlags.Left;
                break;
            case ContentAlignment.MiddleRight:
                formatFlags = TextFormatFlags.VerticalCenter | TextFormatFlags.Right;
                break;
            case ContentAlignment.BottomLeft:
                formatFlags = TextFormatFlags.Bottom | TextFormatFlags.Left;
                break;
            case ContentAlignment.BottomCenter:
                formatFlags = TextFormatFlags.Bottom | TextFormatFlags.HorizontalCenter;
                break;
            case ContentAlignment.BottomRight:
                formatFlags = TextFormatFlags.Bottom | TextFormatFlags.Right;
                break;
            case ContentAlignment.MiddleCenter:
            default:
                break;
        }
        if (rtl)
        {
            formatFlags |= TextFormatFlags.RightToLeft; // 无效果
        }
        TextRenderer.DrawText(g, text, font, rect, color, formatFlags);            
}

要注意添加了小三角后的文字绘制区域问题,如果使用默认的全部预期有可能导致文字超出范围,且,布局也不是相对于绘制的主体。如下图对比的效果。

因此,可修改圆角绘制的函数,使其返回绘制后的主体区域。

private void Panel1_Paint(object sender, PaintEventArgs e)
{
    var panel = sender as Panel;
    var rect=e.Graphics.DrawRoundRectAndCusp(new Rectangle(0, 0, panel.Width, panel.Height), 18, Color.FromArgb(90, 143, 0), Color.FromArgb(41, 67, 0),true, rectAlign);
    
    //e.Graphics.DrawText(e.ClipRectangle, "这是一个Panel控件,非常适合显示消息", Color.White, panel.Font);
    // 使用合适的区域
    e.Graphics.DrawText(rect, "这是一个Panel控件,非常适合显示消息", Color.White, panel.Font);
}

通过添加Label控件实现对文本的处理【有尖角时需要额外处理】

label2.Text = "我是Label显示在圆角按钮上";
label2.Parent = button1;
label2.AutoSize = false;
label2.Dock = DockStyle.Fill;
label2.BackColor = Color.Transparent;
label2.TextAlign = ContentAlignment.MiddleCenter;
label2.ForeColor = Color.Wheat;

Paint事件中绘制圆角的优点

完全由用户绘制按钮区域实现圆角【之前文章介绍过】,可以发现,与直接在Paint事件中实现圆角,有着一定的锯齿,虽然不是很严重,但是还是Paint事件中实现的圆角看着相对好一些。

之前完全由用户绘制控件区域出现锯齿的问题,原因在于使用 new Region(graphicPath)构造函数创建的新Region对象,创建的Region无法实现抗锯齿,即使开始抗锯齿和高质量绘制(无法应用到最终的Region)。

[唯一的]解决办法,是不创建新的Region,直接绘制无锯齿图形【无法对Region进行抗锯齿】。

窗体失去焦点时按钮外观边框问题

窗体失去焦点后,Button会显示出一个边框(即使已经设置BorderSize为0),只有在窗体失去焦点后,最后一个被激活过的Button(点击过的)才会出现。

比较好的解决办法是在Button的Paint事件中执行Button.NotifyDefault(false)方法。

下面是几种解决方式:

  • 通过继承Button控件,重写NotifyDefault方法实现。
class MyButton:Button
{
    public override void NotifyDefault(bool value)
    {
        base.NotifyDefault(false);
    }
}
参考自 Removing the focus rectangle from a flat button when the form itself does not have focus
  • 设置FlatAppearance.BorderColor颜色与下层背景色一致

或者,还有一个取巧的解决办法,就是,设置FlatAppearance.BorderColor为Button按钮后面的背景色,这样就看不出来,类似没有的效果。如 button1.FlatAppearance.BorderColor = SystemColors.Control;

  • 在Button的Paint事件方法中调用NotifyDefault(false)

或者,不是继承重写Button控件,直接在Button的Paint事件中,调用Button.NotifyDefault(false);方法。

private void Button1_Paint(object sender, PaintEventArgs e)
{
    var btn = (Button)sender;
    e.Graphics.DrawRoundRectAndCusp(new Rectangle(0, 0, btn.Width, btn.Height), 18, Color.FromArgb(0, 122, 204), Color.FromArgb(8, 39, 57));

    ((Button)sender).NotifyDefault(false); // 去除窗体失去焦点时最新激活的按钮边框外观样式
}

Paint事件中绘制控件的问题【不要使用(事件)参数e.ClipRectangle

直接看下图演示,在发生控件大小、位置调整等需要重绘时,会发生控件错乱、显示不完整重叠等问题。

比如Panel事件中,原本的写法为:

var rect=e.Graphics.DrawRoundRectAndCusp(e.ClipRectangle, 18, Color.FromArgb(90, 143, 0), Color.FromArgb(41, 67, 0),true, rectAlign);

将其改为转换传递过来的控件参数为Panel对象,并通过其宽高获取其绘制区域:

var rect=e.Graphics.DrawRoundRectAndCusp(new Rectangle(0, 0, panel.Width, panel.Height), 18, Color.FromArgb(90, 143, 0), Color.FromArgb(41, 67, 0),true, rectAlign);

修改后错乱问题就不存在了:

参考

参考自 初学c# -- 学习笔记(六) winfrom组件圆角


注:本文转自代码迷途_社区达人页-阿里云开发者社区
posted @   soliang  阅读(278)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
点击右上角即可分享
微信分享提示