WPF自定义控件(教程含源码)-圆盘菜单
控件需求
- 圆盘菜单控件样式如下图所示
圆盘按钮
- 满足的功能需求
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自定义控件(教程含源码)-圆盘菜单相关推荐
- WPF自定义控件(教程含源码)-圆形进度条、环形进度条
使用环形进度条显示用量百分比 控件效果如下 控件的关键属性如下: Background:控制背景圆环的原色. Stroke:控制进度圆环颜色.以及中间文本颜色. Value:进度百分比,double类 ...
- Odoo16 教程含源码
Odoo16 教程含源码 Odoo16 开发教程 版本变化 模块开发步骤 源码 Odoo16 开发教程 Odoo 号称全球第一的开源ERP平台,除了提供一站式的企业应用开发解决方案,作为一个网站设计器 ...
- SwiftUI 音乐和网络大全之网络音乐播放App支持iTunes搜索与播放(教程含源码)
实战需求 SwiftUI 音乐和网络大全之网络音乐播放App支持iTunes搜索与播放(教程含源码) 本文价值与收获 看完本文后,您将能够作出下面的界面 实战代码 import SwiftUIstru ...
- SwiftUI 精品项目之完整MOOC幕课iOS项目 含服务端 轮播欢迎页面(教程含源码)
实战需求 SwiftUI 精品项目之完整MOOC幕课iOS项目 (教程含源码) 本文价值与收获 看完本文后,您将能够作出下面的界面 看完本文您将掌握的技能 自动轮播 个性化注册界面 个人信息界面 带f ...
- macOS 音频编辑剪切软件源码支持mp3等格式(教程含源码)
实战需求 macOS 音频编辑剪切软件源码支持mp3等格式(教程含源码) 本文价值与收获 看完本文后,您将能够作出下面的界面 看完本文您将掌握的技能 支持剪切音频 支持复制音频 支持删除音频 支持un ...
- 抖音小程序基础之 目前提供哪些API(教程含源码)
抖音小程序基础之 目前提供哪些API(教程含源码) 小程序开发框架提供丰富的 字节跳动宿主 原生 API,可以方便的调起 字节跳动宿主 提供的能力,如获取系统信息等.详细介绍请参考 API 文档. 通 ...
- SwiftUI iOS 精品项目之每天收集的故事卡片(教程含源码)
实战需求 SwiftUI iOS 精品项目之每天收集的故事卡片(教程含源码) 每天收集的故事的卡片 本文价值与收获 看完本文后,您将能够作出下面的界面 核心功能 1.每天总共3个问题!选择一个您喜欢的 ...
- SwiftUI 界面大全之文本折叠书签动画组件3D(中文教程含源码)
实战需求 SwiftUI 界面大全之文本折叠书签动画组件3D(中文教程含源码) 本文价值与收获 看完本文后,您将能够作出下面的界面 基础知识 效果本身其实很简单,包括三件事: 图像的旋转 图像的垂直移 ...
- SwiftUI 绘图shape大全之 Teardrop水滴形状 (中文教程含源码)
实战需求 SwiftUI 绘图shape大全之 Teardrop水滴形状 (中文教程含源码) 本文价值与收获 看完本文后,您将能够作出下面的界面 基础知识 什么是Paths Paths主要用于绘制 ...
最新文章
- php tp5生成条形码,thinkphp5 + barcode 生成条形码
- win10镜像重装,快速设置之后无限重启怎么办?
- 苹果怎么换行打字_停课不停学!苹果电脑学习类软件推荐,丰富您的假期生活...
- SwiftUI之如何使用@EnvironmentObject在视图之间共享数据
- 二叉树的中序遍历—leetcode94
- 3.4 svm人脸识别
- RTCPeerConnection.onicecandidate属性
- Date类+DateFormat
- 学excel还是学python_已经会Excel了还需要学python吗?
- python 如何判断excel单元格为空_如何用python处理excel(二)
- Java开发必看JPA概念大全
- BoltDB 一个简单的纯 Go key/value 存储 [译]
- redis底层数据结构
- 如何自定义一个注解(@Annotation)
- 软件压力测试报告要怎么写,如何做接口压力测试?压力测试报告应该包含哪些结果?...
- 小学生自学奥数必备的这些书籍
- 家用无线TP-LINK路由器使用一段时间后,频繁断网解决办法之一
- java 判断是否信用卡_用java实现验证输入信用卡号码的正误
- h5 底部按钮兼容 iPhone X(解决底部横杠遮挡问题)
- 在线压缩pdf文件任意大小,在线压缩pdf文件大小
热门文章
- 芜湖光华学校优选云盒子教育云盘,自建专属数据中心
- 怎么彻底卸载office365?
- NG Toolset开发笔记--5GNR Resource Grid(50)
- 红光光浴真的有用吗?#大健康#红光光浴#红光#种光光学
- 计算机科学采用通知,关于采用合同模板的通知 科研〔2019〕134号
- CITECT HMI添加周期任务的方法
- 一个百度员工的离职感悟:听话/出活/忍耐/量化
- 详细设计之(人机界面设计问题)
- mysql item.pop_python flas中mysql的popluate组合框值
- 四川农业大学ZigBee复习重点