What is the Radial Panel?
In WPF and Silverlight, it is possible to create custom panels by inheriting directly from the base Panel class. Custom panels arrange their children by overriding the MeasureOverride and ArrangeOverride methods to create new ways to arrange items on screen. The radial panel is a custom panel whose purpose is to arrange items within a certain radius of the center of the panel, each of the items is rotated at a certain angle to resemble a circle. There are several incarnations of the radial panel already on the web, some of them dating back to 2005:
Why Another Radial Panel?
With all these options available, at the beginning we thought it was a matter of choosing one and building our project. However, ASD required a Silverlight panel that enabled an arrangement of items that could be animated around the center, and unfortunately, the solutions available for Silverlight did not seem to support animations. Hence, the project.
Project Requirements
- A Silverlight panel that arranges items around the center, rotating them at a certain angle and at a certain distance from the center.
- Each item should be able to set its own angle and radius.
- It is necessary to attach animations to each of the items independently.
Implementation
Start by declaring the panel class, inheriting from Panel:
public class RadialPanel : Panel
{
}
Then, it is necessary to declare a series of attached properties, that are used by the children of the panel to inform their angle and radius. This is the implementation of the Angle property:
public static readonly DependencyProperty AngleProperty =
DependencyProperty.RegisterAttached(
"Angle",
typeof(Double),
typeof(RadialPanel),
new PropertyMetadata(0.0, new PropertyChangedCallback(OnAnglePropertyChanged))
);
public static void SetAngle(UIElement element, Double value)
{
element.SetValue(AngleProperty, value);
}
public static Double GetAngle(UIElement element)
{
return (Double)element.GetValue(AngleProperty);
}
private static void OnAnglePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) {
}
Notice that even though we have declared a property changed handler, it does not have an implementation. It is in this handler that the animation capabilities of the property are implemented and therefore, this method is necessary; however the handler code will be added later in the project as it depends on operations that are not implemented yet.
Next, the Radius property is implemented in a similar way:
public static readonly DependencyProperty RadiusProperty = DependencyProperty.RegisterAttached(
"Radius",
typeof(Double),
typeof(RadialPanel),
new PropertyMetadata(0.0, new PropertyChangedCallback(OnRadiusPropertyChanged))
);
public static void SetRadius(UIElement element, Double value)
{
element.SetValue(RadiusProperty, value);
}
public static Double GetRadius(UIElement element)
{
return (Double)element.GetValue(RadiusProperty);
}
private static void OnRadiusPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) {
}
Just like with the Angle property, the callback handler for the Radius property is not implemented yet. Additionally, it is necessary to declare a private attached property called Center, that will be used internally by the panel:
private static readonly DependencyProperty CenterProperty = DependencyProperty.RegisterAttached(
"Center",
typeof(Point),
typeof(RadialPanel),
new PropertyMetadata(new Point(), null)
);
private static void SetCenter(UIElement element, Point value)
{
element.SetValue(CenterProperty, value);
}
private static Point GetCenter(UIElement element)
{
return (Point)element.GetValue(CenterProperty);
}
The center property does not require a property changed callback handler.
With the properties in place, it is time to create an auxiliary method whose purpose is to arrange an item that lives inside the panel. This method encapsulates the logic related with the arrangement of the radial panel and will be used by other members of the class:
private static void ArrangeChild(UIElement child)
{
child.RenderTransform = new RotateTransform();
RotateTransform rotateTransform =
child.RenderTransform as RotateTransform;
rotateTransform.Angle = GetAngle(child);
rotateTransform.CenterX =
child.DesiredSize.Width / 2;
rotateTransform.CenterY = -GetRadius(child);
child.Arrange(new Rect(
new Point(GetCenter(child).X - child.DesiredSize.Width / 2,
GetCenter(child).Y + GetRadius(child)),
new Size(child.DesiredSize.Width,
child.DesiredSize.Height)));
}
The ArrangeChild method performs the following operations:
- Receives the child it affects as a parameter.
- Assigns a new RotateTransform to the RenderTransform property of the child.
- Uses the GetAngle method of the Angle attached property to assign a value to the RotateTransform.Angle property.
- Calculates and assignes the center of the RotateTransform using the width of the child and the Radius attached property.
- Arranges the child using the value of the private attached property Center, as well as the Radius.
Now we can override the MeasureOverride and ArrangementOverride methods to implement the arrangement of the items:
protected override Size MeasureOverride(Size availableSize)
{
double maxChildRadius = 0;
foreach (UIElement child in Children)
{
child.Measure(new
Size(Double.PositiveInfinity,
Double.PositiveInfinity));
maxChildRadius =
Math.Max(maxChildRadius, GetRadius(child));
}
double squareSize = 2 * (maxChildRadius);
return new Size(squareSize, squareSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
var centerX = finalSize.Width / 2;
var centerY = finalSize.Height / 2;
foreach (UIElement child in Children)
{
child.SetValue(CenterProperty, new Point(centerX, centerY));
ArrangeChild(child);
}
return finalSize;
}
MeasureOverride finds the size of the children and calculates the area the panel occupies on screen. ArrangeOverride iterates through the children, sets the center value and arranges using the auxiliary method ArrangeChild created previously. Finally we need to add the implementation of the property changed callback methods for Angle and Radius, in order to enable the support for animation:
private static void OnAnglePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) {
var child = sender as UIElement;
ArrangeChild(child);
}
private static void OnRadiusPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) {
var child = sender as UIElement;
ArrangeChild(child);
}
The callback methods make sure that the child element is re-arranged whenever the property changes. This is more efficient than invalidating the arrange of the panel.
Using the radial panel
With the implementation complete, it is time to create an instance of the panel and add some children within:
<Grid x:Name="LayoutRoot" Background="White">
<Ellipse Stroke="Black" Fill="Gold"
Width="50" Height="50"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
<this:RadialPanel x:Name="TranslationRadialPanel"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch">
<Ellipse Width="20" Height="20"
this:RadialPanel.Angle="150"
this:RadialPanel.Radius="45"
Stroke="Black" Fill="Navy" x:Name="Ellipse01"/>
<Ellipse Width="20" Height="20"
this:RadialPanel.Angle="120"
this:RadialPanel.Radius="75"
Stroke="Black" Fill="Navy" x:Name="Ellipse02"/>
<Ellipse Width="20" Height="20"
this:RadialPanel.Angle="130"
this:RadialPanel.Radius="90"
Stroke="Black" Fill="Navy" x:Name="Ellipse03"/>
<Ellipse Width="20" Height="20"
this:RadialPanel.Angle="-30"
this:RadialPanel.Radius="75"
Stroke="Black" Fill="Navy" x:Name="Ellipse04"/>
<Ellipse Width="20" Height="20"
this:RadialPanel.Angle="250"
this:RadialPanel.Radius="55"
Stroke="Black" Fill="Navy" x:Name="Ellipse05"/>
<Ellipse Width="20" Height="20"
this:RadialPanel.Angle="230"
this:RadialPanel.Radius="40"
Stroke="Black" Fill="Navy" x:Name="Ellipse06"/>
<Ellipse Width="20" Height="20"
this:RadialPanel.Angle="50"
this:RadialPanel.Radius="65"
Stroke="Black" Fill="Navy" x:Name="Ellipse07"/>
<Ellipse Width="20" Height="20"
this:RadialPanel.Angle="150"
this:RadialPanel.Radius="180"
Stroke="Black" Fill="Crimson" x:Name="Ellipse08"/>
<Ellipse Width="20" Height="20"
this:RadialPanel.Angle="120"
this:RadialPanel.Radius="160"
Stroke="Black" Fill="Crimson" x:Name="Ellipse09"/>
<Ellipse Width="20" Height="20"
this:RadialPanel.Angle="130"
this:RadialPanel.Radius="190"
Stroke="Black" Fill="Crimson" x:Name="Ellipse10"/>
<Ellipse Width="20" Height="20"
this:RadialPanel.Angle="-30"
this:RadialPanel.Radius="185"
Stroke="Black" Fill="Crimson" x:Name="Ellipse11"/>
<Ellipse Width="20" Height="20"
this:RadialPanel.Angle="250"
this:RadialPanel.Radius="165"
Stroke="Black" Fill="Crimson" x:Name="Ellipse12"/>
<Ellipse Width="20" Height="20"
this:RadialPanel.Angle="230"
this:RadialPanel.Radius="135"
Stroke="Black" Fill="Crimson" x:Name="Ellipse13"/>
<Ellipse Width="20" Height="20"
this:RadialPanel.Angle="50"
this:RadialPanel.Radius="180"
Stroke="Black" Fill="Crimson" x:Name="Ellipse14"/>
</this:RadialPanel>
</Grid>
This is how the arrangement looks like at runtime:
Now, in order to animate the attached properties, it is necessary to add a storyboard to the resources of the Page user control:
<UserControl.Resources>
<Storyboard x:Key="TranslationStoryboard"/>
</UserControl.Resources>
As the number of items in the arrangement is considerable, we decided to automate the process of creating the animations in the code behind:
private DoubleAnimationUsingKeyFrames GetAnimation(UIElement child, String name)
{
var random = new Random(DateTime.Now.Millisecond);
var animation = new DoubleAnimationUsingKeyFrames()
{
RepeatBehavior = RepeatBehavior.Forever,
};
Storyboard.SetTargetName(animation, name);
Storyboard.SetTargetProperty(animation, new PropertyPath(RadialPanel.AngleProperty));
var keyframe = new SplineDoubleKeyFrame()
{
KeyTime = new TimeSpan(random.Next(20000000, 120000000)),
KeySpline = new KeySpline()
{
ControlPoint1 = new Point(0, 0),
ControlPoint2 = new Point(1, 1)
},
Value = RadialPanel.GetAngle(child) + 360
};
animation.KeyFrames.Add(keyframe);
return animation;
}
Finally in the constructor of the page, the application iterates through the children of the radial panel and creates an animation for each of them:
public Page()
{
InitializeComponent();
var translationStoryboard = (Resources["TranslationStoryboard"] as Storyboard);
foreach (var child in TranslationRadialPanel.Children) {
if (child is FrameworkElement)
{
var element = child as Ellipse;
translationStoryboard.Children.Add(GetAnimation(element, element.Name));
}
}
translationStoryboard.Begin();
}
The resulting application animates the Angle property making each of the items inside the radial panel rotate around the center of the panel infinitely.
You can get the complete code and see the demonstration here:
Currently rated 5.0 by 2 people
- Currently 5/5 Stars.
- 1
- 2
- 3
- 4
- 5