一个扇形分割排版控件
先上结果
等角分割,文字以扇形中线显示 https://www.zhihu.com/video/1681204209972834304扇形实现
画扇形用Matrix计算坐标还是比较方便,分是否整圆,(整圆是画了两个半圆)
var r = Radius - (StrokeThickness / 2);
if (IsCircle())
var p0 = new Point(Center.X, Center.Y - r);
var p1 = new Point(Center.X, Center.Y + r);
var data = $"M {p0.X},{p0.Y} A {r} {r} 180 1 0 {p1.X},{p1.Y} A {r} {r} 180 1 0 {p0.X},{p0.Y}";
_MainPath.Data = Geometry.Parse(data);
var p0 = new Point(r + Center.X, Center.Y);
var matrix = new Matrix();
matrix.RotateAt(AngleStart, Center.X, Center.Y);
var p1 = matrix.Transform(p0);
matrix.RotateAt(AngleEnd - AngleStart, Center.X, Center.Y);
var p2 = matrix.Transform(p0);
var a = AngleEnd - AngleStart;
var b = a > 180 ? 1 : 0;
var data = $"M {Center.X},{Center.Y} L {p1.X},{p1.Y} A {r} {r} {a} {b} 1 {p2.X} {p2.Y} Z";
_MainPath.Data = Geometry.Parse(data);
}
完整代码如下
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
using Brush = System.Windows.Media.Brush;
using Point = System.Windows.Point;
namespace ArcSplitDemo.Controls
[TemplatePart(Name = "Part_MainPath", Type = typeof(Path))]
[TemplatePart(Name = "Part_ContentElement", Type = typeof(Control))]
public class ArcControl : ContentControl
Path _MainPath = null;
Control _ContentElement = null;
static ArcControl()
DefaultStyleKeyProperty.OverrideMetadata(typeof(ArcControl), new FrameworkPropertyMetadata(typeof(ArcControl)));
#region RotateContent DependencyProperty
public bool RotateContent
get { return (bool)GetValue(RotateContentProperty); }
set { SetValue(RotateContentProperty, value); }
public static readonly DependencyProperty RotateContentProperty =
DependencyProperty.Register("RotateContent", typeof(bool), typeof(ArcControl),
new PropertyMetadata(true, new PropertyChangedCallback(ArcControl.OnRotateContentPropertyChanged)));
private static void OnRotateContentPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
if (obj is ArcControl)
(obj as ArcControl).OnRotateContentValueChanged();
protected void OnRotateContentValueChanged()
ResetContentRender();
#endregion
#region ContentOffset DependencyProperty
public Point ContentOffset
get { return (Point)GetValue(ContentOffsetProperty); }
set { SetValue(ContentOffsetProperty, value); }
public static readonly DependencyProperty ContentOffsetProperty =
DependencyProperty.Register("ContentOffset", typeof(Point), typeof(ArcControl),
new PropertyMetadata(new Point(0, 0), new PropertyChangedCallback(ArcControl.OnContentOffsetPropertyChanged)));
private static void OnContentOffsetPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
if (obj is ArcControl)
(obj as ArcControl).OnContentOffsetValueChanged();
protected void OnContentOffsetValueChanged()
ResetContentRender();
#endregion
#region Center DependencyProperty
public System.Windows.Point Center
get { return (System.Windows.Point)GetValue(CenterProperty); }
set { SetValue(CenterProperty, value); }
public static readonly DependencyProperty CenterProperty =
DependencyProperty.Register("Center", typeof(System.Windows.Point), typeof(ArcControl),
new PropertyMetadata(new System.Windows.Point(0, 0), new PropertyChangedCallback(ArcControl.OnCenterPropertyChanged)));
private static void OnCenterPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
if (obj is ArcControl)
(obj as ArcControl).OnCenterValueChanged();
protected void OnCenterValueChanged()
ResetPath();
#endregion
#region Radius DependencyProperty
public double Radius
get { return (double)GetValue(RadiusProperty); }
set { SetValue(RadiusProperty, value); }
public static readonly DependencyProperty RadiusProperty =
DependencyProperty.Register("Radius", typeof(double), typeof(ArcControl),
new PropertyMetadata(50.0, new PropertyChangedCallback(ArcControl.OnRadiusPropertyChanged)));
private static void OnRadiusPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
if (obj is ArcControl)
(obj as ArcControl).OnRadiusValueChanged();
protected void OnRadiusValueChanged()
ResetPath();
#endregion
#region AngleStart DependencyProperty
public double AngleStart
get { return (double)GetValue(AngleStartProperty); }
set { SetValue(AngleStartProperty, value); }
public static readonly DependencyProperty AngleStartProperty =
DependencyProperty.Register("AngleStart", typeof(double), typeof(ArcControl),
new PropertyMetadata(0.0, new PropertyChangedCallback(ArcControl.OnAngleStartPropertyChanged)));
private static void OnAngleStartPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
if (obj is ArcControl)
(obj as ArcControl).OnAngleStartValueChanged();
protected void OnAngleStartValueChanged()
ResetPath();
#endregion
#region AngleEnd DependencyProperty
public double AngleEnd
get { return (double)GetValue(AngleEndProperty); }
set { SetValue(AngleEndProperty, value); }
public static readonly DependencyProperty AngleEndProperty =
DependencyProperty.Register("AngleEnd", typeof(double), typeof(ArcControl),
new PropertyMetadata(30.0, new PropertyChangedCallback(ArcControl.OnAngleEndPropertyChanged)));
private static void OnAngleEndPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
if (obj is ArcControl)
(obj as ArcControl).OnAngleEndValueChanged();
protected void OnAngleEndValueChanged()
ResetPath();
#endregion
#region Stroke DependencyProperty
public Brush Stroke
get { return (Brush)GetValue(StrokeProperty); }
set { SetValue(StrokeProperty, value); }
public static readonly DependencyProperty StrokeProperty =
DependencyProperty.Register("Stroke", typeof(Brush), typeof(ArcControl),
new PropertyMetadata(new SolidColorBrush(Colors.Black), new PropertyChangedCallback(ArcControl.OnStrokePropertyChanged)));
private static void OnStrokePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
if (obj is ArcControl)
(obj as ArcControl).OnStrokeValueChanged();
protected void OnStrokeValueChanged()
#endregion
#region StrokeThickness DependencyProperty
public double StrokeThickness
get { return (double)GetValue(StrokeThicknessProperty); }
set { SetValue(StrokeThicknessProperty, value); }
public static readonly DependencyProperty StrokeThicknessProperty =
DependencyProperty.Register("StrokeThickness", typeof(double), typeof(ArcControl),
new PropertyMetadata(1.0, new PropertyChangedCallback(ArcControl.OnStrokeThicknessPropertyChanged)));
private static void OnStrokeThicknessPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
if (obj is ArcControl)
(obj as ArcControl).OnStrokeThicknessValueChanged();
protected void OnStrokeThicknessValueChanged()
#endregion
#region StrokeDash DependencyProperty
public DoubleCollection StrokeDash
get { return (DoubleCollection)GetValue(StrokeDashProperty); }
set { SetValue(StrokeDashProperty, value); }
public static readonly DependencyProperty StrokeDashProperty =
DependencyProperty.Register("StrokeDash", typeof(DoubleCollection), typeof(ArcControl),
new PropertyMetadata(new DoubleCollection(), new PropertyChangedCallback(ArcControl.OnStrokeDashPropertyChanged)));
private static void OnStrokeDashPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
if (obj is ArcControl)
(obj as ArcControl).OnStrokeDashValueChanged();
protected void OnStrokeDashValueChanged()
#endregion
#region Fill DependencyProperty
public Brush Fill
get { return (Brush)GetValue(FillProperty); }
set { SetValue(FillProperty, value); }
public static readonly DependencyProperty FillProperty =
DependencyProperty.Register("Fill", typeof(Brush), typeof(ArcControl),
new PropertyMetadata(new SolidColorBrush(Colors.White), new PropertyChangedCallback(ArcControl.OnFillPropertyChanged)));
private static void OnFillPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
if (obj is ArcControl)
(obj as ArcControl).OnFillValueChanged();
protected void OnFillValueChanged()
#endregion
public override void OnApplyTemplate()
base.OnApplyTemplate();
_MainPath = base.GetTemplateChild("Part_MainPath") as Path;
_ContentElement = base.GetTemplateChild("Part_ContentElement") as Control;
if (_ContentElement != null)
_ContentElement.Loaded += _ContentElement_Loaded;
ResetPath();
private void _ContentElement_Loaded(object sender, RoutedEventArgs e)
ResetContentRender();
private void ResetContentRender()
if (_ContentElement != null && _ContentElement.ActualHeight > 0 && _ContentElement.ActualHeight > 0)
var h = _ContentElement.ActualHeight;
var w = _ContentElement.ActualWidth;
if (IsCircle())
_ContentElement.Margin = new Thickness(Center.X - w / 2, Center.Y - h / 2, 0, 0);
return;
var angle = (AngleStart + AngleEnd) / 2;
var matrix = new Matrix();
matrix.RotateAt(angle, Center.X, Center.Y);
Point center = new Point(Center.X + Radius / 2, Center.Y);
var isLeft = false;
if (angle > 270)
angle = angle - 360;
else if (angle < -90)
angle = angle + 360;
if (angle > -90 && angle <= 90)
center = matrix.Transform(new Point(center.X + ContentOffset.X, center.Y + ContentOffset.Y));
isLeft = true;
center = matrix.Transform(new Point(center.X - ContentOffset.X, center.Y - ContentOffset.Y));
_ContentElement.Margin = new Thickness(center.X - w / 2, center.Y - h / 2, 0, 0);
if (RotateContent)
_ContentElement.RenderTransformOrigin = new Point(0.5, 0.5);
_ContentElement.RenderTransform = new RotateTransform { Angle = isLeft ? (angle - 180) : angle };
_ContentElement.RenderTransformOrigin = new Point(0.5, 0.5);
_ContentElement.RenderTransform = null;
private bool IsCircle()
var a = AngleEnd - AngleStart;
return a == 0 || a >= 360;
private void ResetPath()
if (_MainPath == null)
return;
var r = Radius - (StrokeThickness / 2);
if (IsCircle())
var p0 = new Point(Center.X, Center.Y - r);
var p1 = new Point(Center.X, Center.Y + r);
var data = $"M {p0.X},{p0.Y} A {r} {r} 180 1 0 {p1.X},{p1.Y} A {r} {r} 180 1 0 {p0.X},{p0.Y}";
_MainPath.Data = Geometry.Parse(data);
var p0 = new Point(r + Center.X, Center.Y);
var matrix = new Matrix();
matrix.RotateAt(AngleStart, Center.X, Center.Y);
var p1 = matrix.Transform(p0);
matrix.RotateAt(AngleEnd - AngleStart, Center.X, Center.Y);
var p2 = matrix.Transform(p0);
var a = AngleEnd - AngleStart;
var b = a > 180 ? 1 : 0;
var data = $"M {Center.X},{Center.Y} L {p1.X},{p1.Y} A {r} {r} {a} {b} 1 {p2.X} {p2.Y} Z";
_MainPath.Data = Geometry.Parse(data);
if (ClipToBounds)
this.Clip = _MainPath.Data;
ResetContentRender();
样式文件
只是一个Path+ContentControl
<Style TargetType="{x:Type local:ArcControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ArcControl}">
<Path x:Name="Part_MainPath"
Fill="{TemplateBinding Fill}"
Stroke="{TemplateBinding Stroke}"
StrokeThickness="{TemplateBinding StrokeThickness}"
StrokeDashArray="{TemplateBinding StrokeDash}" />
<ContentControl x:Name="Part_ContentElement"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
排版部分
比较简单,按元素个数计算角度即可
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace ArcSplitDemo.Controls
public class ArcSplitPanel : Panel
List<ArcControl> _Items = null;
IEnumerable _ItemsSource = null;
private Brush[] _Fills = new Brush[]
new SolidColorBrush(Colors.Blue),
new SolidColorBrush(Colors.LightBlue),
new SolidColorBrush(Colors.Gray),
new SolidColorBrush(Colors.LightGray),
new SolidColorBrush(Colors.Green),
new SolidColorBrush(Colors.LightGray),
new SolidColorBrush(Colors.Yellow),
static ArcSplitPanel()
DefaultStyleKeyProperty.OverrideMetadata(typeof(ArcSplitPanel), new FrameworkPropertyMetadata(typeof(ArcSplitPanel)));
#region AngleStart DependencyProperty
public double AngleStart
get { return (double)GetValue(AngleStartProperty); }
set { SetValue(AngleStartProperty, value); }
public static readonly DependencyProperty AngleStartProperty =
DependencyProperty.Register("AngleStart", typeof(double), typeof(ArcSplitPanel),
new PropertyMetadata(-90.0, new PropertyChangedCallback(ArcSplitPanel.OnAngleStartPropertyChanged)));
private static void OnAngleStartPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
if (obj is ArcSplitPanel)
(obj as ArcSplitPanel).OnAngleStartValueChanged();
protected void OnAngleStartValueChanged()
InvalidateMeasure();
#endregion
#region FillColors DependencyProperty
public string FillColors
get { return (string)GetValue(FillColorsProperty); }
set { SetValue(FillColorsProperty, value); }
public static readonly DependencyProperty FillColorsProperty =
DependencyProperty.Register("FillColors", typeof(string), typeof(ArcSplitPanel),
new PropertyMetadata("", new PropertyChangedCallback(ArcSplitPanel.OnFillColorsPropertyChanged)));
private static void OnFillColorsPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
if (obj is ArcSplitPanel)
(obj as ArcSplitPanel).OnFillColorsValueChanged();
protected void OnFillColorsValueChanged()
if (!string.IsNullOrEmpty(FillColors))
var colors = FillColors.Split(new char[] { ',' }, System.StringSplitOptions.RemoveEmptyEntries)
.Select(c => int.Parse(c, System.Globalization.NumberStyles.HexNumber))
.Select(c => new SolidColorBrush(Color.FromArgb((byte)(c >> 24), (byte)(c << 8 >> 24), (byte)(c << 16 >> 24), (byte)(c << 24 >> 24))))
.ToArray();
InvalidateMeasure();
#endregion
#region ItemsSource DependencyProperty
public IEnumerable ItemsSource
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(ArcSplitPanel),
new PropertyMetadata(null, new PropertyChangedCallback(ArcSplitPanel.OnItemsSourcePropertyChanged)));
private static void OnItemsSourcePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
if (obj is ArcSplitPanel)
(obj as ArcSplitPanel).OnItemsSourceValueChanged();
protected void OnItemsSourceValueChanged()
if (_ItemsSource != null && _ItemsSource is INotifyCollectionChanged items)
items.CollectionChanged -= Items_CollectionChanged;
_ItemsSource = ItemsSource;
if (_ItemsSource != null && _ItemsSource is INotifyCollectionChanged newItems)
newItems.CollectionChanged += Items_CollectionChanged;
ResetItems();
private void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
ResetItems();
#endregion
#region ItemTemplate DependencyProperty
public DataTemplate ItemTemplate
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
public static readonly DependencyProperty ItemTemplateProperty =
DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(ArcSplitPanel),
new PropertyMetadata(null, new PropertyChangedCallback(ArcSplitPanel.OnItemTemplatePropertyChanged)));
private static void OnItemTemplatePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
if (obj is ArcSplitPanel)
(obj as ArcSplitPanel).OnItemTemplateValueChanged();
protected void OnItemTemplateValueChanged()
#endregion
#region ItemStyle DependencyProperty
public Style ItemStyle
get { return (Style)GetValue(ItemStyleProperty); }
set { SetValue(ItemStyleProperty, value); }
public static readonly DependencyProperty ItemStyleProperty =
DependencyProperty.Register("ItemStyle", typeof(Style), typeof(ArcSplitPanel),
new PropertyMetadata(null, new PropertyChangedCallback(ArcSplitPanel.OnItemStylePropertyChanged)));
private static void OnItemStylePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
if (obj is ArcSplitPanel)
(obj as ArcSplitPanel).OnItemStyleValueChanged();
protected void OnItemStyleValueChanged()
#endregion
#region 重设元素
private void ResetItems()
var newItems = new List<ArcControl>();
_Items = newItems;
var items = new List<ArcControl>();
foreach (var c in Children)
if (c is ArcControl a)
items.Add(a);
if (_ItemsSource == null)
foreach (var item in items)
this.Children.Remove(item);
return;
var arcItems = items.ToDictionary(d => d.Content);
foreach (var data in _ItemsSource)
if (arcItems.TryGetValue(data, out var control))
arcItems.Remove(data);
newItems.Add(control);
continue;
control = new ArcControl();
control.DataContext = data;
control.Content = data;
control.SetBinding(ArcControl.StyleProperty, new Binding("ItemStyle") { Source = this });
control.SetBinding(ArcControl.ContentTemplateProperty, new Binding("ItemTemplate") { Source = this });
newItems.Add(control);
this.Children.Add(control);
if (arcItems.Count > 0)
foreach (var a in arcItems)
this.Children.Remove(a.Value);
InvalidateMeasure();
#endregion
protected override Size MeasureOverride(Size availableSize)
var h = availableSize.Height;
var w = availableSize.Width;
if (double.IsNaN(h) || double.IsInfinity(h))
h = 0d;
if (double.IsNaN(w) || double.IsInfinity(w))
w = 0d;
var size = new Size(w, h);
if (w == 0d || h == 0d)
else if (_Items != null && _Items.Count > 0)
var count = _Items.Count;
if (count > 0)
var center = new Point(w / 2, h / 2);
var radius = w > h ? h / 2 : w / 2;
var start = AngleStart;
var angle = 360.0 / count;
for (var i = 0; i < count; i++)
var item = _Items[i];
item.AngleStart = start;
item.AngleEnd = start + angle;
start += angle;
if (_Fills != null && _Fills.Length > i)
var fill = _Fills[i];
item.Fill = fill;
item.Fill = new SolidColorBrush(Colors.Transparent);
item.Center = center;
item.Radius = radius;
item.Measure(size);
return size;
protected override Size ArrangeOverride(Size finalSize)
var h = finalSize.Height;
var w = finalSize.Width;
if (double.IsNaN(h) || double.IsInfinity(h))
h = 0d;
if (double.IsNaN(w) || double.IsInfinity(w))
w = 0d;
if (w == 0d || h == 0d)
else if (_Items != null && _Items.Count > 0)
foreach (var item in _Items)
item.Arrange(new Rect(0, 0, w, h));