Circular Layout Panel v2

On the flight to Remix09 last week I had fun putting together a simple WPF Circular Layout Panel. Today I decided that a nice “extra” feature would be to have the child elements optionally rotated so that they are normalised with the centre of the layout panel.


So after adding a new attached dependency property, IsNormalised I updated my sample clock and fan menu. The following XAML…

<Window x:Class=”PanelTest.Window1″
xmlns
=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x
=”http://schemas.microsoft.com/winfx/2006/xaml”
Title=”Circular Panel Examples” Height=”405″ Width
=”383″
xmlns:local
=”clr-namespace: PanelTest”
xmlns:panels=”clr-namespace:Spencen.Panels;assembly=Spencen.Panels”
>
<
DockPanel
>
<
DockPanel.Resources
>
<!– Stuff to make buttons pretty –>
</DockPanel.Resources
>
<Ellipse Fill=”LightYellow” Stroke=”LightGray” StrokeThickness=”1″ StrokeDashArray
=”0,1,1,0″ />
<panels:CircularPanel StartAngle=”-90″ EndAngle=”630″
TextBlock.FontSize=”24pt”>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>XII</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>I</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>II</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>III</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>IV</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>V</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>VI</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>VII</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>VIII</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>IX</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>X</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>XI</TextBlock
>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>




<
Line x:Name=”secondHand” Fill=”Black” X1=”0″ X2=”0″ Y1=”0″ Y2=”100″
Stroke=”Red” StrokeThickness=”1″ StrokeStartLineCap=”Triangle” StrokeEndLineCap
=”Round”
panels:CircularPanel.FixedAngle
=”-90″
panels:CircularPanel.RadiusScaleX
=”0.4″
panels:CircularPanel.RadiusScaleY
=”0.4″
panels:CircularPanel.IsNormalised
=”True”/>
<
Line x:Name=”minuteHand” Fill=”Black” X1=”2″ X2=”2″ Y1=”0″ Y2=”70″
Stroke=”Black” StrokeThickness=”5″ StrokeStartLineCap=”Triangle” StrokeEndLineCap
=”Round”
panels:CircularPanel.FixedAngle
=”-60″
panels:CircularPanel.RadiusScaleX
=”0.3″
panels:CircularPanel.RadiusScaleY
=”0.3″
panels:CircularPanel.IsNormalised
=”True”/>
<
Line x:Name=”hourHand” Fill=”Black” X1=”5″ X2=”5″ Y1=”0″ Y2=”50″
Stroke=”Black” StrokeThickness=”10″ StrokeStartLineCap=”Triangle” StrokeEndLineCap
=”Round”
panels:CircularPanel.FixedAngle
=”-180″
panels:CircularPanel.RadiusScaleX
=”0.2″
panels:CircularPanel.RadiusScaleY
=”0.2″
panels:CircularPanel.IsNormalised
=”True”/>
<
Ellipse Fill
=”Black”
panels:CircularPanel.FixedAngle
=”0″
panels:CircularPanel.RadiusScaleX
=”0″
panels:CircularPanel.RadiusScaleY=”0″ Width=”30″ Height
=”30″ />
<
panels:CircularPanel.Triggers
>
<
EventTrigger RoutedEvent
=”Loaded”>
<
BeginStoryboard
>
<
Storyboard
>
<
DoubleAnimation Duration=”0:1:0″ By=”360″ RepeatBehavior=”Forever”
Storyboard.TargetName=”secondHand”
Storyboard.TargetProperty
=”(panels:CircularPanel.FixedAngle)”/>
<
DoubleAnimation Duration=”1:0:0″ By=”360″ RepeatBehavior=”Forever”
Storyboard.TargetName=”minuteHand”
Storyboard.TargetProperty
=”(panels:CircularPanel.FixedAngle)”/>
<
DoubleAnimation Duration=”12:0:0″ By=”360″ RepeatBehavior=”Forever”
Storyboard.TargetName=”hourHand”
Storyboard.TargetProperty
=”(panels:CircularPanel.FixedAngle)”/>
</
Storyboard
>
</
BeginStoryboard
>
</
EventTrigger
>
</
panels:CircularPanel.Triggers
>
</
panels:CircularPanel
>
<
panels:CircularPanel Padding=”45″ StartAngle=”-90″ EndAngle=”-90″
>
<
Button panels:CircularPanel.IsNormalised=”true”>1</Button
>
<
Button panels:CircularPanel.IsNormalised=”true”>2</Button
>
<
Button panels:CircularPanel.IsNormalised=”true”>3</Button
>
<
Button panels:CircularPanel.IsNormalised=”true”>4</Button
>
<
Button Panel.ZIndex=”1″ Background=”Gray”>Menu</Button
>
<
Button panels:CircularPanel.IsNormalised=”true”>5</Button
>
<
Button panels:CircularPanel.IsNormalised=”true”>6</Button
>
<
Button panels:CircularPanel.IsNormalised=”true”>7</Button
>
<
Button panels:CircularPanel.IsNormalised=”true”>8</Button
>
<
panels:CircularPanel.Triggers
>
<
EventTrigger RoutedEvent
=”MouseEnter”>
<
BeginStoryboard
>
<
Storyboard
>
<
DoubleAnimation To=”-180″ Storyboard.TargetProperty
=”StartAngle”>
<
DoubleAnimation.EasingFunction
>
<
ElasticEase Springiness=”10″ Oscillations
=”2″/>
</
DoubleAnimation.EasingFunction
>
</
DoubleAnimation
>
<
DoubleAnimation To=”0″ Storyboard.TargetProperty
=”EndAngle”>
<
DoubleAnimation.EasingFunction
>
<
ElasticEase Springiness=”10″ Oscillations
=”2″/>
</
DoubleAnimation.EasingFunction
>
</
DoubleAnimation
>
</
Storyboard
>
</
BeginStoryboard
>
</
EventTrigger
>
<
EventTrigger RoutedEvent
=”MouseLeave”>
<
BeginStoryboard
>
<
Storyboard
>
<
DoubleAnimation To=”-90″ AccelerationRatio=”0.5″ DecelerationRatio=”0.5″
Storyboard.TargetProperty
=”StartAngle”/>
<
DoubleAnimation To=”-90″ AccelerationRatio=”0.5″ DecelerationRatio=”0.5″
Storyboard.TargetProperty
=”EndAngle”/>
</
Storyboard
>
</
BeginStoryboard
>
</
EventTrigger
>
</
panels:CircularPanel.Triggers
>
</
panels:CircularPanel
>
</DockPanel
>
</
Window>

Now produces…


 CircularPanel - Normalised


For reference this is so much easier than doing the same thing in WinForms. Custom layout panels really were quite a pain.


Updated source code for CircularPanel is here.