using System.Windows.Controls; using System.Windows.Shapes; using Point = System.Windows.Point; using Size = System.Windows.Size; namespace WPFluent.Controls; /// /// Control that draws a symmetrical arc with rounded edges. /// /// /// /// <ui:Arc /// EndAngle="359" /// StartAngle="0" /// Stroke="{ui:ThemeResource /// SystemAccentColorSecondaryBrush}" /// StrokeThickness="2" /// Visibility="Visible" /> /// /// public class Arc : Shape { /// /// Identifies the dependency property. /// public static readonly DependencyProperty EndAngleProperty = DependencyProperty.Register( nameof(EndAngle), typeof(double), typeof(Arc), new PropertyMetadata(0.0d, PropertyChangedCallback)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty StartAngleProperty = DependencyProperty.Register( nameof(StartAngle), typeof(double), typeof(Arc), new PropertyMetadata(0.0d, PropertyChangedCallback)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty SweepDirectionProperty = DependencyProperty.Register( nameof(SweepDirection), typeof(SweepDirection), typeof(Arc), new PropertyMetadata(SweepDirection.Clockwise, PropertyChangedCallback)); private Viewbox _rootLayout; static Arc() { // Modify the metadata of the StrokeStartLineCap dependency property. StrokeStartLineCapProperty.OverrideMetadata( typeof(Arc), new FrameworkPropertyMetadata(PenLineCap.Round, PropertyChangedCallback)); } private void EnsureRootLayout() { if (_rootLayout != null) { return; } _rootLayout = new Viewbox { SnapsToDevicePixels = true }; AddVisualChild(_rootLayout); } protected override Size ArrangeOverride(Size finalSize) { EnsureRootLayout(); _rootLayout!.Arrange(new Rect(default, finalSize)); return finalSize; } /// /// Get the geometry that defines this shape. Based /// on Mark Feldman implementation. /// protected Geometry DefinedGeometry() { var geometryStream = new StreamGeometry(); var arcSize = new Size( Math.Max(0, (RenderSize.Width - StrokeThickness) / 2), Math.Max(0, (RenderSize.Height - StrokeThickness) / 2)); using var context = geometryStream.Open(); context.BeginFigure(PointAtAngle(Math.Min(StartAngle, EndAngle)), false, false); context.ArcTo(PointAtAngle(Math.Max(StartAngle, EndAngle)), arcSize, 0, IsLargeArc, SweepDirection, true, false); geometryStream.Transform = new TranslateTransform(StrokeThickness / 2, StrokeThickness / 2); return geometryStream; } protected override Visual GetVisualChild(int index) { if (index != 0) { throw new ArgumentOutOfRangeException(nameof(index), "Arc should have only 1 child"); } EnsureRootLayout(); return _rootLayout; } protected override Size MeasureOverride(Size availableSize) { EnsureRootLayout(); _rootLayout!.Measure(availableSize); return _rootLayout.DesiredSize; } /// /// Overrides the default OnRender method to draw the element. /// /// A object that is drawn during the rendering pass of this . protected override void OnRender(DrawingContext drawingContext) { base.OnRender(drawingContext); Pen pen = new(Stroke, StrokeThickness) { StartLineCap = StrokeStartLineCap, EndLineCap = StrokeStartLineCap, }; drawingContext.DrawGeometry(Stroke, pen, DefinedGeometry()); } /// /// Draws a point on the coordinates of the given angle. Based on Mark Feldman implementation. /// /// The angle at which to create the point. protected Point PointAtAngle(double angle) { if (SweepDirection == SweepDirection.Counterclockwise) { angle += 90; angle %= 360; if (angle < 0) { angle += 360; } var radAngle = angle * (Math.PI / 180); var xRadius = (RenderSize.Width - StrokeThickness) / 2; var yRadius = (RenderSize.Height - StrokeThickness) / 2; return new Point(xRadius + xRadius * Math.Cos(radAngle), yRadius - yRadius * Math.Sin(radAngle)); } else { angle -= 90; angle %= 360; if (angle < 0) { angle += 360; } var radAngle = angle * (Math.PI / 180); var xRadius = (RenderSize.Width - StrokeThickness) / 2; var yRadius = (RenderSize.Height - StrokeThickness) / 2; return new Point(xRadius + xRadius * Math.Cos(-radAngle), yRadius - yRadius * Math.Sin(-radAngle)); } } /// /// Event triggered when one of the key parameters is changed. Forces the geometry to be redrawn. /// protected static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not Arc control) { return; } control.IsLargeArc = Math.Abs(control.EndAngle - control.StartAngle) > 180; control.InvalidateVisual(); } /// protected override Geometry DefiningGeometry => DefinedGeometry(); /// /// Gets or sets the final angle from which the arc will be drawn. /// public double EndAngle { get => (double)GetValue(EndAngleProperty); set => SetValue(EndAngleProperty, value); } /// /// Gets a value indicating whether one of the two larger arc sweeps is chosen; otherwise, if is , one of the smaller arc sweeps is chosen. /// public bool IsLargeArc { get; internal set; } = false; /// /// Gets or sets the initial angle from which the arc will be drawn. /// public double StartAngle { get => (double)GetValue(StartAngleProperty); set => SetValue(StartAngleProperty, value); } /// /// Gets or sets the direction to where the arc will be drawn. /// public SweepDirection SweepDirection { get => (SweepDirection)GetValue(SweepDirectionProperty); set => SetValue(SweepDirectionProperty, value); } }