在 WinForms 项目里,默认 Button 控件往往够用,但一旦对界面质感有要求,圆角边缘粗糙、边框不自然的问题就会很明显。本文按“能不能做、怎么做、为什么这样画”三个问题展开,带你用 GDI+ 做一个可配置、可复用的高质量圆角按钮控件,并说明双层绘制机制为什么能把显示效果拉开差距。
为什么要自己做圆角按钮
WinForms 默认按钮的外观能力有限,尤其是圆角效果和边缘平滑度,通常很难直接满足定制化 UI 的要求。相比简单改色或改边框,自己继承 Button 并接管绘制逻辑,有几个明显好处:
- 边框大小可以独立控制
- 圆角半径可以按需求调整
- 边框颜色、背景色、文字颜色都能直接配置
- 通过抗锯齿和路径绘制获得更平滑的圆角效果
这类控件的核心价值,不只是“做成圆角”,而是让它在不同尺寸、不同背景色、不同主题下都保持一致的显示质量。
先搭好自定义按钮类和可配置属性
创建继承自 Button 的控件类
第一步是创建一个继承自 Button 的类,并定义边框尺寸、圆角半径、边框颜色这几个基础字段:
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.ComponentModel;
public class RoundButton : Button
{
// 字段定义
private int borderSize = 0;
private int borderRadius = 20;
private Color borderColor = Color.PaleVioletRed;
}
补齐设计期和运行期都能用的属性
接下来把这些字段暴露成属性,并在修改后调用 Invalidate() 触发重绘。这样做的意义很直接:只要属性变了,按钮就会立即按新参数重新绘制。
[Category("Round Button")]
public int BorderSize
{
get => borderSize;
set
{
borderSize = value;
Invalidate();
}
}
[Category("Round Button")]
public int BorderRadius
{
get => borderRadius;
set
{
borderRadius = value;
Invalidate();
}
}
[Category("Round Button")]
public Color BorderColor
{
get => borderColor;
set
{
borderColor = value;
Invalidate();
}
}
[Category("Round Button")]
public Color BackgroundColor
{
get => BackColor;
set => BackColor = value;
}
[Category("Round Button")]
public Color TextColor
{
get => ForeColor;
set => ForeColor = value;
}
这里的 BackgroundColor 和 TextColor 实际上分别映射到 BackColor 与 ForeColor,这样可以继续沿用 WinForms 本身的颜色体系,又能让控件对外保持统一配置入口。
默认样式怎么设,圆角半径怎么限制
在构造函数里设定初始外观
构造函数主要负责初始化默认样式,包括扁平化风格、默认尺寸、背景色和文字色,同时去掉系统默认边框:

