WPF自定义控件(教程含源码)-圆盘菜单

本文详细介绍了如何在WPF中创建一个圆盘菜单控件,该控件能够根据按钮数量自动调整大小,并允许自定义描边颜色、描边大小和填充颜色。控件由扇形按钮组成,通过Path Data属性绘制扇形路径,并利用数学公式确定点的坐标。同时,文章提供了扇形按钮的后台代码和样式设置,包括按钮的旋转角度转换和缩放动画效果。

控件需求

  • 圆盘菜单控件样式如下图所示

圆盘按钮

  •  满足的功能需求

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
    }

以上就是控件全部的源码。有问题欢迎在评论去交流

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lhyriver

制作不易,打赏给我提供动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值