在日常开发中,我们常常会需要实现一个渐变的圆环,如制作一个仪表盘,像下面这样——
而往往开发软件中并没有能够直接设置弧形路径渐变的画刷,因此实现起来多有不便。
下面介绍三种方法来实现这样的弧形渐变效果
1、交给美工
这也是最简单的方法,找美工提供一张设计好的渐变环作为底图贴上去就可以了。实际开发中应当是这样,省时省力,且性能损耗是最低的。
2、手动实现渐变路径
思路可能与美工实现的方法类似,用多段线性渐变拼接成一个多角度渐变。
如果需要绘制的圆环小于180°,则不需要大费周章,直接一个线性渐变就能解决,像下面这样
渐变画刷的代码如下:
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="Red" Offset="0"/>
<GradientStop Color="Yellow" Offset="0.25"/>
<GradientStop Color="Green" Offset="0.5"/>
<GradientStop Color="Cyan" Offset="0.75"/>
<GradientStop Color="Purple" Offset="1"/>
</LinearGradientBrush>
如果觉得渐变不够自然,则可以将圆弧拆分成多段绘制,比如180°拆成两个90°的圆弧,
两段渐变画刷分别如下:
<!--第一段-->
<LinearGradientBrush StartPoint="0,1" EndPoint="1,0">
<GradientStop Color="Red" Offset="0"/>
<GradientStop Color="Yellow" Offset="0.5"/>
<GradientStop Color="Green" Offset="1"/>
</LinearGradientBrush>
<!--第二段-->
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="Green" Offset="0"/>
<GradientStop Color="Cyan" Offset="0.5"/>
<GradientStop Color="Purple" Offset="1"/>
</LinearGradientBrush>
优化后的效果如下:
对于角度较小的圆弧,这样做是没什么问题,但是一旦角度变大,需要分割的数量就变多,而拼接区的颜色过渡处理起来就相对麻烦。比如上述方法处理后的圆弧,加粗后能看出明显分层
3、角度微分法
此方法可以说是第二种方法的升级版,就是将角度分割得足够小,每段弧的颜色依次渐变。
此方法只需要解决两个问题:一是生成具有多个渐变点的颜色集合,二是精确计算每段弧的起始和终止点。
第一个问题,我们先根据GradientStopCollection属性,计算出每个Color对应到颜色集合中的索引,再生成每两个GradientStop之间的渐变色填充到集合中。
运用到如下两个函数:
/// <summary>
/// 获取单段渐变的颜色集合
/// </summary>
/// <param name="startColor">起始颜色</param>
/// <param name="endColor">终止颜色</param>
/// <param name="count">分度数</param>
/// <returns>返回颜色集合</returns>
public static List<Color> GetSingleColorList(Color startColor, Color endColor, int count, bool isIncludingEndColor = false)
{
List<Color> colorFactorList = new List<Color>();
int alphaSpan = endColor.A - startColor.A;
int redSpan = endColor.R - startColor.R;
int greenSpan = endColor.G - startColor.G;
int blueSpan = endColor.B - startColor.B;
for (int i = 0; i < count; i++)
{
Color color = Color.FromArgb(
(byte)(startColor.A + (int)((double)i / (isIncludingEndColor ? (count - 1) : count) * alphaSpan)),
(byte)(startColor.R + (int)((double)i / (isIncludingEndColor ? (count - 1) : count) * redSpan)),
(byte)(startColor.G + (int)((double)i / (isIncludingEndColor ? (count - 1) : count) * greenSpan)),
(byte)(startColor.B + (int)((double)i / (isIncludingEndColor ? (count - 1) : count) * blueSpan))
);
colorFactorList.Add(color);
}
return colorFactorList;
}
/// <summary>
/// 获取多段渐变的颜色集合
/// </summary>
/// <param name="gradientStopCollection"></param>
/// <param name="totalCount"></param>
/// <returns></returns>
public static List<Color> GetColorList(GradientStopCollection gradientStopCollection, int totalCount)
{
// 1、计算渐变关键点在颜色集合中的位置
var stopIndexList = new List<int>();
var stopColorList = new List<Color>();
var gradientStops = gradientStopCollection.OrderBy(a => a.Offset);
foreach (var stop in gradientStops)
{
stopColorList.Add(stop.Color);
var offset = Math.Max(0, stop.Offset);
offset = Math.Min(1, stop.Offset);
stopIndexList.Add((int)(totalCount * offset));
}
if (stopIndexList.First() != 0)
{
stopIndexList.Insert(0, 0);
stopColorList.Insert(0, stopColorList.First());
}
if (stopIndexList.Last() != totalCount)
{
stopIndexList.Add(totalCount);
stopColorList.Add(stopColorList.Last());
}
// 2、生成渐变色集合
var gradientColorList = new List<Color>();
for (int i = 0; i < stopIndexList.Count - 1; i++)
{
var count = stopIndexList[i + 1] - stopIndexList[i];
var colors = GetSingleColorList(stopColorList[i], stopColorList[i + 1], count, i == stopIndexList.Count - 2);
gradientColorList.AddRange(colors);
}
return gradientColorList;
}
第二个问题,运用椭圆公式求出椭圆上指定角度对应的点坐标即可。
求点坐标的函数如下:
/// <summary>
/// 将角度转化为弧度
/// </summary>
/// <param name="angle"></param>
/// <returns></returns>
public static double GetRadian(double angle)
{
return angle / 180.0 * Math.PI;
}
/// <summary>
/// 获取椭圆上的点坐标
/// </summary>
/// <param name="majorAxis">椭圆长轴长度</param>
/// <param name="minorAxis">椭圆短轴长度</param>
/// <param name="angle">角度</param>
/// <returns></returns>
public static Point GetPointOnEllipse(double majorAxis, double minorAxis, double angle)
{
Point point = new Point();
if ((90 - angle) % 360 == 0)
{
point.X = 0;
point.Y = minorAxis;
}
else if ((270 - angle) % 360 == 0)
{
point.X = 0;
point.Y = -minorAxis;
}
else
{
var rad = GetRadian(angle);
point.X = majorAxis * minorAxis / Math.Sqrt(Math.Pow(minorAxis, 2) + Math.Pow(majorAxis * Math.Tan(rad), 2));
point.X *= Math.Cos(rad) > 0 ? 1 : -1;
point.Y = point.X * Math.Tan(rad);
}
return point;
}
接下来就不细致讲解控件的构造过程,放出全部代码如下:
[ContentProperty("GradientStops")]
public class GradientCircle : Control
{
#region 依赖属性
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(GradientCircle), new PropertyMetadata(0.0));
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(GradientCircle), new PropertyMetadata(0.0));
public GradientStopCollection GradientStops
{
get { return (GradientStopCollection)GetValue(GradientStopsProperty); }
set { SetValue(GradientStopsProperty, value); }
}
// Using a DependencyProperty as the backing store for GradientStops. This enables animation, styling, binding, etc...
public static readonly DependencyProperty GradientStopsProperty =
DependencyProperty.Register("GradientStops", typeof(GradientStopCollection), typeof(GradientCircle), new PropertyMetadata(null));
public double StrokeThickness
{
get { return (double)GetValue(StrokeThicknessProperty); }
set { SetValue(StrokeThicknessProperty, value); }
}
// Using a DependencyProperty as the backing store for StrokeThickness. This enables animation, styling, binding, etc...
public static readonly DependencyProperty StrokeThicknessProperty =
DependencyProperty.Register("StrokeThickness", typeof(double), typeof(GradientCircle), new PropertyMetadata(10.0));
public double Resolution
{
get { return (double)GetValue(ResolutionProperty); }
set { SetValue(ResolutionProperty, value); }
}
// Using a DependencyProperty as the backing store for Resolution. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ResolutionProperty =
DependencyProperty.Register("Resolution", typeof(double), typeof(GradientCircle), new PropertyMetadata(1.0, (s, e) =>
{
if (s is GradientCircle circle)
{
if ((double)e.NewValue > 180)
{
circle.Resolution = (double)e.OldValue;
}
}
}));
public PenLineCap StrokeStartLineCap
{
get { return (PenLineCap)GetValue(StrokeStartLineCapProperty); }
set { SetValue(StrokeStartLineCapProperty, value); }
}
// Using a DependencyProperty as the backing store for StrokeStartLineCap. This enables animation, styling, binding, etc...
public static readonly DependencyProperty StrokeStartLineCapProperty =
DependencyProperty.Register("StrokeStartLineCap", typeof(PenLineCap), typeof(GradientCircle), new PropertyMetadata(PenLineCap.Flat));
public PenLineCap StrokeEndLineCap
{
get { return (PenLineCap)GetValue(StrokeEndLineCapProperty); }
set { SetValue(StrokeEndLineCapProperty, value); }
}
// Using a DependencyProperty as the backing store for StrokeEndLineCap. This enables animation, styling, binding, etc...
public static readonly DependencyProperty StrokeEndLineCapProperty =
DependencyProperty.Register("StrokeEndLineCap", typeof(PenLineCap), typeof(GradientCircle), new PropertyMetadata(PenLineCap.Flat));
#endregion
#region 重写方法
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property == WidthProperty
|| e.Property == HeightProperty
|| e.Property == StartAngleProperty
|| e.Property == EndAngleProperty
|| e.Property == StrokeThicknessProperty
|| e.Property == GradientStopsProperty
|| e.Property == ResolutionProperty
|| e.Property == StrokeStartLineCapProperty
|| e.Property == StrokeEndLineCapProperty)
{
this.InvalidateVisual();
}
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
if (StartAngle == EndAngle || GradientStops == null || GradientStops.Count == 0 || StrokeThickness == 0) return;
if (double.IsNaN(ActualWidth) || double.IsNaN(ActualHeight) || ActualWidth == 0 || ActualHeight == 0) return;
if (ActualWidth < StrokeThickness || ActualHeight < StrokeThickness) { StrokeThickness = Math.Min(ActualWidth / 2, ActualHeight / 2); }
// 1、计算圆弧分段个数
var arcCount = (int)(Math.Ceiling(Math.Abs((EndAngle - StartAngle) / Resolution)));
// 2、生成渐变色集合
var gradientColorList = GetMultiSegmentColorList(GradientStops, arcCount);
// 3、生成N段渐变圆弧
var clockWise = EndAngle > StartAngle ? 1 : 0;
var delta = clockWise == 1 ? Resolution : -Resolution;
for (int i = 0; i < arcCount; i++)
{
var startAngle = StartAngle + i * delta + (delta > 0 ? -0.5 : 0.5);
var endAngle = delta > 0 ? Math.Min(StartAngle + (i + 1) * delta, EndAngle) : Math.Max(StartAngle + (i + 1) * delta, EndAngle);
var padding = StrokeThickness / 2;
var majorAxis = (ActualWidth - StrokeThickness) / 2;
var minorAxis = (ActualHeight - StrokeThickness) / 2;
var startPoint = GetPointOnEllipse(majorAxis, minorAxis, startAngle);
var endPoint = GetPointOnEllipse(majorAxis, minorAxis, endAngle);
var geometry = Geometry.Parse($"M{startPoint.X + majorAxis + padding},{startPoint.Y + minorAxis + padding} " +
$"A{majorAxis},{minorAxis} " +
$"0,0,{clockWise} " +
$"{endPoint.X + majorAxis + padding},{endPoint.Y + minorAxis + padding}");
var pen = new Pen(new SolidColorBrush(gradientColorList[i]), StrokeThickness);
if (i == 0)
{
pen.StartLineCap = StrokeStartLineCap;
}
else if (i == arcCount - 1)
{
pen.EndLineCap = StrokeEndLineCap;
}
drawingContext.DrawGeometry(null, pen, geometry);
}
}
#endregion
#region 通用方法
/// <summary>
/// 获取单段渐变的颜色集合
/// </summary>
/// <param name="startColor">起始颜色</param>
/// <param name="endColor">终止颜色</param>
/// <param name="count">颜色数量</param>
/// <returns>返回颜色集合</returns>
public static List<Color> GetSingleSegmentColorList(Color startColor, Color endColor, int count, bool isIncludingEndColor = false)
{
List<Color> colorFactorList = new List<Color>();
int alphaSpan = endColor.A - startColor.A;
int redSpan = endColor.R - startColor.R;
int greenSpan = endColor.G - startColor.G;
int blueSpan = endColor.B - startColor.B;
for (int i = 0; i < count; i++)
{
Color color = Color.FromArgb(
(byte)(startColor.A + (int)((double)i / (isIncludingEndColor ? (count - 1) : count) * alphaSpan)),
(byte)(startColor.R + (int)((double)i / (isIncludingEndColor ? (count - 1) : count) * redSpan)),
(byte)(startColor.G + (int)((double)i / (isIncludingEndColor ? (count - 1) : count) * greenSpan)),
(byte)(startColor.B + (int)((double)i / (isIncludingEndColor ? (count - 1) : count) * blueSpan))
);
colorFactorList.Add(color);
}
return colorFactorList;
}
/// <summary>
/// 获取多段渐变的颜色集合
/// </summary>
/// <param name="gradientStopCollection">渐变点集合</param>
/// <param name="totalCount">总颜色数量</param>
/// <returns></returns>
public static List<Color> GetMultiSegmentColorList(GradientStopCollection gradientStopCollection, int totalCount)
{
// 1、计算渐变关键点在颜色集合中的位置
var stopIndexList = new List<int>();
var stopColorList = new List<Color>();
var gradientStops = gradientStopCollection.OrderBy(a => a.Offset);
foreach (var stop in gradientStops)
{
stopColorList.Add(stop.Color);
var offset = Math.Max(0, stop.Offset);
offset = Math.Min(1, stop.Offset);
stopIndexList.Add((int)(totalCount * offset));
}
if (stopIndexList.First() != 0)
{
stopIndexList.Insert(0, 0);
stopColorList.Insert(0, stopColorList.First());
}
if (stopIndexList.Last() != totalCount)
{
stopIndexList.Add(totalCount);
stopColorList.Add(stopColorList.Last());
}
// 2、生成渐变色集合
var gradientColorList = new List<Color>();
for (int i = 0; i < stopIndexList.Count - 1; i++)
{
var count = stopIndexList[i + 1] - stopIndexList[i];
var colors = GetSingleSegmentColorList(stopColorList[i], stopColorList[i + 1], count, i == stopIndexList.Count - 2);
gradientColorList.AddRange(colors);
}
return gradientColorList;
}
/// <summary>
/// 将角度转化为弧度
/// </summary>
/// <param name="angle"></param>
/// <returns></returns>
public static double GetRadian(double angle)
{
return angle / 180.0 * Math.PI;
}
/// <summary>
/// 获取椭圆上的点坐标
/// </summary>
/// <param name="majorAxis">椭圆长轴长度</param>
/// <param name="minorAxis">椭圆短轴长度</param>
/// <param name="angle">角度</param>
/// <returns></returns>
public static Point GetPointOnEllipse(double majorAxis, double minorAxis, double angle)
{
Point point = new Point();
if ((90 - angle) % 360 == 0)
{
point.X = 0;
point.Y = minorAxis;
}
else if ((270 - angle) % 360 == 0)
{
point.X = 0;
point.Y = -minorAxis;
}
else
{
var rad = GetRadian(angle);
point.X = majorAxis * minorAxis / Math.Sqrt(Math.Pow(minorAxis, 2) + Math.Pow(majorAxis * Math.Tan(rad), 2));
point.X *= Math.Cos(rad) > 0 ? 1 : -1;
point.Y = point.X * Math.Tan(rad);
}
return point;
}
#endregion
}
由于控件完全是由代码生成图形界面的,所以不需要样式支撑。
最后放一张该控件生成的完整圆环:
对应Xaml代码如下:
<local:GradientCircle Width="100" Height="100" StrokeThickness="20" StartAngle="0" EndAngle="360" Resolution="0.5">
<local:GradientCircle.GradientStops>
<GradientStopCollection>
<GradientStop Color="Red" Offset="0"/>
<GradientStop Color="Orange" Offset="0.1428"/>
<GradientStop Color="Yellow" Offset="0.2857"/>
<GradientStop Color="Green" Offset="0.4285"/>
<GradientStop Color="Cyan" Offset="0.5714"/>
<GradientStop Color="Blue" Offset="0.7142"/>
<GradientStop Color="Purple" Offset="0.8571"/>
<GradientStop Color="Red" Offset="1"/>
</GradientStopCollection>
</local:GradientCircle.GradientStops>
</local:GradientCircle>