HypernovaDesktop Components · docs
Binarymission
WPF › Circular Panel › CircularPanel

CircularPanel

class.NET 6 / 8 / 10.NET Framework 4.8Binarymission.WPF.Controls.Panels

A layout panel that places its children round a circle or an ellipse – evenly round the whole ring, or fanned across part of it – and can turn the ring to any angle.

public class CircularPanel : Panel

Inherits from / implements: Panel

Remarks

Use it for radial menus, a ring of shortcuts round a central picture, the hours on a clock face, seats round a table plan, or a carousel-like ring of thumbnails that turns when RenderAngle is animated. It works as a panel in its own right or, through ItemsControl.ItemsPanel, as the layout of any bound list.

The ring

The ring is centred in the panel and follows its shape: an ellipse whose horizontal radius is the panel's width divided by ItemsClosenessFactor and whose vertical radius is its height divided by the same factor. A square panel gives a circle. The ring grows and shrinks as the panel is resized.

Placing the items

Each child keeps the size it asks for and is centred on its point on the ring; the panel never squeezes a child to fit. The first child sits at RenderAngle, measured in radians clockwise from the right-hand side of the ring, and each child after it one gap further round. The gap depends on AngleCalculationStrategy: with GapAngleCalculationStrategy.Dynamic the children share the whole circle equally, and with GapAngleCalculationStrategy.Custom they share the angle in ItemsGapAngleFactor.

Size

The panel takes all the room it is offered. Where it is offered unlimited room – in a StackPanel, say, or an auto-sized row – it asks for enough for its largest child to fit on the ring. It does not clip: an item placed past the edge is still drawn, unless a container round the panel clips it.

Examples

Eight department shortcuts round a circle, the first at the top.

<Window x:Class="Intranet.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:panels="clr-namespace:Binarymission.WPF.Controls.Panels;assembly=Binarymission.WPF.Controls.CircularPanel"
        Title="Departments" Width="520" Height="520">

    <panels:CircularPanel Width="440" Height="440"
                          ItemsClosenessFactor="2.6"
                          RenderAngle="-1.5708">
        <Button Width="90" Height="36" Content="Sales" />
        <Button Width="90" Height="36" Content="Finance" />
        <Button Width="90" Height="36" Content="HR" />
        <Button Width="90" Height="36" Content="IT" />
        <Button Width="90" Height="36" Content="Legal" />
        <Button Width="90" Height="36" Content="Marketing" />
        <Button Width="90" Height="36" Content="Operations" />
        <Button Width="90" Height="36" Content="Support" />
    </panels:CircularPanel>
</Window>

A bound list laid out round a ring that turns slowly and continuously.

<ItemsControl ItemsSource="{Binding Products}" Width="400" Height="400">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <panels:CircularPanel ItemsClosenessFactor="3">
                <panels:CircularPanel.Triggers>
                    <EventTrigger RoutedEvent="Loaded">
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation Storyboard.TargetProperty="RenderAngle"
                                                 From="0" To="6.2832" Duration="0:0:30"
                                                 RepeatBehavior="Forever" />
                            </Storyboard>
                        </BeginStoryboard>
                    </EventTrigger>
                </panels:CircularPanel.Triggers>
            </panels:CircularPanel>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Image Source="{Binding Thumbnail}" Width="64" Height="64" ToolTip="{Binding Name}" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

See also

Members at a glance

Constructors
CircularPanelCreates an empty circular panel that spreads its children evenly round the whole ring, starting at the right-hand side.
Properties
AngleCalculationStrategyGets or sets whether the children are spread evenly round the whole ring or across the angle in ItemsGapAngleFactor.
ItemsClosenessFactorGets or sets how far in from the panel's edge the ring of children sits: the larger the value, the smaller and tighter the ring.
ItemsGapAngleFactorGets or sets the angle, in radians, that the children share between them when AngleCalculationStrategy is GapAngleCalculationStrategy.Custom.
RenderAngleGets or sets where on the ring the first child is placed, which turns the whole ring of children.
Methods
ArrangeOverridePlaces each child, at the size it asked for, centred on its point on the ring.
MeasureOverrideMeasures each child at the size it wants, and works out how much room the panel needs.
Fields
AngleCalculationStrategyPropertyIdentifies the AngleCalculationStrategy dependency property.
ItemsClosenessFactorPropertyIdentifies the ItemsClosenessFactor dependency property.
ItemsGapAngleFactorPropertyIdentifies the ItemsGapAngleFactor dependency property.
RenderAnglePropertyIdentifies the RenderAngle dependency property.

Constructors

CircularPanel

public CircularPanel() : base()

Creates an empty circular panel that spreads its children evenly round the whole ring, starting at the right-hand side.

Properties

AngleCalculationStrategy

public GapAngleCalculationStrategy AngleCalculationStrategy

Gets or sets whether the children are spread evenly round the whole ring or across the angle in ItemsGapAngleFactor.

Value
One of the GapAngleCalculationStrategy values. The default is GapAngleCalculationStrategy.Dynamic: evenly round the whole ring.
Remarks

With the default ItemsGapAngleFactor of a full turn, both strategies place the children identically.

See also

ItemsClosenessFactor

public double ItemsClosenessFactor

Gets or sets how far in from the panel's edge the ring of children sits: the larger the value, the smaller and tighter the ring.

Value
The number the panel's width and height are divided by to give the ring's horizontal and vertical radii. The default is 2.5.
Remarks

