65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (42投票s)

2009年8月23日

CPOL

4分钟阅读

viewsIcon

116096

downloadIcon

5950

使用 GDI+ 创建一个圆形进度控件,类似于 Mac OS X 中的控件。

引言

我一直对 Mac OS X GUI 印象深刻。它非常简洁和优雅。在本文中,我将向您展示如何使用 GDI+ 创建一个用户控件,类似于 Mac OS X 中的异步圆形进度指示器。

根据我从 BillWoodRuffdequadin 收到的评论,我对我的代码进行了进一步的改进,创建了两个新的圆形进度控件

  • 优化后的圆形进度控件
  • 矩阵圆形进度控件
我将逐一介绍它们的细节...

圆形进度控件

在附加的项目中,我创建了一个用户控件,称为 CircularProgressControl,它封装了进度控件的渲染。它还提供了一些额外的属性,比如控件的颜色、进度速度以及控件的起始角度。下面是 CircularProgressControl 的类图

StartStop 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 等于 CircularProgressControlInterval 属性的值。在定时器的每次 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 属性。

© . All rights reserved.