Circular Layout Panel

I’ve been given an opportunity to write a custom WPF layout panel. This is something that I’ve been wanting to try for ages but have never really had the need. Rather than jumping straight in to some potentially complex layout algorithms, I figured that I’d start with something trivial just to get the hang of things. Hence the CircularPanel was born.

The CircularPanel is a simple Panel derivative that lays out its children in a circular arrangement. It has some useful dependency properties to allow some customization.

  • StartAngle – Angle in degrees at which the first child element will be positioned.
  • EndAngle – Angle in degrees at which the last child element will be positioned. If the EndAngle forms a complete circle, e.g. Math.Abs(StartAngle-EndAngle) > 360 then rather than positioning the last element on top of the first the element an angle will be included between first and last elements.
  • Padding – As per standard – reduces the space within the panel used to layout the child elements.

When adding these properties I originally missed the AffectsArrange flag on the property registration’s FrameworkElementMetaData. Without this flag changing the property (for instance in the designer) would not cause the control to re-render.

I also added some attached dependency properties. These properties can be used by the child UI elements to dictate an override to the default behaviour.

  • FixedAngle – Overrides the default automatic assignment of angle based on child index. Instead the child element is placed at the fixed angle specified in degrees.
  • RadiusScaleX – Allows the radius to be scaled in the X direction for this child element – defaults to 1.
  • RadiusScaleY – Allows the radius to be scaled in the Y direction for this child element – defaults to 1.

When adding these attached properties I made sure I included the AffectsArrange flag. Of course this wasn’t right – changing the attached property requires the parent to re-calculate the arrangement, it doesn’t affect the applied elements arrangement. I updated the flag to AffectsParentArrange and all was good.

Now I’m not sure how much real-world value this panel has – but I did get the combination of these properties to produce some interesting affects. For example:

Set StartAngle = 0, EndAngle = 1080 and then have each element decrease the RadiusScaleX/Y via binding. This produces a nice sprial.

CircularPanel - Spiral

Add 12 auto-placed elements, then three more using FixedAngle combined with an animation to produce a clock with hour, minute and second hands.

CircularPanel - Clock

Use an animation over StartAngle and EndAngle to produce an effect similar to opening a fan.

CircularPanel - Fan

Here’s the interesting code from the ArrangeOverride method on CircularPanel.

protected override Size ArrangeOverride(Size finalSize)
{
int numberOfVisibleChildren = InternalChildren
.OfType<UIElement>()
.Count(u => u.Visibility != Visibility.Collapsed
&& !GetFixedAngle(u).HasValue); if (numberOfVisibleChildren == 0) return finalSize;
// Short circuit if there are no children int currentChildPosition = 0; double startArcAngle = StartAngle / 180 * Math.PI; double endArcAngle = EndAngle / 180 * Math.PI; double arcDelta; if ( Math.Abs(startArcAngle-endArcAngle) >= Math.PI * 2 ) // If we have a full circle then don't end the last element on the
// EndAngle because that would overlay the StartAngle.
arcDelta = (endArcAngle - startArcAngle ) / (double) numberOfVisibleChildren;
else // If we have less than a full circle then make sure we spread the
// elements with first and last on the start and end angles.
arcDelta = (endArcAngle - startArcAngle) / ((double)numberOfVisibleChildren - 1); double maxChildWidth = InternalChildren
.OfType<UIElement>()
.Max( u => u.DesiredSize.Width ); double maxChildHeight = InternalChildren
.OfType<UIElement>()
.Max( u => u.DesiredSize.Height ); double radiusX = ( finalSize.Width - Padding.Left - Padding.Right - maxChildWidth ) / 2; double radiusY = ( finalSize.Height - Padding.Top - Padding.Bottom - maxChildHeight ) / 2; Point midPoint = new Point( radiusX + Padding.Left + maxChildWidth / 2,
radiusY + Padding.Top + maxChildHeight / 2); foreach (UIElement child in InternalChildren) { var childAngle = startArcAngle + arcDelta * currentChildPosition; double? fixedAngle = GetFixedAngle(child); if (fixedAngle.HasValue) childAngle = fixedAngle.Value / 180 * Math.PI; double x = Math.Cos( childAngle ) * radiusX * GetRadiusScaleX(child) +
midPoint.X - child.DesiredSize.Width / 2; double y = Math.Sin( childAngle ) * radiusY * GetRadiusScaleY(child) +
midPoint.Y - child.DesiredSize.Height / 2; child.Arrange(new Rect(new Point(x, y), child.DesiredSize));
// Ignore collapsed children and FixedAngle children. if ( child.Visibility != Visibility.Collapsed && !fixedAngle.HasValue ) currentChildPosition++; } return finalSize; }

Source code with simple sample application here. It’s a VS2010 solution/project but should be easy to converted to VS2008 – just remove the EasingFunctions in the sample app XAML.

3 thoughts on “Circular Layout Panel”

  1. Hi Spencen, nice layout! But I can’t run the code!, I get an error, apparently the project PanelTestXbap can’t be found. Could you please fix it?

Comments are closed.