At 2 the ring touches the panel's edges, so the children's centres lie on the edge and half of each child hangs outside. Above 2 the ring moves inwards; the room left between a child and the edge is size * (0.5 - 1 / factor) - childSize / 2. At the default of 2.5 that is a tenth of the panel's size less half the child's, so a 480-pixel panel leaves 4 logical pixels round an 88-pixel child – and a smaller panel leaves less than nothing. Raise the factor, or enlarge the panel, when large children run over the edge.

Below 2 the ring is larger than the panel and the children sit outside it. They are still drawn, which suits a ring deliberately spread round a central picture, but inside anything that clips they disappear. Values closer to zero than 0.1, and values that are not a finite number, are read as 0.1.

Example
<!-- A ring that sits well inside its panel, leaving room in the middle for a logo. -->
<Grid Width="360" Height="360">
    <Image Source="Images/logo.png" Width="96" Height="96" />
    <panels:CircularPanel ItemsClosenessFactor="3.2">
        <TextBlock Text="Plan" />
        <TextBlock Text="Build" />
        <TextBlock Text="Test" />
        <TextBlock Text="Release" />
    </panels:CircularPanel>
</Grid>

ItemsGapAngleFactor

public double ItemsGapAngleFactor

Gets or sets the angle, in radians, that the children share between them when AngleCalculationStrategy is GapAngleCalculationStrategy.Custom.

Value
An angle in radians. The default is 2 * Math.PI (6.2832), a full turn.
Remarks

The gap from one child to the next is this angle divided by the number of children, so the children cover the angle less one gap: four children with Math.PI (a half turn) sit 0, 45, 90 and 135 degrees round from RenderAngle. To fan n children so that the first and last are exactly an angle A apart, set this to A * n / (n - 1).

A negative angle runs the children anticlockwise. Ignored when AngleCalculationStrategy is GapAngleCalculationStrategy.Dynamic. A value that is not a finite number is read as a full turn.

Example
<!-- Five actions fanned across the top half of the ring, from left to right. -->
<panels:CircularPanel AngleCalculationStrategy="Custom"
                      RenderAngle="3.1416"
                      ItemsGapAngleFactor="3.927">
    <Button Content="Reply" />
    <Button Content="Reply all" />
    <Button Content="Forward" />
    <Button Content="Archive" />
    <Button Content="Delete" />
</panels:CircularPanel>
See also

RenderAngle

public double RenderAngle

Gets or sets where on the ring the first child is placed, which turns the whole ring of children.

Value
An angle in radians, measured clockwise from the right-hand side of the ring (3 o'clock). The default is 0.
Remarks

Every other child follows on from the first, so changing this turns the whole arrangement: -Math.PI / 2 (-1.5708) puts the first child at the top, Math.PI (3.1416) on the left. A value that is not a finite number is read as 0.

The panel lays itself out again whenever the angle changes, so it can be animated with a DoubleAnimation to spin the ring, or bound to a slider to let the user turn it. The children themselves are moved, not rotated: text stays upright.

Example
// Turn the ring a quarter turn clockwise, smoothly, each time the user clicks "Next".
double target = ring.RenderAngle + Math.PI / 2;
ring.BeginAnimation(CircularPanel.RenderAngleProperty,
    new DoubleAnimation(target, TimeSpan.FromMilliseconds(400)) { FillBehavior = FillBehavior.Stop });
ring.RenderAngle = target;
See also

Methods

ArrangeOverride

protected override Size ArrangeOverride(Size finalSize)

Places each child, at the size it asked for, centred on its point on the ring.

Parameters
finalSize
The area the panel has been given. The ring is centred in it and its radii are this width and height divided by ItemsClosenessFactor.
Returns

finalSize: the panel always uses all the area it is given.

Remarks

A RenderAngle or ItemsGapAngleFactor that is not a finite number is read as 0 and a full turn respectively, so a stray binding value cannot place the children at an impossible position.

MeasureOverride

protected override Size MeasureOverride(Size availableSize)

Measures each child at the size it wants, and works out how much room the panel needs.

Parameters
availableSize
The room on offer. Either dimension may be infinite.
Returns

On each axis: all the room on offer when it is a finite amount; otherwise enough for the largest child to sit on the ring inside the panel – the child's size times factor / (factor - 2), where factor is ItemsClosenessFactor, or three times the child's size when the factor is 2 or less.

Remarks

Children are measured with unlimited room, so each one is laid out at its natural size and never squeezed by the panel.

Fields

AngleCalculationStrategyProperty static

public static readonly DependencyProperty AngleCalculationStrategyProperty = DependencyProperty.Register( "AngleCalculationStrategy", typeof(GapAngleCalculationStrategy), typeof(CircularPanel), new PropertyMetadata(GapAngleCalculationStrategy.Dynamic, (o, args)

Identifies the AngleCalculationStrategy dependency property.

ItemsClosenessFactorProperty static

public static readonly DependencyProperty ItemsClosenessFactorProperty = DependencyProperty.Register( "ItemsClosenessFactor", typeof(double), typeof(CircularPanel), new PropertyMetadata(2.5d, (o, args)

Identifies the ItemsClosenessFactor dependency property.

ItemsGapAngleFactorProperty static

public static readonly DependencyProperty ItemsGapAngleFactorProperty = DependencyProperty.Register( "ItemsGapAngleFactor", typeof(double), typeof(CircularPanel), new PropertyMetadata(DefaultAngleFactor, (o, args)

Identifies the ItemsGapAngleFactor dependency property.

RenderAngleProperty static

public static readonly DependencyProperty RenderAngleProperty = DependencyProperty.Register( "RenderAngle", typeof(double), typeof(CircularPanel), new PropertyMetadata(default(double), (o, args)

Identifies the RenderAngle dependency property.