控件需求

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

圆盘按钮

  • 满足的功能需求

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自定义控件(教程含源码)-圆盘菜单相关推荐

  1. WPF自定义控件(教程含源码)-圆形进度条、环形进度条

    使用环形进度条显示用量百分比 控件效果如下 控件的关键属性如下: Background:控制背景圆环的原色. Stroke:控制进度圆环颜色.以及中间文本颜色. Value:进度百分比,double类 ...

  2. Odoo16 教程含源码

    Odoo16 教程含源码 Odoo16 开发教程 版本变化 模块开发步骤 源码 Odoo16 开发教程 Odoo 号称全球第一的开源ERP平台,除了提供一站式的企业应用开发解决方案,作为一个网站设计器 ...

  3. SwiftUI 音乐和网络大全之网络音乐播放App支持iTunes搜索与播放(教程含源码)

    实战需求 SwiftUI 音乐和网络大全之网络音乐播放App支持iTunes搜索与播放(教程含源码) 本文价值与收获 看完本文后,您将能够作出下面的界面 实战代码 import SwiftUIstru ...

  4. SwiftUI 精品项目之完整MOOC幕课iOS项目 含服务端 轮播欢迎页面(教程含源码)

    实战需求 SwiftUI 精品项目之完整MOOC幕课iOS项目 (教程含源码) 本文价值与收获 看完本文后,您将能够作出下面的界面 看完本文您将掌握的技能 自动轮播 个性化注册界面 个人信息界面 带f ...

  5. macOS 音频编辑剪切软件源码支持mp3等格式(教程含源码)

    实战需求 macOS 音频编辑剪切软件源码支持mp3等格式(教程含源码) 本文价值与收获 看完本文后,您将能够作出下面的界面 看完本文您将掌握的技能 支持剪切音频 支持复制音频 支持删除音频 支持un ...

  6. 抖音小程序基础之 目前提供哪些API(教程含源码)

    抖音小程序基础之 目前提供哪些API(教程含源码) 小程序开发框架提供丰富的 字节跳动宿主 原生 API,可以方便的调起 字节跳动宿主 提供的能力,如获取系统信息等.详细介绍请参考 API 文档. 通 ...

  7. SwiftUI iOS 精品项目之每天收集的故事卡片(教程含源码)

    实战需求 SwiftUI iOS 精品项目之每天收集的故事卡片(教程含源码) 每天收集的故事的卡片 本文价值与收获 看完本文后,您将能够作出下面的界面 核心功能 1.每天总共3个问题!选择一个您喜欢的 ...

  8. SwiftUI 界面大全之文本折叠书签动画组件3D(中文教程含源码)

    实战需求 SwiftUI 界面大全之文本折叠书签动画组件3D(中文教程含源码) 本文价值与收获 看完本文后,您将能够作出下面的界面 基础知识 效果本身其实很简单,包括三件事: 图像的旋转 图像的垂直移 ...

  9. SwiftUI 绘图shape大全之 Teardrop水滴形状 (中文教程含源码)

    实战需求 SwiftUI 绘图shape大全之 Teardrop水滴形状 (中文教程含源码) 本文价值与收获 看完本文后,您将能够作出下面的界面 基础知识 ​ 什么是Paths Paths主要用于绘制 ...

最新文章

  1. php tp5生成条形码,thinkphp5 + barcode 生成条形码
  2. win10镜像重装,快速设置之后无限重启怎么办?
  3. 苹果怎么换行打字_停课不停学!苹果电脑学习类软件推荐,丰富您的假期生活...
  4. SwiftUI之如何使用@EnvironmentObject在视图之间共享数据
  5. 二叉树的中序遍历—leetcode94
  6. 3.4 svm人脸识别
  7. RTCPeerConnection.onicecandidate属性
  8. Date类+DateFormat
  9. 学excel还是学python_已经会Excel了还需要学python吗?
  10. python 如何判断excel单元格为空_如何用python处理excel(二)
  11. Java开发必看JPA概念大全
  12. BoltDB 一个简单的纯 Go key/value 存储 [译]
  13. redis底层数据结构
  14. 如何自定义一个注解(@Annotation)
  15. 软件压力测试报告要怎么写,如何做接口压力测试?压力测试报告应该包含哪些结果?...
  16. 小学生自学奥数必备的这些书籍
  17. 家用无线TP-LINK路由器使用一段时间后,频繁断网解决办法之一
  18. java 判断是否信用卡_用java实现验证输入信用卡号码的正误
  19. h5 底部按钮兼容 iPhone X(解决底部横杠遮挡问题)
  20. 在线压缩pdf文件任意大小,在线压缩pdf文件大小

热门文章

  1. 芜湖光华学校优选云盒子教育云盘,自建专属数据中心
  2. 怎么彻底卸载office365?
  3. NG Toolset开发笔记--5GNR Resource Grid(50)
  4. 红光光浴真的有用吗?#大健康#红光光浴#红光#种光光学
  5. 计算机科学采用通知,关于采用合同模板的通知 科研〔2019〕134号
  6. CITECT HMI添加周期任务的方法
  7. 一个百度员工的离职感悟:听话/出活/忍耐/量化
  8. 详细设计之(人机界面设计问题)
  9. mysql item.pop_python flas中mysql的popluate组合框值
  10. 四川农业大学ZigBee复习重点