WPF 中的圆形上下文菜单





5.00/5 (6投票s)
本文旨在演示一个呈圆形的上下文菜单。
引言
标准上下文菜单呈矩形。如今,花哨的UI在LOB应用程序中越来越受欢迎。这要感谢WPF框架。本文旨在演示一个呈圆形的上下文菜单。
背景
基本思路是使用三角函数找到圆上的点。
- x = centerX + cos (angle) * hypotenuse
- y = centerY + sin (angle) * hypotenuse
- 将圆分成与上下文菜单项数量匹配的弧。例如,如果有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})。
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上发生鼠标操作时通知父级。根据这些操作,计时器将停止或启动。