看过上一篇文章的应该知道,Arc和Pie的实现方式区别不过是一个把Path路径的终点与起点相连,一个没有相连,于是本人就索性把二者合二为一了。决定是画Arc还是画Pie,完全根据Stroke和Fill两个属性来判断。
废话不多说,终极代码奉上:
public class Circle : Shape
{
#region 成员变量
private Geometry m_Data;
private Geometry Data
{
get { return m_Data; }
set
{
if (m_Data != value)
{
m_Data = value;
this.InvalidateVisual();
}
}
}
private CircleStatus m_Status;
#endregion
#region 重写方法
/// <summary>
/// 监听StrokeThickness变化
/// </summary>
/// <param name="e"></param>
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property == StrokeThicknessProperty)
{
propertyChangedCallback(this, new DependencyPropertyChangedEventArgs());
}
else if (e.Property == FillProperty)
{
m_Status = e.NewValue == null ? CircleStatus.Ring : CircleStatus.Pie;
propertyChangedCallback(this, new DependencyPropertyChangedEventArgs());
}
else if (e.Property == WidthProperty || e.Property == HeightProperty || e.Property == ActualWidthProperty || e.Property == ActualHeightProperty)
{
propertyChangedCallback(this, new DependencyPropertyChangedEventArgs());
}
}
/// <summary>
/// 重写此方法实现图形绘制
/// </summary>
protected override Geometry DefiningGeometry { get { return Data; } }
#endregion
#region 依赖属性
/// <summary>
/// 起始角度
/// </summary>
public double StartAngle
{
get { return (double)GetValue(StartAngleProperty); }
set { SetValue(StartAngleProperty, value); }
}
// Using a DependencyProperty as the backing store for StartAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty StartAngleProperty =
DependencyProperty.Register("StartAngle", typeof(double), typeof(Circle), new PropertyMetadata(0.0, propertyChangedCallback));
/// <summary>
/// 终止角度
/// </summary>
public double EndAngle
{
get { return (double)GetValue(EndAngleProperty); }
set { SetValue(EndAngleProperty, value); }
}
// Using a DependencyProperty as the backing store for EndAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty EndAngleProperty =
DependencyProperty.Register("EndAngle", typeof(double), typeof(Circle), new PropertyMetadata(0.0, propertyChangedCallback));
/// <summary>
/// 内边距
/// </summary>
public double Padding
{
get { return (double)GetValue(PaddingProperty); }
set { SetValue(PaddingProperty, value); }
}
// Using a DependencyProperty as the backing store for Padding. This enables animation, styling, binding, etc...
public static readonly DependencyProperty PaddingProperty =
DependencyProperty.Register("Padding", typeof(double), typeof(Circle), new PropertyMetadata(0.0, propertyChangedCallback));
#endregion
#region 图形语句生成
private static void propertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is Circle circle)
{
// 计算内边距
var padding = circle.m_Status == CircleStatus.Ring ? circle.StrokeThickness / 2 : 0;
padding += circle.Padding;
// 判断绘制的圆弧是否可见,以提高性能
double width = circle.Width;
double height = circle.Height;
if (double.IsNaN(width) || double.IsNaN(height))
{
if (double.IsNaN(circle.DesiredSize.Width) || double.IsNaN(circle.DesiredSize.Height) || circle.DesiredSize == new Size(0, 0))
{
width = circle.ActualWidth;
height = circle.ActualHeight;
}
else
{
width = circle.DesiredSize.Width;
height = circle.DesiredSize.Height;
}
}
if (width > 0 && height > 0 && circle.StartAngle != circle.EndAngle)
{
double ellipseA = width / 2 - padding;
double ellipseB = height / 2 - padding;
// 起止角度间隔大于等于360°,直接画圆
if (Math.Abs(circle.StartAngle - circle.EndAngle) >= 360)
{
string data = $"M{padding + ellipseA * 2},{padding + ellipseB + 0.001} A{ellipseA},{ellipseB} 0,1,1 {padding + ellipseA * 2},{padding + ellipseB}z";
circle.Data = (Geometry)Geometry.Parse(data);
return;
}
// 椭圆公式:X²/a²+Y²/b²=1
// Rect与椭圆各参数的对应关系:
// Rect.X与Rect.Y分别是椭圆外接矩形相对Circle区域的左上角的偏移量;
// Rect.Width与Rect.Height分别是椭圆外接矩形的宽和高
// 以下根据StartAngle和EndAngle算出圆弧在矩形函数曲线中的起始点和终止点
// 判断绘制方向
bool clockWise = circle.EndAngle > circle.StartAngle;
// 判断优弧/劣弧
bool majorArc = Math.Abs(circle.StartAngle - circle.EndAngle) % 360 >= 180;
// 将起始点和终止点转化为椭圆上的坐标
double rad = 0;
Point startPoint = new Point();
if ((90 - circle.StartAngle) % 360 == 0)
{
startPoint.X = 0;
startPoint.Y = ellipseB;
}
else if ((270 - circle.StartAngle) % 360 == 0)
{
startPoint.X = 0;
startPoint.Y = -ellipseB;
}
else
{
rad = GetRadian(circle.StartAngle);
startPoint.X = ellipseA * ellipseB / Math.Sqrt(Math.Pow(ellipseB, 2) + Math.Pow(ellipseA * Math.Tan(rad), 2));
startPoint.X *= Math.Cos(rad) > 0 ? 1 : -1;
startPoint.Y = startPoint.X * Math.Tan(rad);
}
Point endPoint = new Point();
if ((90 - circle.EndAngle) % 360 == 0)
{
endPoint.X = 0;
endPoint.Y = ellipseB;
}
else if ((270 - circle.EndAngle) % 360 == 0)
{
endPoint.X = 0;
endPoint.Y = -ellipseB;
}
else
{
rad = GetRadian(circle.EndAngle);
endPoint.X = ellipseA * ellipseB / Math.Sqrt(Math.Pow(ellipseB, 2) + Math.Pow(ellipseA * Math.Tan(rad), 2));
endPoint.X *= Math.Cos(rad) > 0 ? 1 : -1;
endPoint.Y = endPoint.X * Math.Tan(rad);
}
string pathData = $"M";
if (circle.m_Status == CircleStatus.Pie)
{
pathData += $"{ellipseA + padding},{ellipseB + padding} ";
}
pathData += $"{startPoint.X + ellipseA + padding},{startPoint.Y + ellipseB + padding} ";
pathData += $"A{ellipseA},{ellipseB} ";
pathData += $"0,{(majorArc ? "1" : "0")},{(clockWise ? "1" : "0")} ";
pathData += $"{endPoint.X + ellipseA + padding},{endPoint.Y + ellipseB + padding}";
circle.Data = (Geometry)Geometry.Parse(pathData);
}
else
{
circle.Data = (Geometry)Geometry.Parse("");
}
}
}
/// <summary>
/// 将角度转化为弧度
/// </summary>
/// <param name="angle"></param>
/// <returns></returns>
private static double GetRadian(double angle)
{
return angle / 180.0 * Math.PI;
}
#endregion
}
进阶玩法:
只需两个Circle即可构成环形进度条
<Grid Width="70" Height="70">
<local:Circle StartAngle="0" EndAngle="360" Stroke="LightGray" StrokeThickness="15"/>
<local:Circle StartAngle="-90" EndAngle="180" Stroke="DodgerBlue" StrokeThickness="15"/>
<TextBlock Text="75%" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>