/************************************************************************************* Extended WPF Toolkit Copyright (C) 2007-2013 Xceed Software Inc. This program is provided to you under the terms of the Microsoft Public License (Ms-PL) as published at http://wpftoolkit.codeplex.com/license For more features, controls, and fast professional support, pick up the Plus Edition at http://xceed.com/wpf_toolkit Stay informed: follow @datagrid on Twitter or Like http://facebook.com/datagrids ***********************************************************************************/ using System; using System.Collections.Specialized; using System.Windows; using System.Windows.Media; using Xceed.Wpf.Toolkit.Primitives; using Xceed.Wpf.Toolkit.Core; using Xceed.Wpf.Toolkit.Core.Utilities; namespace Xceed.Wpf.Toolkit { public sealed class Pie : ShapeBase { #region Constructors static Pie() { DefaultStyleKeyProperty.OverrideMetadata( typeof( Pie ), new FrameworkPropertyMetadata( typeof( Pie ) ) ); // The default stretch mode of Pie is Fill Pie.StretchProperty.OverrideMetadata( typeof( Pie ), new FrameworkPropertyMetadata( Stretch.Fill ) ); Pie.StrokeLineJoinProperty.OverrideMetadata( typeof( Pie ), new FrameworkPropertyMetadata( PenLineJoin.Round ) ); } public Pie() : base() { } #endregion #region EndAngle Property public static readonly DependencyProperty EndAngleProperty = DependencyProperty.Register( "EndAngle", typeof( double ), typeof( Pie ), new FrameworkPropertyMetadata( 360d, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback( Pie.OnEndAngleChanged ), new CoerceValueCallback( Pie.CoerceEndAngleValue ) ) ); public double EndAngle { get { return ( double )this.GetValue( Pie.EndAngleProperty ); } set { this.SetValue( Pie.EndAngleProperty, value ); } } private static void OnEndAngleChanged( DependencyObject d, DependencyPropertyChangedEventArgs e ) { ( ( Pie )d ).OnEndAngleChanged( e ); } private void OnEndAngleChanged( DependencyPropertyChangedEventArgs e ) { // avoid re-entrancy if( this.IsUpdatingEndAngle ) return; if( !( this.IsUpdatingStartAngle || this.IsUpdatingSlice || this.IsUpdatingSweepDirection ) ) { switch( this.Mode ) { case PieMode.Slice: throw new InvalidOperationException( ErrorMessages.GetMessage( "EndAngleCannotBeSetDirectlyInSlice" ) ); } } // EndAngle, Slice, and SweepDirection are interrelated and must be kept in sync this.IsUpdatingEndAngle = true; try { if( this.Mode == PieMode.EndAngle ) { this.CoerceValue( Pie.SweepDirectionProperty ); } this.CoerceValue( Pie.SliceProperty ); } finally { this.IsUpdatingEndAngle = false; } } private static object CoerceEndAngleValue( DependencyObject d, object value ) { // keep EndAngle in sync with Slice and SweepDirection Pie pie = ( Pie )d; if( pie.IsUpdatingSlice || pie.IsUpdatingSweepDirection || ( pie.IsUpdatingStartAngle && pie.Mode == PieMode.Slice ) ) { double newValue = pie.StartAngle + ( ( pie.SweepDirection == SweepDirection.Clockwise ) ? 1.0 : -1.0 ) * pie.Slice * 360; if( !DoubleHelper.AreVirtuallyEqual( ( double )value, newValue ) ) { value = newValue; } } return value; } #endregion #region Mode Property public static readonly DependencyProperty ModeProperty = DependencyProperty.Register( "Mode", typeof( PieMode ), typeof( Pie ), new FrameworkPropertyMetadata( PieMode.Manual, new PropertyChangedCallback( Pie.OnModeChanged ) ) ); public PieMode Mode { get { return ( PieMode )this.GetValue( Pie.ModeProperty ); } set { this.SetValue( Pie.ModeProperty, value ); } } private static void OnModeChanged( DependencyObject d, DependencyPropertyChangedEventArgs e ) { ( ( Pie )d ).OnModeChanged( e ); } private void OnModeChanged( DependencyPropertyChangedEventArgs e ) { // disallow reentrancy if( this.IsUpdatingMode ) return; this.IsUpdatingMode = true; try { if( this.Mode == PieMode.EndAngle ) { this.CoerceValue( Pie.SweepDirectionProperty ); } } finally { this.IsUpdatingMode = false; } } #endregion #region Slice Property public static readonly DependencyProperty SliceProperty = DependencyProperty.Register( "Slice", typeof( double ), typeof( Pie ), new FrameworkPropertyMetadata( 1d, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback( Pie.OnSliceChanged ), new CoerceValueCallback( Pie.CoerceSliceValue ) ), new ValidateValueCallback( Pie.ValidateSlice ) ); public double Slice { get { return ( double )this.GetValue( Pie.SliceProperty ); } set { this.SetValue( Pie.SliceProperty, value ); } } private static void OnSliceChanged( DependencyObject d, DependencyPropertyChangedEventArgs e ) { ( ( Pie )d ).OnSliceChanged( e ); } private void OnSliceChanged( DependencyPropertyChangedEventArgs e ) { // avoid re-entrancy if( this.IsUpdatingSlice ) return; if( !( this.IsUpdatingStartAngle || this.IsUpdatingEndAngle || this.IsUpdatingSweepDirection ) ) { if( this.Mode == PieMode.EndAngle ) throw new InvalidOperationException( ErrorMessages.GetMessage( "SliceCannotBeSetDirectlyInEndAngle" ) ); } // EndAngle and Slice are interrelated and must be kept in sync this.IsUpdatingSlice = true; try { if( !( this.IsUpdatingStartAngle || this.IsUpdatingEndAngle || ( this.Mode == PieMode.Manual && this.IsUpdatingSweepDirection ) ) ) { this.CoerceValue( Pie.EndAngleProperty ); } } finally { this.IsUpdatingSlice = false; } } private static object CoerceSliceValue( DependencyObject d, object value ) { // keep Slice in sync with EndAngle, StartAngle, and SweepDirection Pie pie = ( Pie )d; if( pie.IsUpdatingEndAngle || pie.IsUpdatingStartAngle || pie.IsUpdatingSweepDirection ) { double slice = Math.Max( -360.0, Math.Min( 360.0, ( pie.EndAngle - pie.StartAngle ) ) ) / ( ( pie.SweepDirection == SweepDirection.Clockwise ) ? 360.0 : -360.0 ); double newValue = DoubleHelper.AreVirtuallyEqual( slice, 0 ) ? 0 : ( slice < 0 ) ? slice + 1 : slice; if( !DoubleHelper.AreVirtuallyEqual( ( double )value, newValue ) ) { value = newValue; } } return value; } private static bool ValidateSlice( object value ) { double newValue = ( double )value; if( newValue < 0 || newValue > 1 || DoubleHelper.IsNaN( newValue ) ) throw new ArgumentException( ErrorMessages.GetMessage( "SliceOOR" ) ); return true; } #endregion #region StartAngle Property public static readonly DependencyProperty StartAngleProperty = DependencyProperty.Register( "StartAngle", typeof( double ), typeof( Pie ), new FrameworkPropertyMetadata( 360d, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback( Pie.OnStartAngleChanged ) ) ); public double StartAngle { get { return ( double )this.GetValue( Pie.StartAngleProperty ); } set { this.SetValue( Pie.StartAngleProperty, value ); } } private static void OnStartAngleChanged( DependencyObject d, DependencyPropertyChangedEventArgs e ) { ( ( Pie )d ).OnStartAngleChanged( e ); } private void OnStartAngleChanged( DependencyPropertyChangedEventArgs e ) { // avoid re-entrancy if( this.IsUpdatingStartAngle ) return; // StartAngle, Slice, and SweepDirection are interrelated and must be kept in sync this.IsUpdatingStartAngle = true; try { switch( Mode ) { case PieMode.Manual: this.CoerceValue( Pie.SliceProperty ); break; case PieMode.EndAngle: this.CoerceValue( Pie.SweepDirectionProperty ); this.CoerceValue( Pie.SliceProperty ); break; case PieMode.Slice: this.CoerceValue( Pie.EndAngleProperty ); break; } } finally { this.IsUpdatingStartAngle = false; } } #endregion #region SweepDirection Property public static readonly DependencyProperty SweepDirectionProperty = DependencyProperty.Register( "SweepDirection", typeof( SweepDirection ), typeof( Pie ), new FrameworkPropertyMetadata( ( SweepDirection )SweepDirection.Clockwise, FrameworkPropertyMetadataOptions.AffectsRender, new PropertyChangedCallback( Pie.OnSweepDirectionChanged ), new CoerceValueCallback( Pie.CoerceSweepDirectionValue ) ) ); public SweepDirection SweepDirection { get { return ( SweepDirection )this.GetValue( Pie.SweepDirectionProperty ); } set { this.SetValue( Pie.SweepDirectionProperty, value ); } } private static void OnSweepDirectionChanged( DependencyObject d, DependencyPropertyChangedEventArgs e ) { ( ( Pie )d ).OnSweepDirectionChanged( e ); } private void OnSweepDirectionChanged( DependencyPropertyChangedEventArgs e ) { // avoid re-entrancy if( this.IsUpdatingSweepDirection ) return; // EndAngle, Slice, and SweepDirection are interrelated and must be kept in sync this.IsUpdatingSweepDirection = true; try { switch( Mode ) { case PieMode.Slice: this.CoerceValue( Pie.EndAngleProperty ); break; default: this.CoerceValue( Pie.SliceProperty ); break; } } finally { this.IsUpdatingSweepDirection = false; } } private static object CoerceSweepDirectionValue( DependencyObject d, object value ) { // keep SweepDirection in sync with EndAngle and StartAngle Pie pie = ( Pie )d; if( pie.IsUpdatingEndAngle || pie.IsUpdatingStartAngle || pie.IsUpdatingMode ) { if( DoubleHelper.AreVirtuallyEqual( pie.StartAngle, pie.EndAngle ) ) { // if the values are equal, use previously coerced value value = pie.SweepDirection; } else { value = ( pie.EndAngle < pie.StartAngle ) ? SweepDirection.Counterclockwise : SweepDirection.Clockwise; } } return value; } #endregion #region GeometryTransform Property public override Transform GeometryTransform { get { return Transform.Identity; } } #endregion #region RenderedGeometry Property public override Geometry RenderedGeometry { get { // for a Pie, the RenderedGeometry is the same as the DefiningGeometry return this.DefiningGeometry; } } #endregion #region DefiningGeometry Protected Property protected override Geometry DefiningGeometry { get { double slice = Slice; if( _rect.IsEmpty || slice <= 0 ) return Geometry.Empty; if( slice >= 1 ) return new EllipseGeometry( _rect ); // direction of flow is determined by the SweepDirection property double directionalFactor = ( this.SweepDirection == SweepDirection.Clockwise ) ? 1.0 : -1.0; double startAngle = StartAngle; Point pointA = EllipseHelper.PointOfRadialIntersection( _rect, startAngle ); Point pointB = EllipseHelper.PointOfRadialIntersection( _rect, startAngle + directionalFactor * slice * 360 ); PathSegmentCollection segments = new PathSegmentCollection(); segments.Add( new LineSegment( pointA, true ) ); ArcSegment arc = new ArcSegment(); arc.Point = pointB; arc.Size = new Size( _rect.Width / 2, _rect.Height / 2 ); arc.IsLargeArc = slice > 0.5; arc.SweepDirection = SweepDirection; segments.Add( arc ); PathFigureCollection figures = new PathFigureCollection(); figures.Add( new PathFigure( RectHelper.Center( _rect ), segments, true ) ); return new PathGeometry( figures ); } } #endregion #region IsUpdatingEndAngle Private Property private bool IsUpdatingEndAngle { get { return _cacheBits[ ( int )CacheBits.IsUpdatingEndAngle ]; } set { _cacheBits[ ( int )CacheBits.IsUpdatingEndAngle ] = value; } } #endregion #region IsUpdatingMode Private Property private bool IsUpdatingMode { get { return _cacheBits[ ( int )CacheBits.IsUpdatingMode ]; } set { _cacheBits[ ( int )CacheBits.IsUpdatingMode ] = value; } } #endregion #region IsUpdatingSlice Private Property private bool IsUpdatingSlice { get { return _cacheBits[ ( int )CacheBits.IsUpdatingSlice ]; } set { _cacheBits[ ( int )CacheBits.IsUpdatingSlice ] = value; } } #endregion #region IsUpdatingStartAngle Private Property private bool IsUpdatingStartAngle { get { return _cacheBits[ ( int )CacheBits.IsUpdatingStartAngle ]; } set { _cacheBits[ ( int )CacheBits.IsUpdatingStartAngle ] = value; } } #endregion #region IsUpdatingSweepDirection Private Property private bool IsUpdatingSweepDirection { get { return _cacheBits[ ( int )CacheBits.IsUpdatingSweepDirection ]; } set { _cacheBits[ ( int )CacheBits.IsUpdatingSweepDirection ] = value; } } #endregion internal override Size GetNaturalSize() { double strokeThickness = this.GetStrokeThickness(); return new Size( strokeThickness, strokeThickness ); } internal override Rect GetDefiningGeometryBounds() { return _rect; } protected override Size ArrangeOverride( Size finalSize ) { double penThickness = this.GetStrokeThickness(); double margin = penThickness / 2; _rect = new Rect( margin, margin, Math.Max( 0, finalSize.Width - penThickness ), Math.Max( 0, finalSize.Height - penThickness ) ); switch( Stretch ) { case Stretch.None: // empty rectangle _rect.Width = _rect.Height = 0; break; case Stretch.Fill: // already initialized for Fill break; case Stretch.Uniform: // largest square that fits in the final size if( _rect.Width > _rect.Height ) { _rect.Width = _rect.Height; } else { _rect.Height = _rect.Width; } break; case Stretch.UniformToFill: // smallest square that fills the final size if( _rect.Width < _rect.Height ) { _rect.Width = _rect.Height; } else { _rect.Height = _rect.Width; } break; } return finalSize; } protected override Size MeasureOverride( Size constraint ) { if( this.Stretch == Stretch.UniformToFill ) { double width = constraint.Width; double height = constraint.Height; if( Double.IsInfinity( width ) && Double.IsInfinity( height ) ) { return this.GetNaturalSize(); } else if( Double.IsInfinity( width ) || Double.IsInfinity( height ) ) { width = Math.Min( width, height ); } else { width = Math.Max( width, height ); } return new Size( width, width ); } return this.GetNaturalSize(); } protected override void OnRender( DrawingContext drawingContext ) { if( !_rect.IsEmpty ) { Pen pen = this.GetPen(); drawingContext.DrawGeometry( this.Fill, pen, this.RenderedGeometry ); } } #region Private Fields private Rect _rect = Rect.Empty; private BitVector32 _cacheBits = new BitVector32( 0 ); #endregion #region CacheBits Nested Type private enum CacheBits { IsUpdatingEndAngle = 0x00000001, IsUpdatingMode = 0x00000002, IsUpdatingSlice = 0x00000004, IsUpdatingStartAngle = 0x00000008, IsUpdatingSweepDirection = 0x00000010, } #endregion } }