圆形进度控件 - Mac OS X 风格






4.75/5 (42投票s)
使用 GDI+ 创建一个圆形进度控件,类似于 Mac OS X 中的控件。
引言
我一直对 Mac OS X GUI 印象深刻。它非常简洁和优雅。在本文中,我将向您展示如何使用 GDI+ 创建一个用户控件,类似于 Mac OS X 中的异步圆形进度指示器。
根据我从 BillWoodRuff 和 dequadin 收到的评论,我对我的代码进行了进一步的改进,创建了两个新的圆形进度控件
- 优化后的圆形进度控件
- 矩阵圆形进度控件
圆形进度控件
在附加的项目中,我创建了一个用户控件,称为 CircularProgressControl
,它封装了进度控件的渲染。它还提供了一些额外的属性,比如控件的颜色、进度速度以及控件的起始角度。下面是 CircularProgressControl
的类图
Start
和 Stop
API 允许用户分别启动和停止动画。
使用控件
为了使用此控件,您可以添加对此项目的引用,并将 CircularProgressControl
从工具箱拖放到您的 Form
上。您可以设置颜色、速度和起始角度。起始角度以度为单位指定,并按顺时针方向增加。
您还可以设置旋转方向:CLOCKWISE
(顺时针)或 ANTICLOCKWISE
(逆时针)。为此,您需要将 Rotation
属性设置为 Direction
枚举的其中一个值。
public enum Direction
{
CLOCKWISE,
ANTICLOCKWISE
}
圆形进度控件揭秘
为了渲染控件的辐条,首先控件计算两个圆 - 内圆和外圆。这两个圆是同心的。这两个圆的半径取决于 CircularProgressControl
的大小。起点 (X1, Y1) 和终点 (X2, Y2) 是根据辐条的角度计算的。两个相邻辐条之间的角度 (m_AngleIncrement
) 基于辐条的数量 (m_SpokesCount
),其计算公式如下
m_AngleIncrement = (int)(360/m_SpokesCount);
第一个辐条颜色的 Alpha 值为 255。在渲染每个辐条后,下一个辐条颜色的 alpha 值会减少一个固定量 (m_AlphaDecrement
)。
辐条的粗细也随 CircularProgressControl
的大小而变化。
计算和渲染在控件的 PaintEventHandler
中完成。
protected override void OnPaint(PaintEventArgs e)
{
// All the paintin will be handled by us.
//base.OnPaint(e);
e.Graphics.InterpolationMode =
System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
e.Graphics.SmoothingMode =
System.Drawing.Drawing2D.SmoothingMode.HighQuality;
// Since the Rendering of the spokes is dependent upon the current size of the
// control, the following calculation needs to be done within the Paint eventhandler.
int alpha = m_AlphaStartValue;
int angle = m_StartAngle;
// Calculate the location around which the spokes will be drawn
int width = (this.Width < this.Height) ? this.Width : this.Height;
m_CentrePt = new PointF(this.Width / 2, this.Height / 2);
// Calculate the width of the pen which will be used to draw the spokes
m_Pen.Width = (int)(width / 15);
if (m_Pen.Width < MINIMUM_PEN_WIDTH)
m_Pen.Width = MINIMUM_PEN_WIDTH;
// Calculate the inner and outer radii of the control.
// The radii should not be less than the
// Minimum values
m_InnerRadius = (int)(width * (140 / (float)800));
if (m_InnerRadius < MINIMUM_INNER_RADIUS)
m_InnerRadius = MINIMUM_INNER_RADIUS;
m_OuterRadius = (int)(width * (250 / (float)800));
if (m_OuterRadius < MINIMUM_OUTER_RADIUS)
m_OuterRadius = MINIMUM_OUTER_RADIUS;
// Render the spokes
for (int i = 0; i < m_SpokesCount; i++)
{
PointF pt1 = new PointF(m_InnerRadius *
(float)Math.Cos(ConvertDegreesToRadians(angle)),
m_InnerRadius * (float)Math.Sin(ConvertDegreesToRadians(angle)));
PointF pt2 = new PointF(m_OuterRadius *
(float)Math.Cos(ConvertDegreesToRadians(angle)),
m_OuterRadius * (float)Math.Sin(ConvertDegreesToRadians(angle)));
pt1.X += m_CentrePt.X;
pt1.Y += m_CentrePt.Y;
pt2.X += m_CentrePt.X;
pt2.Y += m_CentrePt.Y;
m_Pen.Color = Color.FromArgb(alpha, this.TickColor);
e.Graphics.DrawLine(m_Pen, pt1, pt2);
if (Rotation == Direction.CLOCKWISE)
{
angle -= m_AngleIncrement;
}
else if (Rotation == Direction.ANTICLOCKWISE)
{
angle += m_AngleIncrement;
}
alpha -= m_AlphaDecrement;
}
}
当调用 Start
API 时,会启动一个 Timer
,其 TickInterval
等于 CircularProgressControl
的 Interval
属性的值。在定时器的每次 Tick
事件中,第一个辐条的角度会根据 Rotation
属性增加或减少,然后调用 Invalidate()
方法,强制控件重新绘制。Stop
API 停止定时器。
优化后的圆形进度控件
在优化后的圆形进度控件中,我从 OnPaint
方法中取出了点计算部分,以提高控件的性能,并将其移动到另一个名为 CalculateSpokePoints
的方法中。此方法将在构造函数中、当 Rotation 属性更改时以及当控件的大小在运行时或设计时更改时调用(这通过订阅 ClientSizeChanged
事件来处理)。我还定义了一个数据结构来存储每个辐条的起点和终点。
struct Spoke
{
public PointF StartPoint;
public PointF EndPoint;
public Spoke(PointF pt1, PointF pt2)
{
StartPoint = pt1;
EndPoint = pt2;
}
}
在 CalculateSpokePoints
方法中,计算点并将其封装在 Spoke
结构中,并存储在 m_SpokePoints
中。
/// <summary>
/// Calculate the Spoke Points and store them
/// </summary>
private void CalculateSpokesPoints()
{
m_Spokes = new List<Spoke>();
// Calculate the angle between adjacent spokes
m_AngleIncrement = (360 / (float)m_SpokesCount);
// Calculate the change in alpha between adjacent spokes
m_AlphaChange = (int)((255 - m_AlphaLowerLimit) / m_SpokesCount);
// Calculate the location around which the spokes will be drawn
int width = (this.Width < this.Height) ? this.Width : this.Height;
m_CentrePt = new PointF(this.Width / 2, this.Height / 2);
// Calculate the width of the pen which will be used to draw the spokes
m_Pen.Width = (int)(width / 15);
if (m_Pen.Width < MINIMUM_PEN_WIDTH)
m_Pen.Width = MINIMUM_PEN_WIDTH;
// Calculate the inner and outer radii of the control.
//The radii should not be less than the Minimum values
m_InnerRadius = (int)(width * INNER_RADIUS_FACTOR);
if (m_InnerRadius < MINIMUM_INNER_RADIUS)
m_InnerRadius = MINIMUM_INNER_RADIUS;
m_OuterRadius = (int)(width * OUTER_RADIUS_FACTOR);
if (m_OuterRadius < MINIMUM_OUTER_RADIUS)
m_OuterRadius = MINIMUM_OUTER_RADIUS;
float angle = 0;
for (int i = 0; i < m_SpokesCount; i++)
{
PointF pt1 = new PointF(m_InnerRadius * (float)Math.Cos(
ConvertDegreesToRadians(angle)), m_InnerRadius * (float)Math.Sin(
ConvertDegreesToRadians(angle)));
PointF pt2 = new PointF(m_OuterRadius * (float)Math.Cos(
ConvertDegreesToRadians(angle)), m_OuterRadius * (float)Math.Sin(
ConvertDegreesToRadians(angle)));
pt1.X += m_CentrePt.X;
pt1.Y += m_CentrePt.Y;
pt2.X += m_CentrePt.X;
pt2.Y += m_CentrePt.Y;
// Create a spoke based on the points generated
Spoke spoke = new Spoke(pt1, pt2);
// Add the spoke to the List
m_Spokes.Add(spoke);
if (Rotation == Direction.CLOCKWISE)
{
angle -= m_AngleIncrement;
}
else if (Rotation == Direction.ANTICLOCKWISE)
{
angle += m_AngleIncrement;
}
}
}
以 0 度角绘制的辐条的 Alpha 值计算如下
/// <summary>
/// Calculate the Alpha Value of the Spoke drawn at 0 degrees angle
/// </summary>
private void CalculateAlpha()
{
if (this.Rotation == Direction.CLOCKWISE)
{
if (m_StartAngle >= 0)
{
m_AlphaStartValue = 255 - (((int)((
m_StartAngle % 360) / m_AngleIncrement) + 1) *
m_AlphaChange);
}
else
{
m_AlphaStartValue = 255 - (((int)((360 +
(m_StartAngle % 360)) / m_AngleIncrement) + 1) *
m_AlphaChange);
}
}
else
{
if (m_StartAngle >= 0)
{
m_AlphaStartValue = 255 - (((int)((360 - (
m_StartAngle % 360)) / m_AngleIncrement) + 1) *
m_AlphaChange);
}
else
{
m_AlphaStartValue = 255 - (((int)(((
360 - m_StartAngle) % 360) / m_AngleIncrement) +
1) * m_AlphaChange);
}
}
}
现在 OnPaint
方法如下所示
/// <summary>
/// Handles the Paint Event of the control
/// </summary>
/// <param name="e">PaintEventArgs</param>
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.InterpolationMode =
System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
int alpha = m_AlphaStartValue;
// Render the spokes
for (int i = 0; i < m_SpokesCount; i++)
{
m_Pen.Color = Color.FromArgb(alpha, this.TickColor);
e.Graphics.DrawLine(m_Pen, m_Spokes[i].StartPoint,
m_Spokes[i].EndPoint);
alpha -= m_AlphaChange;
if (alpha < m_AlphaLowerLimit)
alpha = 255 - m_AlphaChange;
}
}
每次发生 Timer.Elapsed
时,我都在操作 m_AlphaStartValue
,以便辐条的 alpha 值不断变化。
矩阵圆形进度控件
我进一步修改了 OptimizedCircularProgressControl
以创建此控件。在 Paint 方法中,我采用了平移和旋转变换。
/// <summary>
/// Handles the Paint Event of the control
/// </summary>
/// <param name="e">PaintEventArgs</param>
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.InterpolationMode =
System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
// Perform a Translation so that the centre of the control
// is the centre of Rotation
e.Graphics.TranslateTransform(m_CentrePt.X, m_CentrePt.Y,
System.Drawing.Drawing2D.MatrixOrder.Prepend);
// Perform a Rotation about the control's centre
e.Graphics.RotateTransform(m_StartAngle,
System.Drawing.Drawing2D.MatrixOrder.Prepend);
int alpha = 255;
// Render the spokes
for (int i = 0; i < m_SpokesCount; i++)
{
m_Pen.Color = Color.FromArgb(alpha, this.TickColor);
e.Graphics.DrawLine(m_Pen, m_Spokes[i].StartPoint,
m_Spokes[i].EndPoint);
alpha -= m_AlphaChange;
if (alpha < m_AlphaLowerLimit)
alpha = 255 - m_AlphaChange;
}
// Perform a reverse Rotation and Translation to obtain the
// original Transformation
e.Graphics.RotateTransform(-m_StartAngle,
System.Drawing.Drawing2D.MatrixOrder.Append);
e.Graphics.TranslateTransform(-m_CentrePt.X, -m_CentrePt.Y,
System.Drawing.Drawing2D.MatrixOrder.Append);
}
旋转角度在 Timer.Elapsed
事件处理程序中更改。
当您编译附加的源代码(优化后的进度控件)并执行它时,您将看到一个 Form,其中我并排放置了 3 个控件。
注意
在使用 OptimizedCircularControl 时,如果您自己设置 StartAngle,请确保先设置 Rotation 属性。