Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Circular Context menu in WPF

0.00/5 (No votes)
18 Jul 2012 1  
The purpose of this article is to illustrate a context menu that renders in cricular shape.

Introduction

Standard context menu is in rectangular shape. Now a days fancy UI is getting popular in LOB apps. Thanks to WPF framework. The purpose of this article is to illustrate a context menu that renders in cricular shape.

Sample Image

Background

The basic idea is to find points on circle using trignometery.

  1. x = centerX + cos (angle) * hypotenuse
  2. y = centerY + sin (angle) * hypotenuse
  3. Divde the circle into arcs that matches context menu item count. i.e If there are 2 context menu item, the circle will be created using 2 arcs.

Using the code

The usage is as below:

<Grid.ContextMenu >
    <ContextMenu>
        <local:CustomMenuItem Header="Cut" Background="Red" 
             Foreground="Black" Click="CustomMenuItem_Click"/>
        <local:CustomMenuItem Header="Copy" Background="Green" Foreground="White" />
        <local:CustomMenuItem Header="Paste" Background="Blue" Foreground="Wheat" />
        <local:CustomMenuItem Header="View" Background="Aqua" Foreground="Black" />
        <local:CustomMenuItem Header="Tool" Background="Black" Foreground="Wheat" />
        <local:CustomMenuItem Header="Donate" Background="Chartreuse" Foreground="White" />
        <local:CustomMenuItem Header="Pend" Background="Brown" Foreground="Wheat" />
        <local:CustomMenuItem Header="Appr" Background="BlueViolet" Foreground="White" />
        <local:CustomMenuItem Header="Pend1" Background="Yellow" Foreground="Green" />
        <local:CustomMenuItem Header="Appr1" Background="Plum" Foreground="White" />
    </ContextMenu>
</Grid.ContextMenu>

The project contains 3 main files.

  • RoundPanel.cs - A custom panel derived from panel that layouts the context menu item. This panel can show menu items on a single track or on multitrack.
  • CustomMenuItem.cs - The class derives from MenuItem, that does the actual drawing of arc and text.
  • ContextMenu and ContextMenuItem styles - It is defiend in App.xaml's resource dictionary.

Let us look at RoundPanel.cs. In this class we override both MeasureOverride and ArrangeOverride.

Panel also has dependancy property "IsMultiTrack", based on its value the menu items will be arranged either on a single track or on multiple track.

In method "MeasureOverride" we find the required size for the ContextMenu panel. So that children can be rendered with out clipping. Method will look as below.

protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize)
{
    Size infiniteSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
    double width = 30 + (this.InternalChildren.Count / 4 * 50);
    return new Size(width, width);
}

In "ArrangeOverride" we have various algorthim to find each childs Start and End Arc points. In case of "MultiTrack", it also find the required circle (Track) radius.

The algorthim to find number of arcs and position of each arc to render smooth cricle is in method ArrangeOverride and is shown below

protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize)
{
    if (IsMultiTrack)
        DrawForMultiTrack(finalSize.Width / 2, finalSize.Height / 2);
    else
    {
        DrawForSingleTrack(finalSize.Width/2, finalSize.Height/2);
    }
    return finalSize; // Returns the final Arranged size
}

The method "DrawMultiTrack" contains the algorithm to draw menu items on multiple track. The algorithm is writtern such that, it start with 2 arcs for 1st track and then draws 4 arcs for remaining tracks. For each track the circle radius is calculated using

hyp = 15 * track;

Various angles are determined by the below methods. E.g. Say for 2 menuItems, the required angles to draw ARCs (i.e. 2 Arcs - which required 4 points {0, 178, 180, 358})

Sample Image - maximum width is 600 pixels

Once Angles are determined. Next step is to calculate the arc start point and end point as shown below
double startX = midX + Math.Cos(DegToRad(startAngle)) * hyp;
double startY = midY + Math.Sin(DegToRad(startAngle)) * hyp;
menuItem.StartX = startX;
menuItem.StartY = startY;

