Files
ShrlAlgoToolkit/WPFluent/Controls/Arc/Arc.cs
ShrlAlgo 4d35cadb56 更新
2025-07-11 09:20:23 +08:00

214 lines
7.0 KiB
C#

using System.Windows.Controls;
using System.Windows.Shapes;
using Point = System.Windows.Point;
using Size = System.Windows.Size;
namespace WPFluent.Controls;
/// <summary>
/// Control that draws a symmetrical arc with rounded edges.
/// </summary>
/// <example>
/// <code lang="xml"> /// &lt;ui:Arc /// EndAngle="359" /// StartAngle="0" /// Stroke="{ui:ThemeResource
/// SystemFillColorAttentionBrush}" /// StrokeThickness="2" /// Visibility="Visible" /&gt; ///</code>
/// </example>
public class Arc : Shape
{
/// <summary>
/// Identifies the <see cref="EndAngle"/> dependency property.
/// </summary>
public static readonly DependencyProperty EndAngleProperty = DependencyProperty.Register(
nameof(EndAngle),
typeof(double),
typeof(Arc),
new PropertyMetadata(0.0d, PropertyChangedCallback));
/// <summary>
/// Identifies the <see cref="StartAngle"/> dependency property.
/// </summary>
public static readonly DependencyProperty StartAngleProperty = DependencyProperty.Register(
nameof(StartAngle),
typeof(double),
typeof(Arc),
new PropertyMetadata(0.0d, PropertyChangedCallback));
/// <summary>
/// Identifies the <see cref="SweepDirection"/> dependency property.
/// </summary>
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;
}
/// <summary>
/// Get the geometry that defines this shape. <para><see href="https://stackoverflow.com/a/36756365/13224348">Based
/// on Mark Feldman implementation.</see></para>
/// </summary>
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;
}
/// <summary>
/// Overrides the default OnRender method to draw the <see cref="Arc"/> element.
/// </summary>
/// <param name="drawingContext">A <see cref="DrawingContext"/> object that is drawn during the rendering pass of this <see cref="Shape"/>.</param>
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
Pen pen = new(Stroke, StrokeThickness) { StartLineCap = StrokeStartLineCap, EndLineCap = StrokeStartLineCap, };
drawingContext.DrawGeometry(Stroke, pen, DefinedGeometry());
}
/// <summary>
/// Draws a point on the coordinates of the given angle. <para><see
/// href="https://stackoverflow.com/a/36756365/13224348">Based on Mark Feldman implementation.</see></para>
/// </summary>
/// <param name="angle">The angle at which to create the point.</param>
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));
}
}
/// <summary>
/// Event triggered when one of the key parameters is changed. Forces the geometry to be redrawn.
/// </summary>
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();
}
/// <inheritdoc/>
protected override Geometry DefiningGeometry => DefinedGeometry();
/// <summary>
/// Gets or sets the final angle from which the arc will be drawn.
/// </summary>
public double EndAngle { get => (double)GetValue(EndAngleProperty); set => SetValue(EndAngleProperty, value); }
/// <summary>
/// Gets a value indicating whether one of the two larger arc sweeps is chosen; otherwise, if is <see
/// langword="false"/>, one of the smaller arc sweeps is chosen.
/// </summary>
public bool IsLargeArc { get; internal set; } = false;
/// <summary>
/// Gets or sets the initial angle from which the arc will be drawn.
/// </summary>
public double StartAngle
{
get => (double)GetValue(StartAngleProperty);
set => SetValue(StartAngleProperty, value);
}
/// <summary>
/// Gets or sets the direction to where the arc will be drawn.
/// </summary>
public SweepDirection SweepDirection
{
get => (SweepDirection)GetValue(SweepDirectionProperty);
set => SetValue(SweepDirectionProperty, value);
}
}