
简介这是一份面向C# WinForm开发者的圆形进度条自定义控件源码资源适用于需要美化UI交互、替代系统默认进度条的桌面应用开发场景尤其适合初学者理解GDI绘图与控件重绘机制也便于中高级开发者快速集成到项目中。压缩包共20个文件包含6个核心C#源码文件含主窗体、设计器及程序入口、3个配置文件App.config等、2个.NET Framework 4.0兼容的第三方控件DLLHZH_Controls、2个本地化资源文件.resx以及项目解决方案.sln、工程文件.csproj、调试符号.pdb和NuGet包.nupkg等完整覆盖编译、调试与部署所需组件总大小16.81MB。已有728人学习下载。读者可直接运行EXE演示效果深入研究圆形进度条的弧线绘制逻辑、动画刷新策略与属性绑定实现并基于现有结构扩展颜色渐变、文字显示、事件回调等功能代码结构清晰、注释规范具备良好的可读性与二次开发基础。1. 项目缘起为什么我们需要一个圆形进度条在桌面应用开发中进度条是提升用户体验的关键组件之一。标准的WinForms工具箱提供了ProgressBar控件但它是一个单调的矩形条。当你的应用需要一点设计感或者界面空间有限比如在圆形头像旁显示加载进度又或者你只是想打破常规让应用看起来更酷、更现代时一个自定义的圆形进度条就成了必需品。我最近在重构一个设备监控的上位机软件时就遇到了这个需求。仪表盘上需要展示多个传感器的实时数据加载状态矩形的进度条与圆形的仪表盘格格不入。虽然网上能找到一些例子但要么封装得过于复杂难以定制要么功能简陋比如不支持渐变、文字显示不灵活。于是我决定自己动手从零实现一个既美观又实用的圆形进度条控件。这个控件的目标很明确易于集成、高度可定制、性能良好。下面我就把这个从设计思路到完整源码的实现过程分享出来你可以直接拿去用在你的WinForm项目里。2. 核心设计自绘控件的实现原理与架构WinForms控件绘制主要分为两种方式用户绘制Owner Draw和完全自绘Custom Draw。对于圆形进度条这种高度定制化的控件我们选择继承Control基类重写OnPaint方法进行完全自绘。这样做能获得最大的灵活性和控制权。2.1 控件的属性设计一个好的自定义控件首先要有清晰、合理的属性。我们的圆形进度条需要控制其外观和行为我设计了以下核心属性public class CircularProgressBar : Control { // 进度值 (0-100) private int _value 0; [DefaultValue(0)] [Description(当前进度值范围0-100)] public int Value { get { return _value; } set { _value Math.Max(0, Math.Min(100, value)); this.Invalidate(); } } // 进度条宽度 private int _barWidth 10; [DefaultValue(10)] [Description(圆形进度条的宽度像素)] public int BarWidth { get { return _barWidth; } set { _barWidth Math.Max(1, value); this.Invalidate(); } } // 进度条颜色支持渐变 private Color _progressColor Color.DodgerBlue; [Description(进度条的颜色)] public Color ProgressColor { get { return _progressColor; } set { _progressColor value; this.Invalidate(); } } // 背景条颜色 private Color _backColor Color.LightGray; [Description(进度条背景的颜色)] public new Color BackColor { get { return _backColor; } set { _backColor value; this.Invalidate(); } } // 是否显示百分比文字 private bool _showText true; [DefaultValue(true)] [Description(是否在中心显示百分比文字)] public bool ShowText { get { return _showText; } set { _showText value; this.Invalidate(); } } // 文字格式 private string _textFormat {0}%; [Description(显示文字的格式{0}会被替换为进度值)] public string TextFormat { get { return _textFormat; } set { _textFormat value; this.Invalidate(); } } }设计解析Value属性这是核心数据。我们通过Math.Max和Math.Min将其钳制在0-100之间避免非法值。setter中调用this.Invalidate()是关键它通知控件需要重绘从而实现进度变化的实时可视化。BarWidth属性控制圆环的粗细。这里也做了最小值为1的保护。颜色属性分别控制前景进度和背景。注意我们new了BackColor属性因为基类Control已有此属性我们这里特指进度条背景与控件的实际背景区分开。ShowText和TextFormat属性提供了显示文字的灵活性。TextFormat允许用户自定义显示格式例如显示为“75%完成”或单纯数字“75”。2.2 绘制区域的计算逻辑在OnPaint方法中我们首先要确定绘制圆环的矩形区域。一个常见的误区是直接在控件的整个客户区ClientRectangle上画圆这会导致圆环紧贴边缘不美观。正确的做法是内缩一个边距这个边距至少为进度条宽度BarWidth的一半以确保圆环完整显示。protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); Graphics g e.Graphics; g.SmoothingMode SmoothingMode.AntiAlias; // 关键启用抗锯齿让边缘平滑 g.TextRenderingHint TextRenderingHint.AntiAliasGridFit; // 文字也抗锯齿 // 计算绘制圆环的矩形区域 int margin this.BarWidth; // 留出足够空间至少是BarWidth Rectangle rect new Rectangle( margin, margin, this.ClientSize.Width - 2 * margin, this.ClientSize.Height - 2 * margin ); // 确保矩形有效宽高为正 if (rect.Width 0 || rect.Height 0) return; // ... 后续绘制代码 }注意SmoothingMode.AntiAlias是让圆形看起来光滑不锯齿的关键设置务必在绘图开始前指定。TextRenderingHint对中心文字同样重要。3. 关键步骤详解从背景到前景的绘制过程绘制顺序遵循“从底层到顶层”的原则先画背景圆环再画前景进度圆环最后叠加文字。3.1 绘制背景圆环背景圆环是一个完整的、颜色较浅的圆环代表了进度的“总长度”。// 1. 绘制背景圆环 using (Pen backPen new Pen(this._backColor, this.BarWidth)) { // 调整Pen的对齐方式为居中这样线条会以我们计算的rect边界为中心向内外扩展 backPen.Alignment PenAlignment.Center; g.DrawArc(backPen, rect, 0, 360); // 从0度到360度画一个完整的圆 }这里使用了PenAlignment.Center。这是什么意思呢想象一下你用一支粗笔画圆笔尖的哪个部分沿着你设定的路径走Center意味着笔画的中心线沿着路径笔画向路径内外各扩展一半的宽度。这确保了无论BarWidth多粗圆环都能完美地容纳在我们计算的rect矩形内。3.2 绘制前景进度圆环这是控件的灵魂。我们需要根据Value属性0-100来计算应该绘制多少弧度的圆环。在GDI中DrawArc方法的角度是以度为单位并且顺时针方向为正方向。通常我们从顶部-90度开始绘制这样0%在顶部随着进度增加弧线顺时针延伸。// 2. 绘制前景进度圆环 if (this._value 0) { using (Pen progressPen new Pen(this._progressColor, this.BarWidth)) { progressPen.Alignment PenAlignment.Center; // 计算进度对应的角度360度 * 进度百分比 float sweepAngle 360f * (this._value / 100f); // 起始角度设为-90度12点钟方向扫过sweepAngle度 // 注意DrawArc的起始角度0度是X轴正方向3点钟方向顺时针为正。 // 我们从-90度开始就是12点钟方向。 g.DrawArc(progressPen, rect, -90, sweepAngle); } }角度计算详解sweepAngle 360 * (Value / 100)。当Value为100时sweepAngle为360画一个整圆。DrawArc的startAngle参数设为-90。在数学坐标系中0度指向右东。-90度则指向上北这符合我们对圆形进度条从顶部开始的普遍认知。这里有一个隐藏的坑DrawArc方法在绘制时如果sweepAngle非常接近360比如359.99在某些系统或缩放比例下可能不会闭合为一个完美的圆起点和终点之间会有一个微小缝隙。一个稳健的做法是当Value为100时直接调用DrawEllipse画一个完整的实心圆或圆环而不是用DrawArc。3.3 绘制中心文字文字绘制需要精确定位使其居于圆环中心。我们使用Graphics.MeasureString获取文字尺寸然后计算绘制起点。// 3. 绘制中心文字 if (this._showText !string.IsNullOrEmpty(this._textFormat)) { string text string.Format(this._textFormat, this._value); using (Font textFont new Font(this.Font.FontFamily, GetOptimalFontSize(rect), FontStyle.Bold)) using (Brush textBrush new SolidBrush(this.ForeColor)) { SizeF textSize g.MeasureString(text, textFont); PointF textLocation new PointF( rect.Left (rect.Width - textSize.Width) / 2, rect.Top (rect.Height - textSize.Height) / 2 ); g.DrawString(text, textFont, textBrush, textLocation); } }字体大小自适应上面的代码中我调用了一个GetOptimalFontSize方法。这是一个非常重要的细节——如果控件大小发生变化固定字号的文字可能太大或太小。一个简单的自适应策略是让字体大小与控件高度或圆环直径成比例。private float GetOptimalFontSize(Rectangle rect) { // 基于圆环直径的0.2倍作为初始字体大小并限制在最小6号最大72号之间 float baseSize Math.Min(rect.Width, rect.Height) * 0.2f; return Math.Max(6, Math.Min(baseSize, 72)); }实操心得文字居中的计算(rect.Width - textSize.Width) / 2看起来简单但MeasureString返回的尺寸包含了一些额外的间距Overhang。对于要求极高的像素级对齐可以考虑使用TextRenderer.MeasureTextGDI或Graphics.MeasureCharacterRanges来获取更精确的边界框。但对于进度条显示上述方法在绝大多数情况下已经足够精确和高效。4. 进阶美化与功能增强一个基础的圆形进度条已经完成了。但要让它在实际项目中脱颖而出我们还需要一些“化妆”和“增肌”。4.1 实现颜色渐变效果单色的圆环略显平淡。我们可以使用LinearGradientBrush或PathGradientBrush来实现颜色渐变让进度条更具动感和质感。这里以沿着圆弧的渐变为例从起始颜色到结束颜色// 在绘制前景进度圆环的部分进行改造 if (this._value 0) { // 创建一个图形路径来表示我们要绘制的圆弧 using (GraphicsPath path new GraphicsPath()) { path.AddArc(rect, -90, 360f * (this._value / 100f)); // 使用PathGradientBrush它可以沿着路径产生渐变 using (PathGradientBrush pgb new PathGradientBrush(path)) { pgb.CenterColor Color.LightSkyBlue; // 中心颜色较亮 pgb.SurroundColors new Color[] { this._progressColor }; // 路径边缘颜色 // 将画笔的笔刷设置为我们的渐变笔刷 using (Pen progressPen new Pen(pgb, this.BarWidth)) { progressPen.Alignment PenAlignment.Center; progressPen.LineJoin LineJoin.Round; // 让线条连接处圆滑 g.DrawPath(progressPen, path); // 用DrawPath替代DrawArc } } } }技术细节PathGradientBrush非常强大它根据你提供的GraphicsPath来创建渐变。CenterColor是路径“内部”的颜色对于一条线这个概念有点抽象但效果是沿着路径法线方向产生渐变SurroundColors是路径边缘的颜色。通过调整这两个颜色你可以创造出从内到外、或沿着路径的复杂渐变效果。LineJoin属性设置为Round可以让进度条的首尾端当不是整圆时呈现圆角更加美观。4.2 添加动画平滑过渡直接跳跃式的进度变化会显得生硬。我们可以添加一个简单的动画让进度值的变化平滑过渡。这需要借助WinForms的Timer组件。首先在控件类中添加私有字段private int _targetValue 0; private System.Windows.Forms.Timer _animationTimer; private const int ANIMATION_INTERVAL 20; // 毫秒然后修改Value属性的setter并初始化定时器public int Value { get { return _value; } set { int newValue Math.Max(0, Math.Min(100, value)); if (newValue ! _targetValue) { _targetValue newValue; StartAnimation(); } } } private void StartAnimation() { if (_animationTimer null) { _animationTimer new System.Windows.Forms.Timer(); _animationTimer.Interval ANIMATION_INTERVAL; _animationTimer.Tick AnimationTimer_Tick; } if (!_animationTimer.Enabled) { _animationTimer.Start(); } } private void AnimationTimer_Tick(object sender, EventArgs e) { // 每次Tick当前值向目标值靠近一步 int step (_targetValue _value) ? 1 : -1; _value step; // 重绘控件 this.Invalidate(); // 到达目标值后停止定时器 if (_value _targetValue) { _animationTimer.Stop(); } }动画逻辑解析我们引入了_targetValue来存储最终要到达的进度值。当外部设置Value时并不直接改变当前显示值_value而是启动一个定时器。定时器每20毫秒触发一次让_value以步长1递增或递减趋近于_targetValue同时每次变化都调用Invalidate()重绘。这样就产生了平滑的动画效果。步长和定时器间隔可以调整来控制动画速度。避坑指南使用Timer时要特别注意控件的生命周期。在控件的Dispose方法中务必检查并停止、释放定时器否则可能导致内存泄漏和事件持续触发。protected override void Dispose(bool disposing) { if (disposing) { _animationTimer?.Stop(); _animationTimer?.Dispose(); } base.Dispose(disposing); }4.3 支持多种进度样式实心、饼图、虚线我们可以通过添加一个ProgressStyle枚举属性让控件支持更多样式。public enum ProgressStyle { Solid, // 实心圆环 Pie, // 饼图 Dashed // 虚线圆环 } private ProgressStyle _style ProgressStyle.Solid; public ProgressStyle Style { get { return _style; } set { _style value; this.Invalidate(); } }在OnPaint中根据不同的样式进行绘制switch (this._style) { case ProgressStyle.Solid: // 之前实现的圆环绘制代码 using (Pen progressPen new Pen(this._progressColor, this.BarWidth)) { ... } break; case ProgressStyle.Pie: // 绘制饼图使用Graphics.FillPie方法 using (Brush progressBrush new SolidBrush(this._progressColor)) { g.FillPie(progressBrush, rect, -90, sweepAngle); } // 可选绘制一个细边来区分 using (Pen borderPen new Pen(this.ForeColor, 1)) { g.DrawPie(borderPen, rect, -90, sweepAngle); } break; case ProgressStyle.Dashed: // 绘制虚线圆环 using (Pen progressPen new Pen(this._progressColor, this.BarWidth)) { progressPen.Alignment PenAlignment.Center; progressPen.DashStyle DashStyle.Dash; // 设置为虚线 // 可以调整DashPattern来自定义虚线样式例如 new float[] { 5, 3 } g.DrawArc(progressPen, rect, -90, sweepAngle); } break; }样式选择建议Solid实心最通用清晰直观。Pie饼图视觉占比大冲击力强适合表示整体中已完成的部分但精度感知不如圆环。Dashed虚线风格更轻量、现代常用于表示后台任务或不确定的进度。5. 性能优化与实战中的坑自定义绘制控件性能是关键。不当的绘制操作会导致界面卡顿尤其是在频繁更新进度时。5.1 双重缓冲与绘制优化WinForms控件默认在接收到WM_PAINT消息时进行绘制这可能导致闪烁。双重缓冲Double Buffering是解决这个问题的标准方案。它将所有绘制操作先在内存中的一块位图上完成然后一次性将整块位图输出到屏幕消除了中间过程的闪烁。为我们的控件启用双重缓冲非常简单public CircularProgressBar() { // 在构造函数中设置控件样式 this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, // 调整大小时重绘 true); this.UpdateStyles(); this.DoubleBuffered true; // 另一种方式与SetStyle效果类似 }AllPaintingInWmPaint控件将忽略WM_ERASEBKGND消息减少一次背景擦除由我们在OnPaint中完成所有绘制。UserPaint控件使用自定义绘制而不是系统绘制。OptimizedDoubleBuffer启用优化后的双重缓冲。ResizeRedraw控件大小改变时自动重绘避免调整大小时出现残影。5.2 高DPI与缩放支持在现代高分辨率屏幕上WinForms应用可能面临缩放问题。我们的自绘控件需要感知系统DPI缩放否则在高DPI下会显得模糊或太小。解决方案使用Graphics对象的DpiX和DpiY属性或者从Control的DeviceDpi属性获取缩放比例然后据此调整我们绘制的尺寸、线条宽度和字体大小。protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); Graphics g e.Graphics; // 计算DPI缩放因子 float dpiScale g.DpiX / 96.0f; // 96是标准96DPI下的值 // 将设计时尺寸如BarWidth10按比例缩放 int scaledBarWidth (int)(this.BarWidth * dpiScale); // 计算绘制矩形时边距也要缩放 int scaledMargin (int)(this.BarWidth * dpiScale); Rectangle rect new Rectangle( scaledMargin, scaledMargin, (int)(this.ClientSize.Width * dpiScale) - 2 * scaledMargin, (int)(this.ClientSize.Height * dpiScale) - 2 * scaledMargin ); // 创建画笔时使用缩放后的宽度 using (Pen backPen new Pen(this._backColor, scaledBarWidth)) { backPen.Alignment PenAlignment.Center; // ... 后续绘制所有尺寸相关参数都应考虑dpiScale } // 字体大小也需要缩放 float scaledFontSize GetOptimalFontSize(rect) * dpiScale; using (Font textFont new Font(this.Font.FontFamily, scaledFontSize, FontStyle.Bold)) { // ... 绘制文字 } }重要提示DPI感知是一个复杂的话题。从 .NET Framework 4.7 开始WinForms提供了更好的高DPI支持应用程序清单中设置dpiAwaretrue/dpiAware。上述方法是一种手动适配在混合DPI的多显示器环境下可能仍有挑战。对于新项目强烈建议以 .NET Core/.NET 5 的WinForms为目标其对高DPI和缩放的支持要完善得多。5.3 常见问题排查LoaderExceptions与设计时支持你可能会在工具箱中添加此控件或在设计界面使用它时遇到类似“无法加载一个或多个请求的类型”的错误并提示查看LoaderExceptions属性。这通常是因为项目目标框架不一致控件库和主项目使用了不同的.NET Framework版本。缺少依赖项控件引用了其他未正确部署的程序集。设计时代码错误控件的构造函数或某些属性getter/setter在设计时Visual Studio设计器加载时抛出了异常。如何排查首先确保解决方案中所有项目的目标框架一致。在控件的代码中使用DesignMode属性来区分设计时和运行时。避免在设计时执行依赖运行环境的操作如读取配置文件、连接数据库。public CircularProgressBar() { InitializeComponent(); // 如果有组件的话 if (!this.DesignMode) { // 只在运行时执行的初始化代码例如连接硬件、启动后台线程 _animationTimer new System.Windows.Forms.Timer(); // ... } }如果错误依然存在在Visual Studio的输出窗口选择“显示输出来源调试”中查看详细的异常堆栈信息LoaderExceptions集合中的内容会打印出来这是定位问题的关键。为了让控件在设计时就有良好的体验我们还可以为属性添加Category和Description特性并在属性窗口中提供更友好的分组和说明。使用DefaultValue特性可以让属性窗口的“重置”功能生效。[Category(Appearance)] [Description(圆形进度条的宽度像素)] [DefaultValue(10)] public int BarWidth { get; set; }6. 完整源码与使用示例经过以上所有步骤我们已经拥有了一个功能丰富、健壮的圆形进度条控件。以下是整合后的核心源码摘要省略了部分细节和异常处理以保持清晰using System; using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; namespace CustomControls { public enum ProgressStyle { Solid, Pie, Dashed } public partial class CircularProgressBar : Control { // ... 属性定义 (Value, BarWidth, ProgressColor, BackColor, ShowText, TextFormat, Style) // ... 字段定义 (_value, _targetValue, _animationTimer) public CircularProgressBar() { SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); DoubleBuffered true; Size new Size(100, 100); ForeColor Color.Black; Font new Font(Segoe UI, 12, FontStyle.Bold); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); Graphics g e.Graphics; g.SmoothingMode SmoothingMode.AntiAlias; g.TextRenderingHint TextRenderingHint.AntiAliasGridFit; // 计算DPI缩放和绘制矩形 float dpiScale g.DpiX / 96.0f; int scaledBarWidth (int)(BarWidth * dpiScale); int margin scaledBarWidth; Rectangle rect new Rectangle(margin, margin, (int)(ClientSize.Width * dpiScale) - 2 * margin, (int)(ClientSize.Height * dpiScale) - 2 * margin); if (rect.Width 0 || rect.Height 0) return; // 1. 绘制背景 using (Pen backPen new Pen(BackColor, scaledBarWidth)) { backPen.Alignment PenAlignment.Center; g.DrawArc(backPen, rect, 0, 360); } // 2. 绘制前景进度 if (Value 0) { float sweepAngle 360f * (Value / 100f); // 处理100%时可能出现的缝隙 if (Math.Abs(sweepAngle - 360) 0.1f) { using (Pen progressPen new Pen(ProgressColor, scaledBarWidth)) { progressPen.Alignment PenAlignment.Center; g.DrawEllipse(progressPen, rect); } } else { switch (Style) { case ProgressStyle.Solid: DrawSolidProgress(g, rect, sweepAngle, scaledBarWidth); break; case ProgressStyle.Pie: DrawPieProgress(g, rect, sweepAngle); break; case ProgressStyle.Dashed: DrawDashedProgress(g, rect, sweepAngle, scaledBarWidth); break; } } } // 3. 绘制文字 if (ShowText !string.IsNullOrEmpty(TextFormat)) { string text string.Format(TextFormat, Value); float fontSize Math.Max(6, Math.Min(rect.Height * 0.2f, 72)) * dpiScale; using (Font textFont new Font(Font.FontFamily, fontSize, FontStyle.Bold)) using (Brush textBrush new SolidBrush(ForeColor)) { SizeF textSize g.MeasureString(text, textFont); g.DrawString(text, textFont, textBrush, rect.Left (rect.Width - textSize.Width) / 2, rect.Top (rect.Height - textSize.Height) / 2); } } } private void DrawSolidProgress(Graphics g, Rectangle rect, float sweepAngle, int width) { using (Pen pen new Pen(ProgressColor, width)) { pen.Alignment PenAlignment.Center; pen.LineJoin LineJoin.Round; g.DrawArc(pen, rect, -90, sweepAngle); } } private void DrawPieProgress(Graphics g, Rectangle rect, float sweepAngle) { using (Brush brush new SolidBrush(ProgressColor)) { g.FillPie(brush, rect, -90, sweepAngle); } } private void DrawDashedProgress(Graphics g, Rectangle rect, float sweepAngle, int width) { using (Pen pen new Pen(ProgressColor, width)) { pen.Alignment PenAlignment.Center; pen.DashStyle DashStyle.Dash; g.DrawArc(pen, rect, -90, sweepAngle); } } // ... 动画相关代码 (StartAnimation, AnimationTimer_Tick) // ... Dispose 方法 } }在项目中使用编译包含上述CircularProgressBar类的项目生成一个DLL。在你的主WinForms项目中右键“工具箱”选择“选择项...”浏览并添加这个DLL。CircularProgressBar控件就会出现在工具箱中。将其拖拽到窗体上就像使用标准控件一样。在代码中你可以通过设置其属性来控制它// 设置进度为75% circularProgressBar1.Value 75; // 改变样式为饼图 circularProgressBar1.Style ProgressStyle.Pie; // 更改颜色 circularProgressBar1.ProgressColor Color.OrangeRed; // 隐藏文字 circularProgressBar1.ShowText false;为了让进度条动起来你可以结合后台任务如BackgroundWorker或Task来更新Value属性。如果启用了动画进度变化将会是平滑的。private async void StartLongRunningTaskButton_Click(object sender, EventArgs e) { for (int i 0; i 100; i) { circularProgressBar1.Value i; // 启用动画后这里会平滑过渡 await Task.Delay(50); // 模拟工作耗时 } }这个圆形进度条控件从最基础的绘制开始逐步增加了渐变、动画、多样式、DPI支持等高级特性并考虑了设计时支持和性能优化。你可以根据自己项目的具体需求在此基础上继续扩展例如添加“不确定进度”的旋转动画、更复杂的多层渐变、或者与具体业务数据如CPU使用率、下载速度绑定。希望这个从零到一的构建过程不仅能给你一个可直接使用的控件更能让你理解WinForms自定义绘制的精髓和解决实际问题的思路。本文还有配套的精品资源点击获取