控件需求
- 圆盘菜单控件样式如下图所示

圆盘按钮
- 满足的功能需求
1.圆盘内的按钮,根据个数自动调整大小。
2.圆盘可以设置内径。
3.扇形按钮可以自定义“描边颜色”、“描边大小”、“填充颜色”
难点
WPF可以使用Path来绘制图形,Path.Data 存放绘制图形的路径。先来思考一下,如何在一个正方形区域内绘制一个扇形?

只需知道图上四个绿色原点的坐标位置即可。从左上角开始,顺时针命名点P1、P2、P3、P4。那么从P1到P2画圆弧,从P2到P3画直线,从P3到P4画圆弧,从P3到P1画直线,就绘制完成全部的路径。
那么,如何知道P1、P2、P3、P4的坐标呢?
借助正弦、余弦就可计算得出。假设P1、P4所在直线的角度为 θ₁,P2、P3所在直线的角度为 θ₂,外圈半径为 r₁,内圈半径为r2。那么,4个点的坐标位置如下:
P1:( r₁*cosθ₁, r₁*sinθ₁)
P2:( r₁*cosθ₂, r₁*sinθ₂)
P3:( r₂*cosθ₂, r₂*sinθ₂)
P4:( r₂*cosθ₁, r₂*sinθ₁)
那点解决了,剩下就是将各个元素拼装组合。
扇形按钮控件
扇形按钮 xaml 模板代码如下:
<!--Button 按钮扇形转换-->
<local:RDiskButtonsButtonContainerConverter x:Key="RDiskButtonsButtonContainerConverter" />
<!--Button 按钮内文字旋转角度-->
<local:RDiskButtonRotateAngleConverter x:Key="RDiskButtonRotateAngleConverter" />
<!--Button 按钮在 RDiskPanel 内的样式-->
<Style TargetType="local:RDiskButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:RDiskButton">
<Grid RenderTransformOrigin=".5 .5">
<Path Stroke="{TemplateBinding Stroke}" StrokeThickness="{TemplateBinding StrokeThickness}" SnapsToDevicePixels="True" Fill="{TemplateBinding Fill}" >
<Path.Data>
<MultiBinding Converter="{StaticResource RDiskButtonsButtonContainerConverter}">
<Binding Path="ActualWidth" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=local:RDiskButton}"/>
<Binding Path="ActualHeight" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=local:RDiskButton}"/>
<Binding Path="StrokeThickness" RelativeSource="{RelativeSource Mode=Self}"/>
<Binding Path="Index" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=local:RDiskButton}"/>
<Binding Path="Items.Count" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=local:RDiskPanel}"/>
<Binding Path="Radius" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=local:RDiskPanel}"/>
</MultiBinding>
</Path.Data>
</Path>
<Grid RenderTransformOrigin=".5 .5">
<ContentPresenter VerticalAlignment="Top" HorizontalAlignment="Center" Margin="{TemplateBinding Padding}" />
<Grid.RenderTransform>
<RotateTransform >
<RotateTransform.Angle>
<MultiBinding Converter="{StaticResource RDiskButtonRotateAngleConverter}">
<Binding Path="Index" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=local:RDiskButton}"/>
<Binding Path="Items.Count" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=local:RDiskPanel}"/>
</MultiBinding>
</RotateTransform.Angle>
</RotateTransform>
</Grid.RenderTransform>
</Grid>
<Grid.RenderTransform>
<ScaleTransform ScaleX="1" ScaleY="1" x:Name="scale"/>
</Grid.RenderTransform>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Cursor" Value="Hand" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsPressed" Value="true"></Condition>
</MultiTrigger.Conditions>
<MultiTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="scale" Storyboard.TargetProperty="ScaleX" To="0.9" Duration="0:0:0.1" />
<DoubleAnimation Storyboard.TargetName="scale" Storyboard.TargetProperty="ScaleY" To="0.9" Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
</MultiTrigger.EnterActions>
<MultiTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="scale" Storyboard.TargetProperty="ScaleX" To="1" Duration="0:0:0.1" />
<DoubleAnimation Storyboard.TargetName="scale" Storyboard.TargetProperty="ScaleY" To="1" Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
</MultiTrigger.ExitActions>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
扇形按钮后台代码如下:
internal class RDiskButtonsButtonContainerConverter : IMultiValueConverter {
const int spanAngle = 6; // 间隔角度
const double startAngle = -90; // 起始角度
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
if (values.Length >= 3
&& values[0] is double width
&& values[1] is double height
&& values[2] is double strokeWidth
&& values[3] is int index
&& values[4] is int count
&& values[5] is double radius) {
if (count == 0 || index <= -1) return "";
var angle = 360 / count;
var center = new Point(width * 0.5, height * 0.5);
width -= strokeWidth;
height -= strokeWidth;
double a1 = startAngle - angle * 0.5 + spanAngle * 0.5 + angle * index;
double a2 = startAngle + angle * 0.5 - spanAngle * 0.5 + angle * index;
var r1 = Math.Min(width, height) * 0.5;
if (radius > r1) radius = 0;
var p1 = a1.AngleToPoint(r1, center, 0 * 0.5);
var p2 = a2.AngleToPoint(r1, center, 0 * 0.5);
var p3 = a2.AngleToPoint(radius, center, 0 * 0.5);
var p4 = a1.AngleToPoint(radius, center, 0 * 0.5);
var dataStr = $"M {p1.X},{p1.Y} A {r1},{r1} 0 {(Math.Abs(a1 - a2) >= 180 ? 1 : 0)} 1 {p2.X},{p2.Y} L {p3.X},{p3.Y} A {radius},{radius} 0 {(Math.Abs(a1 - a2) >= 180 ? 1 : 0)} 0 {p4.X},{p4.Y} L {p1.X},{p1.Y} z";
var converter = TypeDescriptor.GetConverter(typeof(Geometry));
return (Geometry)converter.ConvertFrom(dataStr);
} else {
return "";
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
internal class RDiskButtonRotateAngleConverter : IMultiValueConverter {
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
if (values.Length == 2 && values[0] is int index && values[1] is int count) {
if (count == 0 || index <= -1) return 0;
return index * 360 / count * 1.0d;
} else {
return 0;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
public partial class RDiskButton: Button {
static RDiskButton() {
DefaultStyleKeyProperty.OverrideMetadata(typeof(RDiskButton), new FrameworkPropertyMetadata(typeof(RDiskButton)));
}
public static readonly DependencyProperty IndexProperty =
DependencyProperty.Register("Index", typeof(int), typeof(RDiskButton), new PropertyMetadata(0));
public int Index {
get => (int)GetValue(IndexProperty);
set => SetValue(IndexProperty, value);
}
#region Stroke 描边颜色
public static readonly DependencyProperty StrokeProperty =
DependencyProperty.Register("Stroke", typeof(Brush), typeof(RDiskButton), new PropertyMetadata(Brushes.Red));
public Brush Stroke {
get { return (Brush)GetValue(StrokeProperty); }
set { SetValue(StrokeProperty, value); }
}
#endregion
#region StrokeThickness 描边大小
public static readonly DependencyProperty StrokeThicknessProperty =
DependencyProperty.Register("StrokeThickness", typeof(double), typeof(RDiskButton), new PropertyMetadata(1d));
public double StrokeThickness {
get => (double)GetValue(StrokeThicknessProperty);
set => SetValue(StrokeThicknessProperty, value);
}
#endregion
#region Fill 填充颜色
public static readonly DependencyProperty FillProperty =
DependencyProperty.Register("Fill", typeof(Brush), typeof(RDiskButton), new PropertyMetadata(Brushes.Red));
public Brush Fill {
get { return (Brush)GetValue(FillProperty); }
set { SetValue(FillProperty, value); }
}
#endregion
}
圆盘Panel控件(RDiskPanel)
RDiskPanel继承 ItemsControl
xaml 代码如下:
<ControlTemplate TargetType="local:RDiskPanel" x:Key="RDiskButtons_Template">
<Grid IsItemsHost="True" />
</ControlTemplate>
<Style TargetType="local:RDiskPanel">
<Setter Property="Template" Value="{StaticResource RDiskButtons_Template}" />
</Style>
后台代码如下:
public partial class RDiskPanel : ItemsControl {
static RDiskPanel() {
DefaultStyleKeyProperty.OverrideMetadata(typeof(RDiskPanel), new FrameworkPropertyMetadata(typeof(RDiskPanel)));
}
#region Radius 内径大小
public static readonly DependencyProperty RadiusProperty =
DependencyProperty.Register("Radius", typeof(double), typeof(RDiskPanel), new PropertyMetadata(0d));
public double Radius {
get => (double)GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
}
#endregion
}
以上就是控件全部的源码。有问题欢迎在评论去交流
本文详细介绍了如何在WPF中创建一个圆盘菜单控件,该控件能够根据按钮数量自动调整大小,并允许自定义描边颜色、描边大小和填充颜色。控件由扇形按钮组成,通过Path Data属性绘制扇形路径,并利用数学公式确定点的坐标。同时,文章提供了扇形按钮的后台代码和样式设置,包括按钮的旋转角度转换和缩放动画效果。

1296

被折叠的 条评论
为什么被折叠?