public RoundButton()
{
FlatStyle = FlatStyle.Flat;
FlatAppearance.BorderSize = 0;
Size = new Size(150, 40);
BackColor = Color.MediumSlateBlue;
ForeColor = Color.White;
Resize += new EventHandler(Button_Resize);
}
private void Button_Resize(object sender, EventArgs e)
{
if (borderRadius > Height)
borderRadius = Height;
}
为什么要在 Resize 时限制半径
圆角半径如果超过按钮高度,就会导致路径计算不合理,极端情况下甚至让图形边缘出现异常。这里通过 Resize 事件把 borderRadius 限制在 Height 以内,本质上是在保证圆角参数始终落在可绘制范围内。
这一步看起来简单,但对控件稳定性很关键,尤其是在窗体缩放、运行时动态改尺寸的场景里。
圆角路径和 OnPaint 才是绘制质量的核心
用 GraphicsPath 生成圆角路径
圆角按钮并不是“画一个圆角矩形”这么简单,底层仍然要拆成四段弧线拼出完整轮廓。这里通过 GraphicsPath 依次处理左上、右上、右下、左下四个角:
private GraphicsPath GetFigurePath(Rectangle rect, float radius)
{
GraphicsPath path = new GraphicsPath();
float curveSize = radius * 2F;
path.StartFigure();
// 左上角
path.AddArc(rect.X, rect.Y, curveSize, curveSize, 180, 90);
// 右上角
path.AddArc(rect.Right - curveSize, rect.Y, curveSize, curveSize, 270, 90);
// 右下角
path.AddArc(rect.Right - curveSize, rect.Bottom - curveSize, curveSize, curveSize, 0, 90);
// 左下角
path.AddArc(rect.X, rect.Bottom - curveSize, curveSize, curveSize, 90, 90);
path.CloseFigure();
return path;
}
有了这段方法,后面的绘制逻辑就不需要手工处理每条边和每个角,外层轮廓与内层边框都可以共用同一套路径生成方式。
重写 OnPaint 进行实际绘制
真正决定显示质量的是 OnPaint。这段代码同时处理了按钮区域、边框区域、抗锯齿、边框绘制,以及“圆角按钮”和“普通按钮”两种分支:
protected override void OnPaint(PaintEventArgs pevent)
{
base.OnPaint(pevent);
Rectangle rectSurface = ClientRectangle;
Rectangle rectBorder = Rectangle.Inflate(rectSurface, -borderSize, -borderSize);
int smoothSize = 2;
if (borderSize > 0)
smoothSize = borderSize;
if (borderRadius > 2) // 圆角按钮
{
using (GraphicsPath pathSurface = GetFigurePath(rectSurface, borderRadius))
using (GraphicsPath pathBorder = GetFigurePath(rectBorder, borderRadius - borderSize))
using (Pen penSurface = new Pen(Parent.BackColor, smoothSize))
using (Pen penBorder = new Pen(borderColor, borderSize))
{
pevent.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
Region = new Region(pathSurface);
pevent.Graphics.DrawPath(penSurface, pathSurface);
if (borderSize >= 1)
pevent.Graphics.DrawPath(penBorder, pathBorder);
}
}
else // 普通按钮
{
pevent.Graphics.SmoothingMode = SmoothingMode.None;
Region = new Region(rectSurface);
if (borderSize >= 1)
{
using (Pen penBorder = new Pen(borderColor, borderSize))
{
penBorder.Alignment = PenAlignment.Inset;
pevent.Graphics.DrawRectangle(penBorder, 0, 0, Width - 1, Height - 1);
}
}
}
}
这里有几个关键点值得单独看:
rectSurface表示按钮完整区域,rectBorder则按边框宽度向内收缩smoothSize用来控制外层平滑处理的笔宽- 当
borderRadius > 2时走圆角分支,否则按普通矩形按钮处理 Region = new Region(pathSurface)会把按钮点击区域也裁成对应形状
双层绘制机制为什么能让圆角更平滑
这套实现最关键的设计,不是单纯打开 SmoothingMode.AntiAlias,而是使用了双层路径:

- 外层路径
pathSurface:定义按钮整体轮廓,并负责边缘平滑过渡 - 内层路径
pathBorder:按边框厚度内缩后再绘制真实边框
对应代码如下:
protected override void OnPaint(PaintEventArgs pevent)
{
base.OnPaint(pevent);
Rectangle rectSurface = ClientRectangle;
Rectangle rectBorder = Rectangle.Inflate(rectSurface, -borderSize, -borderSize);
int smoothSize = 2;
if (borderSize > 0) smoothSize = borderSize;
if (borderRadius > 2)
{
using (GraphicsPath pathSurface = GetFigurePath(rectSurface, borderRadius))
using (GraphicsPath pathBorder = GetFigurePath(rectBorder, borderRadius - borderSize))
using (Pen penSurface = new Pen(Parent.BackColor, smoothSize))
using (Pen penBorder = new Pen(borderColor, borderSize))
{
pevent.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
Region = new Region(pathSurface);
pevent.Graphics.DrawPath(penSurface, pathSurface);
if (borderSize >= 1)
pevent.Graphics.DrawPath(penBorder, pathBorder);
}
}
}
penSurface 使用的是 Parent.BackColor,作用相当于先沿着外轮廓补一圈与父容器一致的平滑边缘,再在内部画正式边框。这样处理后,圆角边缘会比单次描边自然得多,尤其是在深浅对比明显的背景里,锯齿感会明显减轻。
父容器背景色变化时为什么还要额外刷新
因为外层平滑边缘使用了 Parent.BackColor,所以一旦父容器背景色变化,按钮边缘也必须同步重绘。否则按钮本身颜色没变,但边缘仍保留旧背景色,就会出现一圈不协调的描边。

处理方式是在控件句柄创建后监听父容器的 BackColorChanged 事件:
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
Parent.BackColorChanged += Container_BackColorChanged;
}
private void Container_BackColorChanged(object sender, EventArgs e)
{
Invalidate();
}
这一步属于很容易漏掉的细节,但它直接影响控件在实际项目中的一致性,特别是在主题切换、面板换肤或动态背景场景里。
总结:这套方案适合什么场景
通过继承 Button 并结合 GDI+ 的 GraphicsPath、Pen、SmoothingMode 和 Region,可以做出一个既能配置边框和圆角,又能保持较高显示质量的 WinForms 按钮控件。
如果你的需求只是简单换颜色,默认控件可能已经够用;但如果你希望按钮在桌面应用里承担更明显的视觉角色,尤其要兼顾圆角边缘质量、边框精度和主题适配,那么这种双层绘制方式会比直接套默认样式更靠谱。













