一个扇形分割排版控件

先上结果

等角分割,文字以扇形中线显示 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));