65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF旋转控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (46投票s)

2019年5月3日

CPOL

8分钟阅读

viewsIcon

46757

downloadIcon

3256

本文介绍了一个高度可配置的旋转拨盘控件。

引言

本文介绍了一个自定义旋转拨盘控件,它具有可配置的属性,包括标签、主刻度线和主刻度线增量。它包含一个简单的演示应用程序,其中有旋转拨盘控件的使用示例。

顶部的三个拨盘使用圆形位置指示器。

左上角的拨盘具有从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 指针的类型。允许的值如下:
  • "circle":圆形位置指示器
  • "arrow":三角形指针
  • "rectangle":矩形指针
  • "standard":剑形指针
   

默认情况下,控件宽度为200个单位。这在XAML和关联代码中设置。

注释

创建带有圆形边框内的两个或多个仪表板的复合拨盘控件并不难。演示应用程序包含一个带有两个半圆形仪表板的示例。

请注意,控件是矩形的,不可见区域会吸收鼠标和键盘事件。这可能导致半圆形拨盘出现问题。解决方案是确保它们之间有良好的间隔。

假设您具备C#和WPF的基本知识,要更改拨盘的外观以满足您的需求是相当简单的。

历史

  • 2019年5月3日:第一个版本
  • 2019年8月21日:第二版,代码已重写,使其更加灵活。
  • 2019年8月23日:对Value依赖属性描述进行了小更新。 
© . All rights reserved.