简介:这是一份面向C# WinForm开发者的圆形进度条自定义控件源码资源,适用于需要美化界面交互、替代系统默认进度条的中初级开发者。资源基于.NET Framework 4.7.2构建,兼容VS2019开发环境,配套HZH_Controls第三方UI控件库(.NET 4.0),可直接编译运行或集成至现有项目。压缩包共20个文件,含6个核心C#源码(含Form1主窗体与设计器逻辑)、3个配置文件(App.config等)、2个关键DLL(含自定义控件库)、2个本地化资源文件(.resx),以及解决方案.sln、项目.csproj、调试符号.pdb和NuGet包.nupkg等完整工程要素,总大小16.81MB,结构规范,便于理解WinForm自绘控件的实现原理与资源组织方式。已有728人学习下载,读者可获得可运行的完整示例工程、圆形进度条绘制逻辑(GDI+路径与渐变填充)、属性绑定机制、以及适配不同尺寸与颜色主题的扩展思路。
1. 项目概述:为什么我们需要一个圆形进度条?
在C# WinForm桌面应用开发中,进度条(ProgressBar)是一个再常见不过的控件。系统自带的ProgressBar控件功能稳定,但样式单一,永远是那个横着的、蓝白相间的长条。当你的应用需要一点设计感,或者界面空间有限(比如在一个圆形的Logo图标旁显示加载进度),又或者你想打造一套独特的UI风格时,这个默认控件就显得力不从心了。这时,一个自定义的、可灵活控制外观的圆形进度条就成了刚需。
我最近在重构一个设备监控的上位机软件,仪表盘界面需要展示多个传感器的实时数据加载进度。用传统的长条进度条会破坏仪表盘的整体美感,而使用圆形进度条,既能清晰指示进度,又能完美融入环形仪表的设计中。于是,我动手实现了一个高性能、易定制的C# WinForm圆形进度条控件,并把核心思路和源码分享出来。这个控件不仅解决了样式问题,更重要的是,它展示了如何在WinForm中通过GDI+进行自定义绘制,这是深入理解Windows桌面开发图形层的关键一步。
2. 核心设计与思路拆解:从需求到方案
2.1 需求分析与技术选型
实现一个圆形进度条,我们首先要明确核心需求:
- 图形展示:能够以圆环或扇形的形式,动态显示一个从0%到100%的进度。
- 高度可定制:颜色(背景色、前景色、文字色)、宽度、半径、起始角度等应可自由调整。
- 性能与流畅度:进度更新时应平滑,不能有闪烁或卡顿。
- 易用性:应该像使用标准控件一样,可以通过属性设置,并响应值的变化事件。
基于这些需求,技术路径很清晰:继承UserControl,在OnPaint方法中使用GDI+进行自定义绘制。为什么不直接用图片序列?因为图片缺乏灵活性,调整颜色、大小都需要重新制作资源,而GDI+绘制是矢量的,可以实时计算,灵活性极高。
为什么选择GDI+而不是更现代的WPF?对于许多现有的、庞大的WinForm项目,尤其是工业上位机、数据采集监控等场景,迁移到WPF成本高昂。在WinForm体系内用GDI+实现自定义控件,是兼顾效果、性能和开发效率的最优解。System.Drawing命名空间下的Graphics、Pen、Brush、SolidBrush等类是我们的核心工具。
2.2 控件属性与事件设计
一个良好的控件,其可配置性通过属性暴露。我们为圆形进度条设计以下关键属性:
| 属性名 | 类型 | 说明 | 默认值 |
|---|---|---|---|
Value | int | 当前进度值,范围通常为0-100。 | 0 |
Maximum | int | 最大值,用于计算百分比。 | 100 |
Minimum | int | 最小值。 | 0 |
CircleColor | Color | 进度条圆环的背景色(未完成部分)。 | LightGray |
ProgressColor | Color | 进度条圆环的前景色(已完成部分)。 | DodgerBlue |
CircleWidth | int | 圆环的宽度(像素)。 | 10 |
TextColor | Color | 中心显示文字的顏色。 | Black |
ShowText | bool | 是否在中心显示进度百分比文字。 | true |
TextFormat | string | 显示文字的格式,如“{0}%”。 | “{0}%” |
StartAngle | float | 进度条绘制的起始角度(以度为单位,12点钟方向为-90度)。 | -90 |
此外,我们还需要一个关键事件:ValueChanged。当Value属性被修改时,应触发此事件,并强制控件重绘(Invalidate())以更新界面。
注意:
StartAngle设置为-90度是一个常见约定,这样0%进度时,进度弧线从正上方(12点方向)开始,顺时针增长,这符合大多数用户的认知习惯。GDI+中角度以度为单位,0度指向X轴正方向(3点钟方向),且角度增加方向为顺时针。这一点与数学上的极坐标系不同,务必牢记。
3. 核心细节解析与实操要点
3.1 理解GDI+的坐标系与绘图流程
在WinForm中绘图,核心是处理Paint事件或重写OnPaint方法。系统会传递一个PaintEventArgs参数,其中的Graphics对象就是我们的画布。所有绘制操作都通过调用这个Graphics对象的方法来完成。
关键点在于坐标系。控件客户区的左上角是(0,0),X轴向右为正,Y轴向下为正。这与我们通常的数学坐标系Y轴向上不同。在绘制圆形时,我们通常需要计算一个外接矩形(Rectangle或RectangleF)来定义圆的位置和大小。
例如,要在一个宽高均为clientSize的控件中心画一个半径为radius的圆,其外接矩形的计算方式是:
float diameter = radius * 2; float x = (clientSize.Width - diameter) / 2; float y = (clientSize.Height - diameter) / 2; RectangleF rect = new RectangleF(x, y, diameter, diameter);这个rect将用于绘制椭圆(当宽高相等时即为圆)和圆弧。
3.2 绘制圆弧的关键:DrawArc与SweepAngle
绘制圆环进度的核心方法是Graphics.DrawArc。它需要一支Pen(定义颜色和宽度)、一个外接矩形、一个起始角度和一个扫描角度。
StartAngle: 我们属性中定义的起始角度。SweepAngle: 这是需要计算的关键值。它表示从起始角度开始,顺时针扫过的角度。进度为0时,SweepAngle为0;进度为100%时,SweepAngle为360度。
计算公式为:
float percentage = (float)(Value - Minimum) / (Maximum - Minimum); float sweepAngle = 360f * percentage;然后,我们绘制两个圆弧:
- 背景圆环:使用
CircleColor的Pen,绘制一个完整的360度圆弧。 - 进度圆环:使用
ProgressColor的Pen,使用相同的rect和StartAngle,但SweepAngle为计算出的sweepAngle。
实操心得:直接使用
DrawArc绘制圆环,Pen的宽度(Width)会有一半在rect的边界内,一半在边界外。如果你希望圆环完全包含在rect内,需要将rect向内收缩Pen.Width/2。但在进度条场景下,为了计算简单和视觉居中,我们通常直接使用rect,并接受圆环略微超出一点的事实,影响不大。
3.3 解决控件闪烁问题
自定义控件绘图最常见的问题就是闪烁。当控件频繁重绘(比如用Timer快速更新进度)时,你会看到明显的闪烁。这是因为WinForm在重绘时,默认会先擦除背景(产生一个白色闪屏),再绘制新内容。
解决方案是双缓冲。我们可以在控件构造函数中设置以下样式:
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);AllPaintingInWmPaint: 禁止控件在绘制前自动擦除背景。UserPaint: 控件自行绘制,而不是系统绘制。OptimizedDoubleBuffer: 启用双缓冲。所有绘制操作先在内存中的位图上完成,然后一次性绘制到屏幕,极大减少闪烁。ResizeRedraw: 控件大小改变时自动重绘。
设置这些样式是WinForm自定义控件平滑绘制的标准操作,务必记住。
4. 实操过程与核心环节实现
下面,我将分步拆解这个圆形进度条控件的完整实现代码。你可以创建一个新的“Windows 窗体控件库”项目,添加一个用户控件,然后将以下代码替换进去。
4.1 定义属性和后备字段
首先,我们为之前设计的属性创建私有字段和公共属性。在属性的set访问器中,要完成值校验、字段赋值、触发事件和请求重绘的工作。
using System; using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; namespace CircularProgressBar { public partial class CircularProgressBar : UserControl { // 私有字段 private int _value = 0; private int _maximum = 100; private int _minimum = 0; private Color _circleColor = Color.LightGray; private Color _progressColor = Color.DodgerBlue; private int _circleWidth = 10; private Color _textColor = Color.Black; private bool _showText = true; private string _textFormat = "{0}%"; private float _startAngle = -90f; // 从12点方向开始 // 公共属性 [Category("Appearance"), Description("当前进度值")] public int Value { get { return _value; } set { // 值范围限制 if (value < _minimum) value = _minimum; if (value > _maximum) value = _maximum; if (_value != value) { _value = value; OnValueChanged(EventArgs.Empty); this.Invalidate(); // 请求重绘 } } } [Category("Behavior"), Description("进度最大值")] public int Maximum { get { return _maximum; } set { if (value <= _minimum) throw new ArgumentException("Maximum must be greater than Minimum."); if (_maximum != value) { _maximum = value; if (_value > _maximum) _value = _maximum; this.Invalidate(); } } } // ... 其他属性(Minimum, CircleColor, ProgressColor等)的定义方式类似, // 在setter中赋值并调用this.Invalidate() // 为节省篇幅,此处省略,文末会提供完整源码链接思路。 [Category("Appearance"), Description("进度条起始角度(度)")] public float StartAngle { get { return _startAngle; } set { _startAngle = value; this.Invalidate(); } } // 定义ValueChanged事件 public event EventHandler ValueChanged; protected virtual void OnValueChanged(EventArgs e) { ValueChanged?.Invoke(this, e); } } }注意:为每个属性添加
[Category]和[Description]特性非常重要。这样,当你在Visual Studio的设计器中将此控件拖到窗体上时,这些属性会出现在属性窗口中对应的分类下,并且有描述提示,极大提升了控件的易用性和专业性。
4.2 构造函数与样式设置
在控件的构造函数中,我们需要初始化组件并设置双缓冲等样式。如果通过“添加用户控件”创建,会有一个InitializeComponent()调用,保留它。
public CircularProgressBar() { InitializeComponent(); // 关键:设置控件样式以防止闪烁 SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); // 设置默认大小 this.Size = new Size(150, 150); }将默认大小设为150x150,这是一个比较合适的圆形进度条尺寸。
4.3 重写OnPaint方法进行绘制
这是整个控件的灵魂所在。我们重写OnPaint方法,在这里完成所有绘制逻辑。
protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); // 调用基类方法,虽然它可能什么都不做,但这是一个好习惯 Graphics graphics = e.Graphics; graphics.SmoothingMode = SmoothingMode.AntiAlias; // 开启抗锯齿,让边缘平滑 // 1. 计算绘制区域 int clientWidth = this.ClientSize.Width; int clientHeight = this.ClientSize.Height; int diameter = Math.Min(clientWidth, clientHeight) - (_circleWidth + 2); // 留出边距 if (diameter <= 0) return; // 防止控件过小 float radius = diameter / 2f; float x = (clientWidth - diameter) / 2f; float y = (clientHeight - diameter) / 2f; RectangleF rect = new RectangleF(x, y, diameter, diameter); // 2. 绘制背景圆环 using (Pen backgroundPen = new Pen(_circleColor, _circleWidth)) { // 绘制一个完整的360度圆环作为背景 graphics.DrawArc(backgroundPen, rect, 0, 360); } // 3. 绘制进度圆环 if (_maximum > _minimum && _value > _minimum) { float percentage = (float)(_value - _minimum) / (_maximum - _minimum); float sweepAngle = 360f * percentage; using (Pen progressPen = new Pen(_progressColor, _circleWidth)) { // 设置线帽为圆形,使进度条两端圆润 progressPen.StartCap = LineCap.Round; progressPen.EndCap = LineCap.Round; // 绘制进度弧段 graphics.DrawArc(progressPen, rect, _startAngle, sweepAngle); } } // 4. 绘制中心文本 if (_showText && !string.IsNullOrEmpty(_textFormat)) { string text = string.Format(_textFormat, _value); using (Font font = new Font(this.Font.FontFamily, diameter * 0.2f, FontStyle.Bold)) using (SolidBrush textBrush = new SolidBrush(_textColor)) { // 测量文本大小,使其居中 SizeF textSize = graphics.MeasureString(text, font); PointF textLocation = new PointF( (clientWidth - textSize.Width) / 2, (clientHeight - textSize.Height) / 2 ); graphics.DrawString(text, font, textBrush, textLocation); } } }代码关键点解析:
SmoothingMode.AntiAlias:这是提升视觉质量的关键设置,它会让线条和文字的边缘变得平滑,而不是锯齿状。using语句:Pen,Brush,Font等GDI+对象是非托管资源,必须及时释放。使用using语句可以确保在代码块结束后自动调用Dispose()方法,避免内存泄漏。这是必须遵守的GDI+编程规范。- 进度计算:
sweepAngle的计算考虑了Minimum不为0的情况,通用性更好。 - 线帽(LineCap):将
progressPen的StartCap和EndCap设置为Round,可以让进度条的两端呈现圆头,视觉效果更佳。 - 文本绘制与自适应:文本字体大小设置为直径的0.2倍,这样当控件缩放时,文字大小也能自适应。通过
MeasureString获取文本尺寸,再计算绘制位置,是实现居中的标准做法。
4.4 处理控件大小变化
为了让控件在调整大小时能正确重绘,我们需要响应Resize事件,或者依靠之前设置的ControlStyles.ResizeRedraw样式。通常设置样式就够了,但为了更精确的控制,可以重写OnResize方法。
protected override void OnResize(EventArgs e) { base.OnResize(e); // 强制控件变为正方形,以保证绘制的是正圆 int size = Math.Min(this.Width, this.Height); if (this.Width != size || this.Height != size) { this.Size = new Size(size, size); } this.Invalidate(); // 请求重绘 }这段代码让控件始终保持正方形。如果你希望控件是矩形,而里面的圆形自适应,可以去掉强制设为正方形的逻辑,只保留Invalidate()。
5. 使用示例与进阶技巧
5.1 在WinForm项目中使用控件
- 编译上面的控件库项目,生成一个DLL。
- 在你的WinForm应用程序项目中,右键“工具箱”,选择“选择项...”。
- 在弹出的对话框中点击“浏览”,找到并选中刚才生成的DLL。
- 工具箱中会出现
CircularProgressBar控件,将其拖放到窗体上。 - 在属性窗口中,你可以轻松修改
ProgressColor、CircleWidth、Value等属性。 - 在窗体代码中,你可以像使用普通进度条一样操作它:
// 假设控件实例名为circularProgressBar1 private void buttonStart_Click(object sender, EventArgs e) { timer1.Enabled = true; } private void timer1_Tick(object sender, EventArgs e) { if (circularProgressBar1.Value < circularProgressBar1.Maximum) { circularProgressBar1.Value += 1; } else { timer1.Enabled = false; } }5.2 进阶美化与功能扩展
基础的圆形进度条已经完成,但我们可以让它更强大、更美观。
1. 添加渐变色进度条单调的纯色有时不够出彩。我们可以使用LinearGradientBrush或PathGradientBrush来绘制渐变色进度。修改进度绘制部分:
using (Pen progressPen = new Pen(_progressColor, _circleWidth)) // 保留Pen用于定义宽度 { // 创建一个沿着圆弧的渐变画刷 GraphicsPath path = new GraphicsPath(); path.AddArc(rect, _startAngle, sweepAngle); using (PathGradientBrush pgb = new PathGradientBrush(path)) { pgb.CenterColor = Color.White; // 中心颜色 pgb.SurroundColors = new Color[] { _progressColor }; // 路径边缘颜色 // 注意:Pen不能直接使用Brush,我们需要用画刷重新创建一个Pen using (Pen gradientPen = new Pen(pgb, _circleWidth)) { gradientPen.StartCap = LineCap.Round; gradientPen.EndCap = LineCap.Round; graphics.DrawArc(gradientPen, rect, _startAngle, sweepAngle); } } }踩坑记录:
PathGradientBrush对路径的闭合性有要求,且直接用于绘制宽线条时,渐变方向可能不理想。对于圆形进度条,使用LinearGradientBrush并精心设置起止点,或者使用多个不同颜色的Pen分段绘制,可能是更简单可控的实现渐变的方案。
2. 添加动画效果让进度条的增长带有动画(如缓动效果)可以极大提升用户体验。这需要借助Timer,但不是简单递增Value,而是递增一个“目标值”和“当前显示值”,并在两者之间进行插值。
private int _targetValue = 0; private float _animatedValue = 0f; private Timer _animationTimer; private void AnimateToValue(int target) { _targetValue = target; if (_animationTimer == null) { _animationTimer = new Timer { Interval = 20 }; // 约50FPS _animationTimer.Tick += (s, e) => { // 使用线性插值(Lerp)向目标值靠近 _animatedValue = _animatedValue + (_targetValue - _animatedValue) * 0.1f; if (Math.Abs(_animatedValue - _targetValue) < 0.5f) { _animatedValue = _targetValue; _animationTimer.Stop(); } // 将_animatedValue赋值给Value,触发重绘 this.Value = (int)Math.Round(_animatedValue); }; } _animationTimer.Start(); } // 使用时,调用AnimateToValue(80),而不是直接设置circularProgressBar1.Value = 803. 自定义文本内容我们的TextFormat属性支持格式化字符串。你可以将其设置为“{0}/{1}”来显示“当前值/最大值”,或者设置为空字符串来完全不显示文本。
6. 常见问题与排查技巧实录
在开发和测试这个控件的过程中,我遇到了几个典型问题,这里记录下来供你参考。
问题1:控件在拖动或快速更新时严重闪烁。
- 排查:首先检查是否在构造函数中正确设置了双缓冲样式(
OptimizedDoubleBuffer等)。如果已设置,检查是否在OnPaint方法中频繁创建和销毁Graphics对象(通常不会)。还有一个常见原因是在OnPaint外部(比如在Timer事件里)直接调用了this.CreateGraphics()进行绘制,这一定会导致闪烁。 - 解决:永远只在
OnPaint或Paint事件中进行绘制。更新数据(Value)后,调用this.Invalidate()或this.Refresh()来请求重绘,让系统在合适的时机调用你的OnPaint方法。
问题2:进度条圆环边缘有锯齿,不光滑。
- 排查:检查
OnPaint方法开头是否设置了graphics.SmoothingMode = SmoothingMode.AntiAlias;。 - 解决:确保该设置被正确执行。同时,检查
Pen的宽度是否为整数,过细的线(如1像素)抗锯齿效果可能不明显。
问题3:改变控件大小时,圆形变形了(成了椭圆)。
- 排查:计算绘制矩形
rect时,是否使用了Math.Min(Width, Height)来确保直径是正方形边长?OnResize方法是否强制将控件大小设为正方形? - 解决:如果希望控件是矩形而内部圆形自适应,应确保
diameter的计算基于Math.Min(ClientSize.Width, ClientSize.Height)。如果希望控件本身保持正方形,则在OnResize中调整Size属性。
问题4:属性在设计器里修改了,但窗体上看不到实时变化。
- 排查:在属性的
set访问器中,修改了私有字段后,是否调用了this.Invalidate()来触发重绘? - 解决:确保所有影响外观的属性(颜色、宽度、角度等)在
set中都调用了this.Invalidate()。
问题5:运行时报错“对象当前正在其他地方使用”。
- 排查:这是典型的GDI+资源未正确释放导致的。检查所有
Pen,Brush,Font,GraphicsPath对象是否都包裹在using语句中,或者手动调用了Dispose()。 - 解决:坚决使用
using语句。这是处理任何实现了IDisposable接口的GDI+对象的最佳实践。
最后,这个自定义圆形进度条控件的完整源码,我已经将核心逻辑全部呈现。你可以基于此代码进行编译和测试。在实际项目中,你可能还需要添加更多的属性,如CircleCapStyle(线帽样式)、ProgressStyle(实心、渐变、图片纹理)等,来满足更复杂的设计需求。其核心思想是不变的:理解GDI+绘图原理、合理设计控件属性与事件、并妥善管理图形资源。掌握了这些,你就能创造出任何你想要的WinForm自定义控件了。
本文还有配套的精品资源,点击获取