C# WinForms自定义圆形进度条控件开发:从GDI+绘制到性能优化
发布时间:2026/9/3 23:18:33 作者:尧图编辑部 阅读量:1,286

简介这是一份面向C# WinForm开发者的自定义圆形进度条控件源码资源适用于需要美化UI交互、替代系统默认进度条的中初级开发者。资源基于.NET Framework 4.7.2构建兼容VS2019开发环境配套控件DLL为.NET Framework 4.0版本可直接集成至现有WinForm项目解决原生ProgressBar缺乏圆角/环形样式的问题。压缩包共20个文件含6个核心CS逻辑文件含主控件类与窗体调用示例、3个配置文件App.config等、2个动态链接库含HZH_Controls第三方UI组件、2个本地化资源文件.resx以及解决方案.sln、项目.csproj、调试符号.pdb等完整工程结构文件总大小16.81MB。已有728人学习下载提供开箱即用的可视化设计支持——包含设计器代码Designer.cs、资源管理器集成Resources.resx、用户设置持久化Settings.settings及可执行演示程序.exe便于快速理解控件注册、属性绑定与事件响应全流程。1. 项目概述为什么我们需要一个圆形进度条在桌面应用开发里进度条是个再常见不过的控件了。系统自带的ProgressBar控件那个横着的长条虽然功能稳定但看久了难免觉得单调尤其是在一些对界面美观度有要求的应用场景里比如音乐播放器、系统监控仪表盘、安装程序或者游戏加载界面。一个设计精美的圆形进度条往往能瞬间提升整个应用的视觉档次和用户体验。我最近在重构一个设备数据采集的上位机软件时就遇到了这个需求。主界面上需要实时显示多个通道的采集进度和状态如果还用传统的长条进度条不仅占地儿而且信息密度低不够直观。这时候一个能够同时展示进度百分比、并可灵活定制颜色和样式的圆形进度条就成了刚需。然而翻遍 WinForms 的标准控件箱你会发现并没有现成的“圆形进度条”控件。这就是我们这次要动手解决的问题从零开始用 C# 和 GDI 绘制一个高性能、可定制化的圆形进度条控件并附上完整的、可运行的源码。这个自制控件不仅解决了界面美化的问题更深层次的价值在于它让你彻底掌握在 WinForms 中如何进行自定义绘制Custom Drawing理解控件的绘制周期、双缓冲技术以及如何设计一个属性完备、事件丰富的可复用组件。这对于希望深入 WinForms 开发或者需要开发独特界面元素的开发者来说是一次非常好的练手机会。2. 核心思路与设计决策2.1 技术选型为什么是 GDI 而不是第三方库实现自定义绘制主要有几条路使用原生的 GDI、借助第三方图形库如 SkiaSharp或者使用较新的 WinForms 自带的ControlPaint类进行简单绘制。对于圆形进度条这个需求我的选择是 GDI。首先轻量与零依赖。GDI (System.Drawing) 是 .NET Framework/.NET Core 及更高版本中内置的无需引入任何额外的 NuGet 包。我们的控件编译后就是一个单纯的 DLL 或直接嵌入项目部署和分发没有任何额外成本。反观 SkiaSharp 虽然强大但会显著增加发布包的体积。其次复杂度适中完全可控。圆形进度条的绘制逻辑并不复杂核心就是画圆环和填充扇形。GDI 的Graphics类提供了DrawArc画弧线和FillPie填充扇形方法正好契合我们的需求。自己手写绘制代码意味着你可以精确控制每一个像素的表现比如渐变色、阴影效果、纹理填充等后期定制和优化的空间巨大。最后性能足够。在 WinForms 桌面应用场景下只要合理使用双缓冲技术后面会详细讲GDI 绘制一个甚至几十个这样的进度条其性能开销都是微乎其微的完全不会造成界面卡顿。2.2 控件设计属性、方法与事件规划在设计这个圆形进度条控件时我把它设想成一个完整的、可投入生产环境的组件。这意味着它需要有清晰的属性来配置外观有方法来控制行为以及有事件来反馈状态。核心属性规划Value(当前值)最核心的属性范围通常在Minimum和Maximum之间。它的变化直接驱动进度条的重新绘制。Minimum/Maximum(最小值/最大值)定义进度范围默认设为 0 和 100符合日常百分比习惯。ProgressColor(进度颜色)已加载部分圆弧的颜色。BackColor(背景色)未加载部分圆弧的背景色。注意这里我选择重写Override控件基类的BackColor属性使其直接用于绘制背景圆环这样更符合直觉。CircleThickness(圆环厚度)控制进度条圆环的粗细单位是像素。TextDisplay(文本显示模式)这是一个枚举属性例如None不显示、Percentage显示百分比、Value显示当前值。它决定了圆环中心显示什么文字。Font/TextColor(字体/文字颜色)控制中心文字的样式。核心方法Increment(int)一个便捷方法用于将当前值增加指定数值并自动触发重绘。这在模拟步进式进度时非常有用。核心事件ValueChanged当Value属性发生变化时触发。外部程序可以监听此事件来执行其他逻辑。有了这个设计蓝图我们就可以开始动手编码了。3. 从零开始创建自定义控件类3.1 创建项目与自定义控件首先打开 Visual Studio以 VS 2022 为例创建一个新的“Windows 窗体应用(.NET Framework)”或“Windows 窗体应用”项目对应 .NET 6/8。我建议使用 .NET Framework 4.7.2 或更高版本或者 .NET 6/8以确保更好的兼容性和性能。在解决方案资源管理器中右键点击项目 - “添加” - “类”。将类名命名为CircularProgressBar.cs。在弹出的对话框中选择“自定义控件”。VS 会自动生成一个继承自Control类的文件并附带一个简单的OnPaint方法框架。注意你也可以通过“添加” - “用户控件”来创建但“自定义控件”是更纯粹的选择它直接继承Control没有默认的容器背景更适合我们这种完全自绘的控件。3.2 定义属性和字段在类文件中我们首先定义私有字段和对应的公共属性。这里以几个关键属性为例using System; using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; namespace YourNamespace.Controls { public partial class CircularProgressBar : Control { // 私有字段 private int _value 0; private int _minimum 0; private int _maximum 100; private Color _progressColor Color.DodgerBlue; private int _circleThickness 10; private DisplayText _textDisplay DisplayText.Percentage; private Color _textColor Color.Black; // 公共属性 [Category(外观), Description(获取或设置进度条的当前值。)] public int Value { get { return _value; } set { if (value ! _value) { // 确保值在范围内 int newValue Math.Max(_minimum, Math.Min(_maximum, value)); _value newValue; // 触发重绘和事件 this.Invalidate(); OnValueChanged(EventArgs.Empty); } } } [Category(外观), Description(获取或设置进度条的最小值。)] public int Minimum { get _minimum; set { _minimum value; if (_value _value) _value _minimum; this.Invalidate(); } } [Category(外观), Description(获取或设置进度条的最大值。)] public int Maximum { get _maximum; set { _maximum value; if (_value _maximum) _value _maximum; this.Invalidate(); } } [Category(外观), Description(获取或设置进度圆环的颜色。)] public Color ProgressColor { get _progressColor; set { _progressColor value; this.Invalidate(); } } [Category(外观), Description(获取或设置圆环的厚度像素。)] public int CircleThickness { get _circleThickness; set { _circleThickness Math.Max(1, value); this.Invalidate(); } // 厚度至少为1 } // 文本显示模式枚举 public enum DisplayText { None, Percentage, Value } [Category(外观), Description(获取或设置中心显示的文本类型。)] public DisplayText TextDisplay { get _textDisplay; set { _textDisplay value; this.Invalidate(); } } [Category(外观), Description(获取或设置中心文本的颜色。)] public Color TextColor { get _textColor; set { _textColor value; this.Invalidate(); } } // 重写基类的BackColor使其直接用于背景圆环 [Category(外观), Description(获取或设置背景圆环的颜色。)] public override Color BackColor { get base.BackColor; set { base.BackColor value; this.Invalidate(); } } // 事件 [Category(行为), Description(当Value属性更改时发生。)] public event EventHandler ValueChanged; protected virtual void OnValueChanged(EventArgs e) { ValueChanged?.Invoke(this, e); } // 构造函数 public CircularProgressBar() { InitializeComponent(); // 由设计器生成用于支持设计时 SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.ResizeRedraw, true); this.BackColor Color.LightGray; // 默认背景色 this.ForeColor Color.Black; // 默认前景色用于文本 this.Size new Size(150, 150); // 默认大小 } // 便捷方法 public void Increment(int value) { this.Value value; } } }关键点解析Category和Description特性这两个特性非常重要。它们决定了属性在 Visual Studio 的属性窗口中如何被分组和显示描述。Category(“外观”)会让属性出现在“外观”分组下极大提升了控件在设计时的可用性。属性设置器中的Invalidate()这是自定义绘制的核心。每次改变影响外观的属性如颜色、值、厚度时都必须调用this.Invalidate()方法。这个方法会通知 Windows 窗体系统“我这个控件的某个区域失效了需要重新绘制”。系统随后会在合适的时机调用我们的OnPaint方法。SetStyle方法在构造函数中调用的SetStyle是解决闪烁问题的关键。我们设置了四个标志OptimizedDoubleBuffer启用双缓冲。绘制操作先在内存中的位图上完成然后一次性输出到屏幕有效消除闪烁。AllPaintingInWmPaint禁止控件在非 Paint 消息中自行绘制背景将所有绘制工作统一到OnPaint中。UserPaint指示控件将自行绘制其整个表面而不是由系统绘制。ResizeRedraw当控件大小改变时自动重绘整个控件。值范围校验在Value属性的setter中使用Math.Max和Math.Min确保新值被钳制在Minimum和Maximum之间。这是一个健壮性设计防止外部设置非法值导致绘制错误。4. 核心绘制逻辑详解控件的灵魂在于OnPaint方法。我们需要在这个方法里使用Graphics对象像画家一样一步步画出我们的圆形进度条。4.1 绘制背景圆环首先我们需要计算绘制区域。为了留出边框和美观我们通常会在控件客户区ClientRectangle内部定义一个稍小的矩形作为绘图区域。protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); // 调用基类方法确保背景等基础绘制完成 Graphics g e.Graphics; g.SmoothingMode SmoothingMode.AntiAlias; // 开启抗锯齿让图形边缘更平滑 // 计算绘图区域留出边距 int margin 2; Rectangle drawArea new Rectangle(margin, margin, this.ClientSize.Width - 2 * margin, this.ClientSize.Height - 2 * margin); // 1. 绘制背景圆环 using (Pen backPen new Pen(this.BackColor, _circleThickness)) { // 绘制一个完整的360度圆弧 g.DrawArc(backPen, drawArea, 0, 360); } // ... 后续绘制进度和文本 }DrawArc方法接受一个矩形定义了椭圆的外接矩形、起始角度和扫描角度。这里我们画了一个完整的圆环作为背景。using语句确保Pen对象在使用后被正确释放这是管理 GDI 资源实现了IDisposable接口的最佳实践能有效避免内存泄漏。4.2 绘制进度扇形关键算法进度部分是整个控件的核心。我们需要根据Value、Minimum、Maximum计算出一个扇形的扫描角度。// 2. 绘制进度扇形 if (_maximum _minimum _value _minimum) { // 计算进度百分比 (0.0 ~ 1.0) float progressRatio (float)(_value - _minimum) / (_maximum - _minimum); // 将百分比转换为角度GDI中0度在3点钟方向顺时针为正 float sweepAngle progressRatio * 360; // 使用更粗的笔或填充扇形来绘制进度 using (Pen progressPen new Pen(_progressColor, _circleThickness)) { // 从-90度开始画这样0%时进度条在顶部12点钟方向 float startAngle -90; g.DrawArc(progressPen, drawArea, startAngle, sweepAngle); } // 另一种更常见的实现使用填充扇形效果更饱满 // using (Brush progressBrush new SolidBrush(_progressColor)) // { // g.FillPie(progressBrush, drawArea, startAngle, sweepAngle); // } // 注意FillPie填充的是整个扇形区域需要调整drawArea和计算来仅填充圆环部分逻辑稍复杂。 }角度计算详解progressRatio这是核心的比例值。(value - minimum) / (maximum - minimum)将当前值映射到 0 到 1 的区间。sweepAngle扫描角度。progressRatio * 360将比例转换为角度。当value等于maximum时sweepAngle为 360 度即一个完整的圆。startAngle起始角度。GDI 的坐标系中0 度指向正东3点钟方向角度增加方向为顺时针。为了让进度条从顶部12点钟方向开始我们设置起始角度为 -90 度。实操心得这里我选择了DrawArc来画进度弧线因为它和背景圆环的绘制方式一致逻辑简单。如果你想实现那种“实心填充”的圆环进度像很多电池图标就需要使用FillPie并结合复杂的区域计算来“挖空”中心部分或者使用GraphicsPath构建一个圆环路径再进行填充。对于初次实现DrawArc方案更清晰易懂。4.3 绘制中心文本最后根据TextDisplay属性的设置在圆环中心绘制文本。// 3. 绘制中心文本 if (_textDisplay ! DisplayText.None) { string displayString ; switch (_textDisplay) { case DisplayText.Percentage: float percentage (_maximum _minimum) ? ((float)(_value - _minimum) / (_maximum - _minimum)) * 100 : 0; displayString ${percentage:F0}%; // F0表示保留0位小数 break; case DisplayText.Value: displayString _value.ToString(); break; } if (!string.IsNullOrEmpty(displayString)) { using (Font textFont new Font(this.Font.FontFamily, this.Font.Size * 1.5f, FontStyle.Bold)) // 放大字体 using (Brush textBrush new SolidBrush(_textColor)) { // 测量文本大小以便居中显示 SizeF textSize g.MeasureString(displayString, textFont); PointF textLocation new PointF( (this.Width - textSize.Width) / 2, (this.Height - textSize.Height) / 2 ); g.DrawString(displayString, textFont, textBrush, textLocation); } } }文本居中技巧g.MeasureString这个方法用于计算指定字体下绘制给定字符串所需的空间大小SizeF。计算位置文本的左上角坐标 (控件宽度 - 文本宽度) / 2和(控件高度 - 文本高度) / 2。这样就实现了完美的水平垂直居中。至此一个基础但功能完整的圆形进度条控件的绘制逻辑就完成了。将控件编译后你就可以在工具箱中找到它像拖拽标准按钮一样拖到窗体上使用了。5. 高级美化与功能扩展基础版本已经可用但要让控件更出彩我们还可以添加一些高级特性。5.1 添加渐变色进度单色进度条看久了会腻渐变色能带来更强的视觉冲击。我们可以使用LinearGradientBrush或PathGradientBrush。// 在绘制进度部分替换原有的Pen using (GraphicsPath path new GraphicsPath()) { // 添加一个弧线路径 path.AddArc(drawArea, startAngle, sweepAngle); // 使用路径渐变画笔 using (PathGradientBrush pgb new PathGradientBrush(path)) { pgb.CenterColor Color.LightSkyBlue; // 中心颜色 Color[] surroundColors { _progressColor }; // 边缘颜色 pgb.SurroundColors surroundColors; using (Pen gradientPen new Pen(pgb, _circleThickness)) { g.DrawArc(gradientPen, drawArea, startAngle, sweepAngle); } } }注意PathGradientBrush需要GraphicsPath。这里我们根据进度弧线创建了一个路径然后基于这个路径创建渐变画笔使得颜色从弧线的一端渐变到另一端。你也可以使用LinearGradientBrush并指定一个矩形区域和角度来实现线性渐变。5.2 添加“光晕”或阴影效果为进度条添加一点内阴影或外发光能增加立体感。这可以通过绘制多个逐渐变淡、变粗或变细的同心圆弧来实现。// 在绘制主进度条之前先绘制一个“光晕”效果 int glowWidth 3; for (int i glowWidth; i 0; i--) { int alpha 80 / (i 1); // 透明度递减 using (Pen glowPen new Pen(Color.FromArgb(alpha, _progressColor), _circleThickness i * 2)) { // 调整绘制区域让光晕向外扩散 Rectangle glowRect Rectangle.Inflate(drawArea, i, i); g.DrawArc(glowPen, glowRect, startAngle, sweepAngle); } } // 然后再绘制原本的主进度条...这段代码在绘制主进度条前先画了3层半透明的、更粗的圆弧颜色和进度色相同但透明度逐层降低从而模拟出一种柔和的发光效果。Rectangle.Inflate方法用于将矩形向外扩大。5.3 实现动画效果让进度条平滑过渡而不是突兀地跳变体验会好很多。这需要用到Timer组件。在控件类中添加一个System.Windows.Forms.Timer私有成员。添加属性如int AnimationStep动画步长和bool IsAnimating。在Value属性的setter中不要直接设置_value并重绘而是设置一个目标值_targetValue然后启动 Timer。在 Timer 的Tick事件里让_value以AnimationStep为单位逐步逼近_targetValue每次步进都调用Invalidate()。当_value等于_targetValue时停止 Timer。private Timer _animationTimer; private int _targetValue; public CircularProgressBar() { // ... 其他初始化代码 _animationTimer new Timer(); _animationTimer.Interval 20; // 约50帧/秒 _animationTimer.Tick AnimationTimer_Tick; } private void AnimationTimer_Tick(object sender, EventArgs e) { if (_value _targetValue) { _value Math.Min(_value AnimationStep, _targetValue); } else if (_value _targetValue) { _value Math.Max(_value - AnimationStep, _targetValue); } else { _animationTimer.Stop(); } this.Invalidate(); OnValueChanged(EventArgs.Empty); } // 修改Value属性的setter public int Value { get { return _value; } set { int newTarget Math.Max(_minimum, Math.Min(_maximum, value)); if (newTarget ! _targetValue) { _targetValue newTarget; _animationTimer.Start(); } } }这样当你改变Value属性时进度条就会平滑地动画到目标位置。6. 实战应用与性能调优6.1 在项目中使用控件控件编译后在 Visual Studio 的设计视图下右键点击工具箱 - “选择项” - 浏览并选中你项目生成的 DLL 或直接选择项目。CircularProgressBar控件就会出现在工具箱中。将其拖到窗体上你就可以在属性窗口中尽情调整它的外观并通过代码控制它的Value。一个典型的使用场景是配合后台任务更新进度private async void buttonStart_Click(object sender, EventArgs e) { buttonStart.Enabled false; circularProgressBar1.Value 0; await Task.Run(() { for (int i 0; i 100; i) { // 模拟耗时工作 Thread.Sleep(50); // 注意必须在UI线程上更新控件属性 this.Invoke(new Action(() { circularProgressBar1.Value i; })); } }); buttonStart.Enabled true; }关键点在后台线程中更新 UI 控件属性必须通过Control.Invoke或BeginInvoke方法将调用封送到创建控件的线程通常是 UI 主线程上执行否则会引发跨线程访问异常。6.2 性能优化要点双缓冲是基础如前所述在构造函数中设置ControlStyles.OptimizedDoubleBuffer等样式是消除闪烁的第一要务。减少不必要的绘制在OnPaint中只绘制需要的内容。如果控件很大但只有一小部分区域失效可以考虑使用e.ClipRectangle传入的PaintEventArgs参数中的裁剪区域进行局部绘制优化只重绘脏区域。但对于我们这个尺寸固定且不大的控件全量重绘开销很小。及时释放 GDI 资源所有Pen,Brush,Font,GraphicsPath等实现了IDisposable的对象都必须使用using语句或在Dispose方法中确保被释放。内存泄漏往往源于此。避免在绘制方法中创建复杂对象例如如果渐变色是固定的可以考虑将LinearGradientBrush作为成员变量缓存起来在OnPaint中直接使用而不是每次绘制都新建。但要注意如果控件大小改变缓存的画笔可能需要重新创建。6.3 常见问题与排查问题1控件在设计时拖到窗体上不显示或者显示一个红叉。排查检查自定义控件的构造函数是否抛出了异常。确保所有在构造函数中初始化的资源都是可用的。查看“错误列表”窗口中的编译或设计时错误信息。解决最常见的原因是InitializeComponent()调用前或后代码有问题或者引用了不存在的资源。简化构造函数确保基础功能先能运行。问题2进度条更新时严重闪烁。排查确认是否设置了SetStyle中的双缓冲标志。检查是否在OnPaint外部比如在Value属性改变时有额外的、不受控的绘制操作。解决确保SetStyle已正确调用。确保所有导致外观变化的属性其setter中都只调用Invalidate()而不是直接进行绘制操作。问题3文本显示位置不居中或模糊。排查检查文本居中的计算逻辑。确认MeasureString使用的字体和DrawString使用的字体是否一致。检查SmoothingMode是否设置为AntiAlias或HighQuality。解决使用StringFormat对象可以更精确地控制文本对齐。StringFormat.Alignment和StringFormat.LineAlignment设置为StringAlignment.Center然后让DrawString绘制在整个矩形区域内可以实现更稳健的居中。问题4改变控件大小时圆环变形或绘制不全。排查检查OnPaint方法中的drawArea计算是否依赖于ClientSize。确认ControlStyles.ResizeRedraw标志已设置。解决确保绘图区域的计算能正确响应控件大小变化。在OnResize事件中也可以调用Invalidate()来强制重绘。问题5在高DPI屏幕上显示模糊。排查这是一个经典的 WinForms 高DPI支持问题。WinForms 默认不是完全 DPI 感知的。解决在应用程序入口点Program.cs的Main方法添加Application.SetHighDpiMode(HighDpiMode.SystemAware);.NET Core/.NET 5。对于控件自身在OnPaint中可以使用e.Graphics.DpiX和e.Graphics.DpiY来获取系统DPI缩放比例并据此调整CircleThickness、字体大小等绘制参数实现自适应缩放。这是一个更进阶的话题但对于需要适配不同显示器的应用至关重要。通过以上步骤你不仅得到了一个可用的圆形进度条控件源码更深入理解了 WinForms 自定义绘制的完整流程和最佳实践。你可以根据项目需求随意扩展这个控件的功能比如添加不同风格仪表盘式、波浪式、绑定数据源等使其成为你项目中的一把利器。本文还有配套的精品资源点击获取