WPF旋转控件






4.90/5 (46投票s)
本文介绍了一个高度可配置的旋转拨盘控件。
引言
本文介绍了一个自定义旋转拨盘控件,它具有可配置的属性,包括标签、主刻度线和主刻度线增量。它包含一个简单的演示应用程序,其中有旋转拨盘控件的使用示例。
顶部的三个拨盘使用圆形位置指示器。
左上角的拨盘具有从0到1的连续值范围,每0.2个单位有一个主刻度线和黑色标签。拨盘宽度为150个单位。
中间顶部的拨盘具有从0到100的整数值,每20个单位有一个主刻度线和黑色标签。拨盘宽度为200个单位。
右上角的拨盘具有从0到50的整数值,每5个单位有一个主刻度线和黑色标签。拨盘宽度为100个单位。
底部的四个拨盘使用指针,并带有彩色弧线。
左下角的拨盘由两个拨盘控件构成。上面的拨盘具有从0到80的连续值范围,每20个单位有一个主刻度线和黑色标签。下面的拨盘具有从0到1.0的连续值范围,每0.1个单位有一个主刻度线和黑色标签。
左中和右中的底部拨盘具有从0到100的连续值范围,每10个单位有一个主刻度线和白色标签。每个拨盘的宽度为200个单位。左中的拨盘的标签位于刻度线之外,而右中的拨盘的标签位于刻度线之内。
右下角的拨盘跨越一个弧,而不是一个完整的圆。
背景
您需要理解C#和WPF的基础知识。
设计旋转拨盘
我的设计目标是使旋转拨盘尽可能可定制,同时又不使其过于复杂。为了实现这一目标,旋转控件由一系列同心圆或拨盘构成,其中一些是虚构的,用于布局标签和刻度线等组件。
标签放置在虚构的标签拨盘的边缘,其半径由LabelDialRadius
依赖属性设置。字体大小由FontSize
依赖属性设置,字体颜色由Foreground
依赖属性设置。
主刻度线放置在虚构的主标签拨盘的边缘,其半径由MajorTickDialRadius
依赖属性设置。每个主刻度线的长度由MajorTickLength
依赖属性设置。
次刻度线放置在虚构的次标签拨盘的边缘,其半径由MinorTickDialRadius
依赖属性设置。每个次刻度线的长度由MinorTickLength
依赖属性设置。
拨盘上第一个值的角度位置由StartAngleInDegrees
依赖属性设置。
拨盘上最后一个值的角度位置由EndAngleInDegrees
依赖属性设置。
这两个角度都相对于12点钟位置并沿顺时针方向。
有两种方法可以向旋转控件添加彩色段或完整的圆。
添加彩色段的第一种方法是使用Segments依赖属性。它包含一个RotaryControlSegment
对象的数组。每个对象定义了一个带有相关颜色和角度的段。这些段是连续的,从第一个值开始。所有段都具有相同的外部半径(由SegmentRadius
设置)和相同的厚度(由SegmentRadius
设置)。
第二种更灵活的方法是使用Arcs
依赖属性。每个弧都有自己的半径、起始角度、弧角度和厚度。
有一个内部拨盘可以用来表示一个旋钮。其半径由InnerDialRadius
依赖属性设置,其颜色由InnerDialFill
依赖属性设置。
指针样式由PointerType
依赖属性设置。支持的值包括圆形指针(表示旋钮上的位置指示器)和箭头指针(表示针式指针)。对于所有指针(除了圆形),都可以设置长度、宽度和填充。
许多属性都有合理的默认值,不需要显式定义。
Using the Code
创建一个带有中央旋钮和位置指示器的简单拨盘很容易,如下所示:
上面的代码是使用以下XAML代码创建的:
<view:RotaryControl Grid.Row="0" Grid.Column="3"
x:Name="_dialTemperature" Value="{Binding Temperature,
Mode=TwoWay}" FontBrush="Black" FontSize="20"
Foreground="Black" Background="Transparent">
<view:RotaryControl.MinimumValue>20</view:RotaryControl.MinimumValue>
<view:RotaryControl.NumberOfMajorTicks>9</view:RotaryControl.NumberOfMajorTicks>
<view:RotaryControl.MajorTickIncrement>10</view:RotaryControl.MajorTickIncrement>
<view:RotaryControl.MajorTickBrush>White</view:RotaryControl.MajorTickBrush>
<view:RotaryControl.NumberOfMinorTicks>4</view:RotaryControl.NumberOfMinorTicks>
<view:RotaryControl.MinorTickBrush>White</view:RotaryControl.MinorTickBrush>
<view:RotaryControl.OuterDialFill>SteelBlue</view:RotaryControl.OuterDialFill>
<view:RotaryControl.OuterDialBorder>Transparent</view:RotaryControl.OuterDialBorder>
<view:RotaryControl.OuterDialBorderThickness>1</view:RotaryControl.OuterDialBorderThickness>
<view:RotaryControl.InnerDialRadius>60</view:RotaryControl.InnerDialRadius>
<view:RotaryControl.PointerFill>
<LinearGradientBrush EndPoint="0.5,0" StartPoint="0.5,1">
<GradientStop Color="#DDDDDD" Offset="0"/>
<GradientStop Color="#BBBBBB" Offset="1.0"/>
</LinearGradientBrush>
</view:RotaryControl.PointerFill>
</view:RotaryControl>
上面定义了一个旋转拨盘,带有灰色中央控件、9个主刻度线、石板蓝色背景、主刻度线增量为10、黑色20点字体标签,以及透明背景。Value
绑定到视图模型中的Temperature
属性。
默认情况下,控件宽度为200个单位。
要调整控件大小,请使用LayoutTransform
。
<view:RotaryControl.LayoutTransform>
<ScaleTransform ScaleX="2" ScaleY="2"/>
</view:RotaryControl.LayoutTransform>
调整控件大小时,当然需要相应地调整字体大小。使用布局变换的优点是,与在代码中实现缩放相比,缩放是均匀的,并且需要的后台代码少得多。
要创建具有整数值的拨盘,请将Value
依赖属性绑定到视图模型中的整数/长整型属性。对于连续值,请将Value
绑定到视图模型中的double
属性。
如果您愿意,可以创建带有指针的控件,如下所示:
上面的代码是使用以下XAML代码创建的:
<view:RotaryControl Grid.Row="1" Grid.Column="5"
FontBrush="White" FontSize="10"
Foreground="Black" Background="Transparent" >
<view:RotaryControl.PointerFill>
<LinearGradientBrush EndPoint="0.5,0" StartPoint="0.5,1">
<GradientStop Color="#DDDDDD" Offset="0"/>
<GradientStop Color="#AAAAAA" Offset="1.0"/>
</LinearGradientBrush>
</view:RotaryControl.PointerFill>
<view:RotaryControl.OuterDialFill>
<LinearGradientBrush EndPoint="0.5,0" StartPoint="0.5,1">
<GradientStop Color="Black" Offset="0"/>
<GradientStop Color="Gray" Offset="0.5"/>
<GradientStop Color="Black" Offset="1.0"/>
</LinearGradientBrush>
</view:RotaryControl.OuterDialFill>
<view:RotaryControl.OuterDialBorder>
<LinearGradientBrush EndPoint="0.5,0" StartPoint="0.5,1">
<GradientStop Color="Gray" Offset="0"/>
<GradientStop Color="White" Offset="0.5"/>
<GradientStop Color="Gray" Offset="1.0"/>
</LinearGradientBrush>
</view:RotaryControl.OuterDialBorder>
<view:RotaryControl.OuterDialBorderThickness>3</view:RotaryControl.OuterDialBorderThickness>
<view:RotaryControl.InnerDialRadius>0</view:RotaryControl.InnerDialRadius>
<view:RotaryControl.InnerDialFill>
<LinearGradientBrush EndPoint="0.5,0" StartPoint="0.5,1">
<GradientStop Color="White" Offset="0"/>
<GradientStop Color="White" Offset="0.5"/>
<GradientStop Color="White" Offset="1.0"/>
</LinearGradientBrush>
</view:RotaryControl.InnerDialFill>
<view:RotaryControl.LabelDialRadius>48</view:RotaryControl.LabelDialRadius>
<view:RotaryControl.MajorTickDialRadius>65.5</view:RotaryControl.MajorTickDialRadius>
<view:RotaryControl.MajorTickLength>6</view:RotaryControl.MajorTickLength>
<view:RotaryControl.NumberOfMajorTicks>11</view:RotaryControl.NumberOfMajorTicks>
<view:RotaryControl.MajorTickIncrement>10</view:RotaryControl.MajorTickIncrement>
<view:RotaryControl.MajorTickBrush>White</view:RotaryControl.MajorTickBrush>
<view:RotaryControl.NumberOfMinorTicks>4</view:RotaryControl.NumberOfMinorTicks>
<view:RotaryControl.MinorTickBrush>White</view:RotaryControl.MinorTickBrush>
<view:RotaryControl.StartAngleInDegrees>210</view:RotaryControl.StartAngleInDegrees>
<view:RotaryControl.EndAngleInDegrees>150</view:RotaryControl.EndAngleInDegrees>
<view:RotaryControl.PointerAxleFill>
<LinearGradientBrush EndPoint="0.5,0" StartPoint="0.5,1">
<GradientStop Color="Gray" Offset="0"/>
<GradientStop Color="White" Offset="0.5"/>
<GradientStop Color="Gray" Offset="1.0"/>
</LinearGradientBrush>
</view:RotaryControl.PointerAxleFill>
<view:RotaryControl.PointerLength>45</view:RotaryControl.PointerLength>
<view:RotaryControl.PointerWidth>2</view:RotaryControl.PointerWidth>
<view:RotaryControl.PointerType>standard</view:RotaryControl.PointerType>
<view:RotaryControl.SegmentThickness>5</view:RotaryControl.SegmentThickness>
<view:RotaryControl.SegmentRadius>35</view:RotaryControl.SegmentRadius>
<view:RotaryControl.Segments>
<x:Array Type="{x:Type view:RotaryControlSegment}" >
<view:RotaryControlSegment Fill="YellowGreen" AngleInDegrees="210"/>
<view:RotaryControlSegment Fill="Gold" AngleInDegrees="30"/>
<view:RotaryControlSegment Fill="Orange" AngleInDegrees="30"/>
<view:RotaryControlSegment Fill="Crimson" AngleInDegrees="30"/>
</x:Array>
</view:RotaryControl.Segments>
</view:RotaryControl>
虽然上面的代码相当冗长,但应该相对容易理解。长度是由于属性数量众多,允许高度定制。彩色段由segments依赖属性定义。这是一个包含四个RotaryControlSegment
对象的数组,每个对象定义一个彩色段。这些段共享相同的半径和厚度。
您也可以创建跨越一个弧的拨盘,如下所示:
上面的代码是使用以下XAML代码创建的:
<view:RotaryControl Grid.Row="1" Grid.Column="7" FontBrush="Black"
FontSize="12" Foreground="Black" Background="Transparent"
Value="{Binding Pressure, Mode=TwoWay}" >
<view:RotaryControl.PointerType>rectangle</view:RotaryControl.PointerType>
<view:RotaryControl.PointerFill>
<LinearGradientBrush EndPoint="0.5,0" StartPoint="0.5,1">
<GradientStop Color="Black" Offset="0"/>
<GradientStop Color="#AAAAAA" Offset="1.0"/>
</LinearGradientBrush>
</view:RotaryControl.PointerFill>
<view:RotaryControl.PointerLength>50</view:RotaryControl.PointerLength>
<view:RotaryControl.PointerWidth>3</view:RotaryControl.PointerWidth>
<view:RotaryControl.PointerAxleFill>Black</view:RotaryControl.PointerAxleFill>
<view:RotaryControl.PointerAxleRadius>4</view:RotaryControl.PointerAxleRadius>
<view:RotaryControl.OuterDialFill>Transparent</view:RotaryControl.OuterDialFill>
<view:RotaryControl.OuterDialBorderThickness>0
</view:RotaryControl.OuterDialBorderThickness>
<view:RotaryControl.InnerDialRadius>10</view:RotaryControl.InnerDialRadius>
<view:RotaryControl.InnerDialFill>White</view:RotaryControl.InnerDialFill>
<view:RotaryControl.LabelDialRadius>77</view:RotaryControl.LabelDialRadius>
<view:RotaryControl.MinimumValue>0</view:RotaryControl.MinimumValue>
<view:RotaryControl.StartAngleInDegrees>210
</view:RotaryControl.StartAngleInDegrees>
<view:RotaryControl.EndAngleInDegrees>330</view:RotaryControl.EndAngleInDegrees>
<view:RotaryControl.MajorTickDialRadius>61
</view:RotaryControl.MajorTickDialRadius>
<view:RotaryControl.MajorTickLength>8</view:RotaryControl.MajorTickLength>
<view:RotaryControl.MajorTickWidth>1</view:RotaryControl.MajorTickWidth>
<view:RotaryControl.NumberOfMajorTicks>6</view:RotaryControl.NumberOfMajorTicks>
<view:RotaryControl.MajorTickIncrement>1</view:RotaryControl.MajorTickIncrement>
<view:RotaryControl.MajorTickBrush>Black</view:RotaryControl.MajorTickBrush>
<view:RotaryControl.MinorTickDialRadius>55
</view:RotaryControl.MinorTickDialRadius>
<view:RotaryControl.MinorTickLength>2</view:RotaryControl.MinorTickLength>
<view:RotaryControl.NumberOfMinorTicks>2</view:RotaryControl.NumberOfMinorTicks>
<view:RotaryControl.MinorTickBrush>Black</view:RotaryControl.MinorTickBrush>
<view:RotaryControl.SegmentThickness>15</view:RotaryControl.SegmentThickness>
<view:RotaryControl.SegmentRadius>67</view:RotaryControl.SegmentRadius>
<view:RotaryControl.Segments>
<x:Array Type="{x:Type view:RotaryControlSegment}" >
<view:RotaryControlSegment Fill="YellowGreen" AngleInDegrees="60"/>
<view:RotaryControlSegment Fill="Gold" AngleInDegrees="30"/>
<view:RotaryControlSegment Fill="Orange" AngleInDegrees="20"/>
<view:RotaryControlSegment Fill="Crimson" AngleInDegrees="10"/>
<view:RotaryControlSegment Fill="White" AngleInDegrees="10"/>
</x:Array>
</view:RotaryControl.Segments>
<view:RotaryControl.Arcs>
<x:Array Type="{x:Type view:RotaryControlArc}" >
<view:RotaryControlArc Fill="Black" StartAngleInDegrees="180"
AngleInDegrees="180" Radius="6" Thickness="1"
Stroke="Black" StrokeThickness="0"/>
<view:RotaryControlArc Fill="Black" StartAngleInDegrees="0"
AngleInDegrees="180" Radius="6" Thickness="1"
Stroke="Black" StrokeThickness="0"/>
<view:RotaryControlArc Fill="White" StartAngleInDegrees="200"
AngleInDegrees="10" Radius="67"
Thickness="35" StrokeThickness="0"/>
<view:RotaryControlArc Fill="White" StartAngleInDegrees="200"
AngleInDegrees="140" Radius="90" Thickness="23"
Stroke="Black" StrokeThickness="0"/>
<view:RotaryControlArc Fill="White" StartAngleInDegrees="200"
AngleInDegrees="140" Radius="52" Thickness="42"
Stroke="Black" StrokeThickness="0"/>
</x:Array>
</view:RotaryControl.Arcs>
</view:RotaryControl>
代码
拨盘实现为一个WPF UserControl
。
<UserControl x:Class="WpfRotaryControlDemo.View.RotaryControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<ResourceDictionary>
<LinearGradientBrush x:Key="InnerDialFillResource">
<LinearGradientBrush.StartPoint>0.5,1.0</LinearGradientBrush.StartPoint>
<LinearGradientBrush.EndPoint>0.5,0.0</LinearGradientBrush.EndPoint>
<GradientStop Color="#BBBBBB" Offset="0"/>
<GradientStop Color="#DDDDDD" Offset="1.0"/>
</LinearGradientBrush>
</ResourceDictionary>
</UserControl.Resources>
<Grid Name="_grid" Width="200"
Height="200" Background="Transparent">
<Ellipse x:Name="_ellipseOuterDial" Width="150"
Height="150" Stroke="Gainsboro"
StrokeThickness="4" Fill="SteelBlue" />
<Ellipse x:Name="_ellipseInnerDial"
Width="100" Height="100" Panel.ZIndex="98"/>
<Ellipse Name="_pointerCircle" Width="20"
Height="20" Stroke="Gainsboro" StrokeThickness="0" Panel.ZIndex="99">
<Ellipse.RenderTransform>
<TransformGroup>
<TranslateTransform x:Name="_markerTranslation" X="35" Y="0"/>
</TransformGroup>
</Ellipse.RenderTransform>
<Ellipse.Fill>
<LinearGradientBrush EndPoint="0.5,0" StartPoint="0.5,1">
<GradientStop Color="Red" Offset="0"/>
<GradientStop Color="DarkRed" Offset="1.0"/>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Path Name="_pointerStandard" Stroke="Red"
StrokeThickness="0" Fill="Red" Panel.ZIndex="100">
<Path.Data>
<PathGeometry>
<PathFigure StartPoint="100,100">
<LineSegment Point="100,98" x:Name="_pointerTopLeft"/>
<LineSegment Point="140,98" x:Name="_pointerTopRight"/>
<LineSegment Point="150,100" x:Name="_pointerTip"/>
<LineSegment Point="140,102" x:Name="_pointerBottomRight"/>
<LineSegment Point="100,102" x:Name="_pointerBottomLeft"/>
<LineSegment Point="100,100"/>
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
<Path Name="_pointerArrow" Stroke="Red"
StrokeThickness="0" Fill="Red" Panel.ZIndex="100">
<Path.Data>
<PathGeometry>
<PathFigure StartPoint="100,100">
<LineSegment Point="100,98" x:Name="_pointerArrowTopLeft"/>
<LineSegment Point="150,100" x:Name="_pointerArrowTip"/>
<LineSegment Point="100,102" x:Name="_pointerArrowBottomLeft"/>
<LineSegment Point="100,100"/>
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
<Path Name="_pointerRectangle" Stroke="Red"
StrokeThickness="0" Fill="Red" Panel.ZIndex="100">
<Path.Data>
<PathGeometry>
<PathFigure StartPoint="100,100">
<LineSegment Point="100,98" x:Name="_pointerRectangleTopLeft"/>
<LineSegment Point="150,98" x:Name="_pointerRectangleTopRight"/>
<LineSegment Point="150,102" x:Name="_pointerRectangleBottomRight"/>
<LineSegment Point="100,102" x:Name="_pointerRectangleBottomLeft"/>
<LineSegment Point="100,100"/>
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
<Path Name="_pointerAxle" Stroke="Black"
StrokeThickness="0" Fill="Black" Panel.ZIndex="101">
<Path.Data>
<PathGeometry>
<PathFigure StartPoint="100,97" x:Name="_pointerPathFigure">
<ArcSegment Point="100,103" Size="3,3"
SweepDirection="Clockwise" IsLargeArc="True"
x:Name="_pointerAxleArc1"/>
<ArcSegment Point="100,97" Size="3,3"
SweepDirection="Clockwise" IsLargeArc="True"
x:Name="_pointerAxleArc2"/>
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
</Grid>
</UserControl>
上面的XAML使用一个外部圆作为控件轮廓,一个内部圆作为旋转旋钮,以及各种形状作为指针,来创建基本的旋转控件。
刻度线和标签是在CreateControl
方法中创建的,该方法从构造函数调用。据我所知,这在XAML中是无法完成的。每个刻度线都使用Polyline
创建,每个注释都使用Label
创建。
控件具有大量的依赖属性。
值 | 当前读数。如果您想绑定到此值: Value="{Binding CoupledValue, Mode=TwoWay}" 请注意,Mode设置为TwoWay,因为默认是OneWayToSource。 |
MinimumValue | 允许的最小值 |
FontBrush | 用于绘制标签拨盘周围数字的笔刷 |
StartAngleInDegrees | 相对于12点钟位置的第一个主刻度线的角度 |
EndAngleInDegrees | 相对于12点钟位置的最后一个主刻度线的角度 |
MajorTickDialRadius | 主刻度线拨盘的半径 |
MajorTickLength | 每个主刻度线的长度 |
MajorTickWidth | 每个主刻度线的宽度 |
NumberOfMajorTicks | 主刻度线的数量(不包括零处的那个) |
MajorTickIncrement | 相邻主刻度线之间的数值增量 |
MajorTickBrush | 用于绘制主刻度线的笔刷 |
MinorTickDialRadius | 次刻度线拨盘的半径 |
MinorTickLength | 每个次刻度线的长度 |
NumberOfMinorTicks | 每个主刻度线增量的次刻度线数量 |
MinorTickBrush | 用于绘制次刻度线的笔刷 |
InnerDialRadius | 内部拨盘的半径。内部拨盘可用于绘制带有圆形位置指示器的旋转旋钮。 |
InnerDialFill | 用于填充内部拨盘的笔刷 |
OuterDialFill | 用于填充外部拨盘的笔刷。外部拨盘包含其他拨盘、标签、刻度线和指针。 |
OuterDialBorder | 用于填充外部拨盘边框的笔刷。 |
OuterDialBorderThickness | 外部拨盘边框的厚度。 |
SegmentThickness | 彩色段的宽度。 |
SegmentRadius | 段的半径。 |
Segments | 可选的连续彩色段数组,它们共享相同的半径和厚度。 |
弧形 | 可选的彩色段数组,每个段都有自己的起始角度、弧角度、半径和厚度。 |
PointerType | 指针的类型。允许的值如下:
|
默认情况下,控件宽度为200个单位。这在XAML和关联代码中设置。
注释
创建带有圆形边框内的两个或多个仪表板的复合拨盘控件并不难。演示应用程序包含一个带有两个半圆形仪表板的示例。
请注意,控件是矩形的,不可见区域会吸收鼠标和键盘事件。这可能导致半圆形拨盘出现问题。解决方案是确保它们之间有良好的间隔。
假设您具备C#和WPF的基本知识,要更改拨盘的外观以满足您的需求是相当简单的。
历史
- 2019年5月3日:第一个版本
- 2019年8月21日:第二版,代码已重写,使其更加灵活。
- 2019年8月23日:对Value依赖属性描述进行了小更新。