65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 中的圆形上下文菜单

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2012 年 7 月 18 日

CPOL

3分钟阅读

viewsIcon

48048

downloadIcon

2045

本文旨在演示一个呈圆形的上下文菜单。

引言

标准上下文菜单呈矩形。如今,花哨的UI在LOB应用程序中越来越受欢迎。这要感谢WPF框架。本文旨在演示一个呈圆形的上下文菜单。

Sample Image

背景

基本思路是使用三角函数找到圆上的点。

  1. x = centerX + cos (angle) * hypotenuse
  2. y = centerY + sin (angle) * hypotenuse
  3. 将圆分成与上下文菜单项数量匹配的弧。例如,如果有2个上下文菜单项,则将使用2个弧创建圆。

使用代码

使用方法如下:

<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>

该项目包含3个主要文件。

  • RoundPanel.cs - 一个从panel派生的自定义面板,用于布局上下文菜单项。此面板可以在单轨或多轨上显示菜单项。
  • CustomMenuItem.cs - 此类派生自MenuItem,用于实际绘制弧和文本。
  • ContextMenu和ContextMenuItem样式 - 在App.xaml的资源字典中定义。

让我们看看RoundPanel.cs。在这个类中,我们重写了MeasureOverride和ArrangeOverride。

面板还有一个依赖属性“IsMultiTrack”,根据其值,菜单项将排列在单轨或多轨上。

在“MeasureOverride”方法中,我们找到ContextMenu面板所需的尺寸。以便子元素可以渲染而不会被裁剪。方法如下所示。

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);
}

在“ArrangeOverride”中,我们有各种算法来查找每个子元素的起始和结束弧点。对于“MultiTrack”,它还查找所需的圆(轨道)半径。

在ArrangeOverride方法中,用于查找弧数和每个弧位置以绘制平滑圆的算法如下所示。

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
}

“DrawMultiTrack”方法包含在多轨上绘制菜单项的算法。该算法从第一轨的2个弧开始,然后为其余轨绘制4个弧。对于每个轨,圆的半径使用以下公式计算:

hyp = 15 * track;

各种角度由以下方法确定。例如,对于2个菜单项,绘制弧所需的角(即2个弧 - 需要4个点{0, 178, 180, 358})。

Sample Image - maximum width is 600 pixels

确定角度后,下一步是计算弧的起点和终点,如下所示:
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;

“DrawForSingleTrack”方法作为算法来确定在单轨上绘制弧的各种测量值。圆的半径使用以下公式计算:

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

弧的终点使用以下公式计算:

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

一旦我们有了这些值,下一步就是找到弧的起点和终点。

实际绘制工作由CustomMenuItem.cs类完成。它具有“OnRender”方法。该类派生自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);

此方法还绘制文本以与弧对齐,如下所示:

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);

为了使用RoundPanel,我们必须重新模板化ContextMenu,如下所示:

<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>

还要确保将MenuItem DataContext设置为null。我使用以下样式完成了此操作。

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

让我们旋转此上下文菜单。在RoundPanel中,我使用了每毫秒滴答一次并将角度增加一的DispatcherTimer,如下所示:

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

我还使用了CustomMenuItem类中的“MouseEvent”事件,以便在MenuItem上发生鼠标操作时通知父级。根据这些操作,计时器将停止或启动。

© . All rights reserved.