double x = midX + Math.Cos(DegToRad(endAngle)) * hyp;
double y = midY + Math.Sin(DegToRad(endAngle)) * hyp;
menuItem.X = x;
menuItem.Y = y;

The method "DrawForSingleTrack" as algorithm to determine various mesaurement for drawing arcs on single track. The circle radius is calulated using the below formula.

int hyp = 15 * (this.InternalChildren.Count / 4 + 1);

End point of Arc is calculated using the below formula.

int angleDisp = 360/this.InternalChildren.Count;
endAngle = angle + angleDisp - 2;

Once we have those values, next step is to find the arcs start point and end point.

The actual drawing is done by class CustomMenuItem.cs. It has method "OnRender". The class is derived from MenuItem.

PathGeometry pathGeom = new PathGeometry();
PathFigure figure = new PathFigure() { IsClosed = false, IsFilled = false };
figure.StartPoint = new Point(StartX, StartY);
ArcSegment segment = new ArcSegment();
segment.IsLargeArc = IsLargeArc;
segment.Size = new Size(Hyp, Hyp);
segment.SweepDirection = SweepDirection.Clockwise;
segment.RotationAngle = 0;
segment.Point = new Point(X, Y);
figure.Segments.Add(segment);
pathGeom.Figures.Add(figure);

if (IsMouseOver)
    drawingContext.DrawGeometry(Brushes.Red, new Pen(Background, 20), pathGeom);
else
    drawingContext.DrawGeometry(Brushes.Red, new Pen(Background, 15), pathGeom);

This method also draws text in order to align with arc and is as below:

if (this.Header == null)
    return;
string text = this.Header.ToString();
this.ToolTip = text; 
if (StartAngle >= 0 && EndAngle <= 180)
    text = new String(text.Reverse().ToArray());
else
    text = this.Header.ToString();

FormattedText formattedText ;
int angle = StartAngle + 10;
int angleSpace =( EndAngle - StartAngle )/ text.Length;
double x;
double y;
int idx = 0;
do
{
    x = MidX  -4  + Math.Cos(DegToRad(angle))*Hyp;
    y = MidY - 7 + Math.Sin(DegToRad(angle))*Hyp;
    formattedText = new FormattedText(
       text[idx++].ToString(),
       CultureInfo.GetCultureInfo("en-us"),
       FlowDirection.LeftToRight,
       new Typeface(this.FontFamily, this.FontStyle, this.FontWeight, this.FontStretch),
       this.FontSize,
       Foreground);
    angle += angleSpace;
     
    drawingContext.DrawText(formattedText, new Point(x, y));

} while (angle < EndAngle && idx < text.Length);

In order to use RoundPanel, we have to retemplate the ContextMenu and will be as shown below

<Style TargetType="{x:Type ContextMenu}">
     <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ContextMenu}">
                <local:RoundPanel IsItemsHost="True" 
                    KeyboardNavigation.DirectionalNavigation="Cycle" 
                    Background="Transparent"  IsMultiTrack="True" >
                </local:RoundPanel>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Also make sue to set the MenuItem DataContext to null. I did that using the below style.

<Style TargetType="{x:Type local:CustomMenuItem}">
    <Setter Property="DataContext" Value="null" />
</Style>

Let'us rotate this context menu. In RoundPanel I used DispatcherTimer that ticks every millisecond and increment angle by one as shown below

void _timer_Tick(object sender, EventArgs e)
{
    RotateTransform rotateTrans = this.RenderTransform as RotateTransform;
    if (rotateTrans != null)
    {
        rotateTrans.Angle = angle++;
        if (angle > 359)
            angle = 0;
    }
}

I also used event "MouseEvent" in class CustomMenuItem to notify the parent when mouse actions occur on MenuItem. Based on those actions, the timer will be stopped or started.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here