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

Windows Phone 7的自定义仪表控件:第三部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (35投票s)

2011年3月22日

CPOL

14分钟阅读

viewsIcon

73445

downloadIcon

1566

本文是该系列文章的第三篇,介绍了用于 WP7 的一套仪表盘控件。本文重点介绍仪表盘中使用的指示器的实现。

引言

本文是介绍为 Windows Phone 7 平台设计和实现一套自定义仪表盘控件的系列文章中的第三篇。第一篇文章 《自定义仪表盘》 讨论了设计注意事项和代码使用,而第二篇文章 《刻度实现》 更深入地探讨了刻度的实现。在本文中,我将介绍指示器的实现。在阅读本文之前,强烈建议您阅读该系列文章的 第一篇第二篇。这样您就能更好地理解本文内容以及指示器在整体图景中的作用。

本系列文章

您可以在下方找到本系列所有文章的列表

指示器基类

这是所有其他指示器应从中派生的基类。`Indicator` 类派生自 `Control` 基类,并且是 `abstract` 类,因为它不应被直接使用。此类公开了所有指示器的通用属性。这些属性是指示器在刻度上指向的 `Value` 以及指示器的 `Owner`(指示器所属的刻度)。

这两个属性可以在下图看到

`Owner` 属性是一个常规的 CLR 属性,用于设置指示器所属的刻度。此属性的定义可以在下面的代码中看到

public Scale Owner
{
    get
    {
        return this.owner;
    }
    internal set
    {
        if (this.owner != value)
        {
            this.owner = value;
            UpdateIndicator(owner);
        }
    }
}

如您从上面的代码中看到的,每次将指示器分配给新刻度时,指示器都会更新。这是通过调用私有函数 `UpdateIndicator` 来完成的。在该函数中,指示器的 `Value` 属性被强制限制在所有者刻度范围内。此方法的定义如下所示

private void UpdateIndicator(Scale owner)
{
    if (owner != null)
    {
        if (Value < owner.Minimum)
            Value = owner.Minimum;
        if (Value > owner.Maximum)
            Value = owner.Maximum;
    }
    UpdateIndicatorOverride(owner);
}

如您所见,在强制值之后,该方法还会调用 `UpdateIndicatorOverride` 方法。这是一个虚方法,可以在派生类中重写,以便在所有者更改时添加额外的逻辑。

`Indicator` 基类的 `Value` 属性是一个依赖属性。此属性有一个更改处理程序,如下所示

private static void ValuePropertyChanged(DependencyObject o, 
        DependencyPropertyChangedEventArgs e)
{
    Indicator ind = o as Indicator;
    if (ind != null)
    {
        ind.OnValueChanged((double)e.NewValue, (double)e.OldValue);
    }
}
protected virtual void OnValueChanged(double newVal, double oldVal) { }

如您所见,更改处理程序会调用 `onValueChanged` 虚方法。这将在派生类中重写,以正确更新指示器。

最后需要讨论的重要代码是 `MeasureOverride` 方法。我重载了这个方法,以便可以自动设置指示器的所有者。在实例化指示器控件并开始布局过程后,将设置指示器的 `Owner` 属性。此方法的定义可以在下面的代码中看到

protected override Size MeasureOverride(Size availableSize)
{
    //the main purpose of this override is to set the owner for the 
    //indicator. The actual measuring calculation will be done in 
    //the derived classes
    DependencyObject parent = base.Parent;
    while (parent != null)
    {
        Scale scale = parent as Scale;
        if (scale != null)
        {
            this.Owner = scale;
            break;
        }
        FrameworkElement el = parent as FrameworkElement;
        if (el != null)
        {
            parent = el.Parent;
        }
    }
    return base.MeasureOverride(availableSize);
}

如您从上面的代码中看到的,该方法会尝试递归地设置指示器的所有者。仅当父级是 `Scale` 类型时,才会设置 `Owner` 属性。

BarIndicator 类

用于构建某些指示器的另一个基类是 `BarIndicator` 类。此类添加了特定于条形指示器的属性。条形指示器由实线路径表示。对于线性刻度,指示器将由矩形表示。对于径向刻度,指示器将由圆弧段表示。特定属性是条形的厚度和条形的画笔。这两个属性可以在下图看到。

如您从图中看到的,此类也是 `abstract` 的。这是因为条形指示器的形状取决于指示器使用的刻度类型。这两个属性是具有附加更改处理程序的依赖属性。更改处理程序显示在下面的代码中

private static void BarThicknessPropertyChanged(DependencyObject o, 
                    DependencyPropertyChangedEventArgs e)
{
    BarIndicator ind = o as BarIndicator;
    if (ind != null)
    {
        ind.OnBarThicknesChanged((int)e.NewValue, (int)e.OldValue);
    }
}
private static void BarBrushPropertyChanged(DependencyObject o, 
                    DependencyPropertyChangedEventArgs e)
{
}
protected virtual void OnBarThicknesChanged(int newVal, int oldVal) { }

如您所见,当厚度更改时,代码会调用虚拟的 `OnBarThicknessChanged` 方法。这将在派生类中使用以正确更新指示器。由于 `Brush` 是可自由冻结的,并且在属性更改时会自动更新,因此条形画笔更改处理程序不执行任何操作。

LinearBarIndicator 类

此类是我将要讨论的第一个具体类。它是可用于线性刻度的条形指示器。此类派生自 `BarIndicator` 基类,并且不定义其他属性。由于这是一个具体的 `Control` 类,因此我在 `generic.xaml` 文件中为其添加了一个默认模板。`LinearBarIndicator` 的默认指示器模板可以在下面的列表中看到

<Style TargetType="loc:LinearBarIndicator" >
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="loc:LinearBarIndicator">
                <Rectangle Fill="{TemplateBinding BarBrush}"></Rectangle>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

如您所见,模板很简单。它甚至没有可以在代码隐藏中使用的部分名称。我这样做是因为我希望指示器非常简单。如果您需要更复杂的线性指示器,可以添加一个带有部分名称的路径而不是矩形,并在代码隐藏中处理该路径。一个这样的例子是如果您想要您的线性指示器看起来像一个正弦函数。

此指示器的代码隐藏只有几个方法。我将要讨论的第一个方法是 `OnValueChanged` 重写。此方法的定义可以在下面的列表中看到

protected override void OnValueChanged(double newVal, double oldVal)
{
    //every time the value changes set the width and the hight of
    //the indicator.
    //setting these properties will trigger the measure and arrange passes
    LinearScale scale = Owner as LinearScale;
    if (scale != null)
    {
        Size sz = GetIndicatorSize(scale);
        Width = sz.Width;
        Height = sz.Height;
    }
}

该方法首先检查所有者是否为 `LinearScale`。如果是,则使用 `GetIndicatorSize` 帮助方法计算指示器的所需大小,并设置控件的 `Width` 和 `Height` 属性。设置这些属性将再次触发测量和排列过程。这是必要的,以便我们可以将指示器重新绘制到正确的大小。

`GetindicatorSize` 方法的代码可以在下面的列表中看到

private Size GetIndicatorSize(LinearScale owner)
{
    //gets the size of the indicator based on the current value and the
    //owner dimensions
    double width = 0, height = 0;
    if (owner.Orientation == Orientation.Horizontal)
    {
        height = BarThickness;
        width = GetExtent(owner.ActualWidth, Value, 
                          owner.Maximum, owner.Minimum);
    }
    else
    {
        width = BarThickness;
        height = GetExtent(owner.ActualHeight, Value, 
                           owner.Maximum, owner.Minimum);
    }
    return new Size(width, height);
}

//gets the length the indicator should have
private double GetExtent(double length, double value, double max, double min)
{
    return length * (value - min) / (max - min);
}

此方法通过使用另一个也已呈现的帮助方法来获取指示器的所需大小。通过指定可用总长度、指示器的当前值和刻度范围,`GetExtent` 帮助方法确定指示器应具有的长度。此方法不指定此长度应该是控件的宽度还是高度。这在 `GetIndicatorSize` 方法中根据指示器所有者的 `Orientation` 属性来确定。

最后两个方法是 `MeasureOverride` 和 `ArrangeOverride`。`MeasureOverride` 方法的定义可以在下面的列表中看到

protected override Size MeasureOverride(Size availableSize)
{
    //call the base version to set the owner
    base.MeasureOverride(availableSize);
    Size size = new Size();
    //get the desired size of the indicator based on the owner size
    LinearScale owner = Owner as LinearScale;
    if (owner != null)
    {
        size = GetIndicatorSize(owner);
    }
    return size;
}

如您所知,此方法用于确定重写它的控件的所需大小。在此实现中,我首先调用基类实现,以便可以设置控件的所有者。接下来,我检查所有者是否为 `LinearScale`,如果是,我调用 `GetIndicatorSize` 帮助方法来获取所需的尺寸。最后,我返回大小为 0 或 `GetIndicatorSize` 方法的结果。

`ArrangeOverride` 方法将用于在可用大小中定位指示器。此定位将取决于刻度的方向和刻度线放置。定义如下所示

protected override Size ArrangeOverride(Size arrangeBounds)
{
    //with every arrange pass the size of the indicator should 
    //be set again. this is important if the orientation is
    //vertical as the start position changes every time the value changes
    //so the indicator should be rearranged
    LinearScale scale = Owner as LinearScale;
    Size sz = base.ArrangeOverride(arrangeBounds);
    if (scale != null)
    {
        //reset the indicator size after each arrange phase
        sz = GetIndicatorSize(scale);
        Width = sz.Width;
        Height = sz.Height;
        Point pos = scale.GetIndicatorOffset(this);
        TranslateTransform tt = new TranslateTransform();
        tt.X = pos.X;
        tt.Y = pos.Y;
        this.RenderTransform = tt;
    }
    return sz;
}

在此方法中,指示器通过使用 `TranslateTransform` 进行排列。为了获得要放置指示器的偏移位置,该方法调用一个内部的 `LinearScale` 函数。另一个需要注意的重要事项是,我重置了指示器的宽度和高度。这是必需的,因为每次调用此方法时,指示器都应具有新尺寸(即使 `Value` 属性保持不变)。我在上一篇文章中没有介绍 `GetIndicatorOffset` 方法,因为我想在这里讨论它。此方法根据刻度方向和刻度线放置来获取指示器应放置的偏移量。此方法的定义如下所示

internal Point GetIndicatorOffset(Indicator ind)
{
    //get's the offset at which the indicator is placed inside the owner
    Point pos = new Point();
    if (Orientation == Orientation.Horizontal)
    {

        if (TickPlacement == LinearTickPlacement.TopLeft)
        {
            pos.X = 0;
            pos.Y = GetLabels().Max(p => p.DesiredSize.Height) + 
              GetTicks().Max(p => p.DesiredSize.Height) + RangeThickness + 5;
        }
        else
        {
            pos.X = 0;
            pos.Y = ActualHeight - ind.DesiredSize.Height - 
              (GetLabels().Max(p => p.DesiredSize.Height) + 
               GetTicks().Max(p => p.DesiredSize.Height) + RangeThickness + 7);
        }
    }
    else
    {
        if (TickPlacement == LinearTickPlacement.TopLeft)
        {
            pos.X = GetLabels().Max(p => p.DesiredSize.Width) + 
              GetTicks().Max(p => p.DesiredSize.Width) + RangeThickness + 6;
            pos.Y = ActualHeight - ind.DesiredSize.Height;
        }
        else
        {
            pos.X = ActualWidth - ind.DesiredSize.Width - 
             (GetLabels().Max(p => p.DesiredSize.Width) + 
              GetTicks().Max(p => p.DesiredSize.Width) + RangeThickness + 6);
            pos.Y = ActualHeight - ind.DesiredSize.Height;
        }
    }
    return pos;
}

下图展示了一组使用此类型指示器的四个线性刻度

如您所见,指示器的形状和位置会根据刻度的 `Orientation` 和 `TickPlacement` 属性而变化。

RadialBarIndicator 类

此类是另一个派生自 `BarIndicator` 基类的类。这将表示径向刻度的条形指示器。`RadialBarIndicator` 的默认模板可以在下面的列表中看到

<Style TargetType="loc:RadialBarIndicator">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="loc:RadialBarIndicator">
                <Path x:Name="PART_BAR" Fill="{TemplateBinding BarBrush}"/>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

指示器由一个路径表示,该路径将在代码隐藏中每次值更改时设置。指示器的形状需要经常更改。第一次发生这种情况是在所有者更改时。为了处理此问题,我重写了 `UpdateIndicatorsOverride` 方法。代码可以在下面的列表中看到

protected override void UpdateIndicatorOverride(Scale owner)
{
    base.UpdateIndicatorOverride(owner);
    RadialScale scale = Owner as RadialScale;
    if (scale != null)
    {
        SetIndicatorGeometry(scale, Value);
    }
}

当值更改或条形厚度更改时,也需要执行同样的操作。这些方法的定义可以在下面的列表中看到

protected override void OnValueChanged(double newVal, double oldVal)
{
    RadialScale scale = Owner as RadialScale;
    if (scale != null)
    {
        SetIndicatorGeometry(scale, Value);
    }
}

protected override void OnBarThicknesChanged(int newVal, int oldVal)
{
    base.OnBarThicknesChanged(newVal, oldVal);
    RadialScale scale = Owner as RadialScale;
    if (scale != null)
    {
        SetIndicatorGeometry(scale, Value);
    }
}

所有这些方法都使用一个私有帮助程序来创建必要的几何形状。该帮助方法的定义可以在下面的列表中看到

private void SetIndicatorGeometry(RadialScale scale, double value)
{
    if (thePath != null)
    {
        double min = scale.MinAngle;
        double max = scale.GetAngleFromValue(Value);
        if (scale.SweepDirection == SweepDirection.Counterclockwise)
        {
            min = -min;
            max = -max;
        }
        double rad = scale.GetIndicatorRadius();
        if (rad > BarThickness)
        {
            Geometry geom = RadialScaleHelper.CreateArcGeometry(
                              min, max, rad, BarThickness, scale.SweepDirection);
            //stop the recursive loop. only set a new geometry
            //if it is different from the current one
            if (thePath.Data == null || thePath.Data.Bounds != geom.Bounds)
                thePath.Data = geom;
        }
    }
}

该方法首先确定指示器的最小和最大角度。这是通过调用名为 `GetAngleFromValue` 的内部刻度方法来完成的。此方法如下所示

internal double GetAngleFromValue(double value)
{
    //ANGLE=((maxa-mina)*VAL+mina*maxv-maxa*minv)/(maxv-minv)
    double angle = ((MaxAngle - MinAngle) * value + MinAngle * 
           Maximum - MaxAngle * Minimum) / (Maximum - Minimum);
    return angle;
}

接下来需要做的是计算指示器的半径。这是通过调用 `GetindicatorRadius` 内部刻度方法来完成的。此方法的定义可以在下面看到

internal double GetIndicatorRadius()
{
    double maxRad = RadialScaleHelper.GetRadius(RadialType, 
           new Size(ActualWidth, ActualHeight), MinAngle, MaxAngle, SweepDirection);
    return maxRad - GetLabels().Max(p => p.DesiredSize.Height) - 
           GetTicks().Max(p => p.DesiredSize.Height) - RangeThickness - 3;
}

如您所见,该方法委托给 `RadialScaleHelper` 以获取允许的最大半径。然后从最大值中减去标签、刻度线和范围的高度。

然后使用上一篇文章中描述的 `CreateArcGeometry` 方法创建指示器的形状。

最后剩余的方法是 `MeasureOverride` 和 `ArrangeOverride`。它们将用于计算指示器的所需大小并对其进行排列。`MeasureOverride` 方法的定义可以在下面的列表中看到

protected override Size MeasureOverride(Size availableSize)
{
    //call the base version to set the parent
    base.MeasureOverride(availableSize);
    //return all the available size
    double width = 0, height = 0;
    if (!double.IsInfinity(availableSize.Width))
        width = availableSize.Width;
    if (!double.IsInfinity(availableSize.Height))
        height = availableSize.Height;
    RadialScale scale = Owner as RadialScale;
    if (scale != null)
    {
        //every time a resize happens the indicator needs to be redrawn
        SetIndicatorGeometry(scale, Value);
    }
    return new Size(width, height);
}

该方法首先调用基类版本以设置所有者。在此之后,该方法使用上面描述的帮助方法计算并设置指示器几何形状。`ArrangeOverride` 方法的定义可以在下面看到

protected override Size ArrangeOverride(Size arrangeBounds)
{
    TranslateTransform tt = new TranslateTransform();
    RadialScale scale = Owner as RadialScale;
    if (scale != null)
    {
        //calculate the geometry again. the first time this
        //was done the owner had a size of (0,0)
        //and so did the indicator. Once the owner has
        //the correct size (measureOveride has been called)
        //i should re-calculate the shape of the indicator
        SetIndicatorGeometry(scale, Value);
        Point center = scale.GetIndicatorOffset();
        tt.X = center.X;
        tt.Y = center.Y;
        RenderTransform = tt;
    }
    return base.ArrangeOverride(arrangeBounds);
}

如您所见,确定了刻度的中心,并通过使用 `TranslateTransform` 来偏移指示器。`GetIndicatorOffset` 内部刻度方法如下所示

internal Point GetIndicatorOffset()
{
    return RadialScaleHelper.GetCenterPosition(RadialType, 
           new Size(ActualWidth, ActualHeight), 
           MinAngle, MaxAngle, SweepDirection);
}

此方法委托给我在系列第二篇文章中讨论过的 `GetCenterPosition` 帮助方法。

下图展示了一组使用此类型指示器的三个径向刻度

如您从上图看到的,通过组合多个刻度并以正确的方式放置它们,您可以获得一些非常好的仪表盘。用于这三个仪表盘的代码可以在下面的列表中看到

<scada:RadialScale Grid.RowSpan="2" Grid.ColumnSpan="2"
       RangeThickness="5" MinorTickStep="10" MajorTickStep="50"
       Maximum="400" MinAngle="-90" MaxAngle="90">
    <scada:RadialScale.Ranges>
        <scada:GaugeRange Color="Red" Offset="20" />
        <scada:GaugeRange Color="Orange" Offset="40" />
        <scada:GaugeRange Color="WhiteSmoke" Offset="60" />
        <scada:GaugeRange Color="{StaticResource PhoneAccentColor}" Offset="100" />
    </scada:RadialScale.Ranges>
    <scada:RadialBarIndicator 
       Value="{Binding ElementName=slider,Path=Value}" 
       BarThickness="20" BarBrush="{StaticResource PhoneAccentBrush}" />
    
</scada:RadialScale>
<scada:RadialScale RangeThickness="5" MinorTickStep="25" MajorTickStep="50"
       MinAngle="110" MaxAngle="160" RadialType="Quadrant" 
       SweepDirection="Counterclockwise" 
       Width="130" Height="130" Margin="80,155,10,44" Grid.RowSpan="2">
    <scada:RadialScale.Ranges>
        <scada:GaugeRange Color="GreenYellow" Offset="50" />
        <scada:GaugeRange Color="Gold" Offset="75" />
        <scada:GaugeRange Color="Red" Offset="100" />
    </scada:RadialScale.Ranges>
    
    <scada:RadialBarIndicator 
       Value="{Binding ElementName=slider2,Path=Value, Mode=TwoWay}" 
       BarBrush="Gold" BarThickness="60"/>
</scada:RadialScale>
<scada:RadialScale RangeThickness="5" MinorTickStep="25" MajorTickStep="50"
       MinAngle="110" MaxAngle="160" RadialType="Quadrant" 
       SweepDirection="Clockwise" Width="130" Height="130" 
       Margin="10,157,80,42" Grid.Column="1" Grid.RowSpan="2">
    <scada:RadialScale.Ranges>
        <scada:GaugeRange Color="GreenYellow" Offset="50" />
        <scada:GaugeRange Color="Gold" Offset="75" />
        <scada:GaugeRange Color="Red" Offset="100" />
    </scada:RadialScale.Ranges>

    <scada:RadialBarIndicator 
      Value="{Binding ElementName=slider2,Path=Value, Mode=TwoWay}" 
      BarBrush="Gold" BarThickness="60"/>
</scada:RadialScale>

MarkerIndicator 类

我实现的下一个具体指示器是标记指示器。此标记指示器可用于线性和径向刻度。此类派生自 `Indicator` 基类,并定义了一个额外的属性。这是 `MarkerTemplate` 属性。该控件的默认模板可以在下面的列表中看到

<Style TargetType="loc:MarkerIndicator">
    <Setter Property="MarkerTemplate">
        <Setter.Value>
            <DataTemplate>
                <Path Data="M0,0 L6,0 L6,20 L0,20 Z"
                          Fill="White" />
            </DataTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="loc:MarkerIndicator">
                <ContentPresenter x:Name="PART_Marker"
                      ContentTemplate="{TemplateBinding MarkerTemplate}" />
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

从上面的 XAML 中,您可以看到默认模板是一个白色矩形。这通过将 `MarkerTemplate` 属性绑定到 `ContentPresenter` 的 `ContentTemplate` 属性来实现。

我将要讨论的此指示器的第一个方法是 `Arrange` 方法。此方法用于通过调用基类 `Indicator` 中最初定义的 `Arrange` 方法来排列指示器。此方法的定义可以在下面的代码中看到

public override void Arrange(Size finalSize)
{
    base.Arrange(DesiredSize);
    //call method to arrange the marker
    SetIndicatorTransforms();
    PositionMarker();
}

如您所见,该方法使用指示器的 `DesiredSize` 调用基类实现。之后,该方法定位并旋转指示器。这分两步完成

  1. 将指示器的 `RenderTransform` 属性设置为 `TransformGroup`,然后
  2. 修改该组中的变换。

`RenderTransform` 属性在 `SetIndicatorTransforms` 方法中设置。此方法的定义可以在下面看到

private void SetIndicatorTransforms()
{
    if (RenderTransform is MatrixTransform)
    {
        TransformGroup tg = new TransformGroup();
        TranslateTransform tt = new TranslateTransform();
        RotateTransform rt = new RotateTransform();

        tg.Children.Add(rt);
        tg.Children.Add(tt);

        this.RenderTransformOrigin = new Point(0.5, 0.5);
        this.RenderTransform = tg;
    }
}

`PositionMarker` 方法是大部分工作完成的地方。此方法首先检查指示器是否具有 `Owner`。如果没有,则返回。

if (Owner == null)
    return;

在此之后,该方法会检查所有者类型。根据类型,指示器将以不同的方式排列。如果所有者是径向刻度,则该方法执行以下操作

  • 计算指示器应旋转的角度。
  • 在变换组中设置 `RotateTransform`。
  • 计算指示器位置。
  • 通过在变换组中设置 `TranslateTransform` 来将指示器移动到所需位置。

以上所有步骤都可以在下面的列表中看到

RadialScale rs = (RadialScale)Owner;
//get the angle based on the value
double angle = rs.GetAngleFromValue(Value);
if (rs.SweepDirection == SweepDirection.Counterclockwise)
{
    angle = -angle;
}
//rotate the marker by angle
TransformGroup tg = RenderTransform as TransformGroup;
if (tg != null)
{
    RotateTransform rt = tg.Children[0] as RotateTransform;
    if (rt != null)
    {
        rt.Angle = angle;
    }
}
//position the marker based on the radius
Point offset = rs.GetIndicatorOffset();
double rad = rs.GetIndicatorRadius();

//position the marker
double px = offset.X + (rad - DesiredSize.Height / 2) * Math.Sin(angle * Math.PI / 180);
double py = offset.Y - (rad - DesiredSize.Height / 2) * Math.Cos(angle * Math.PI / 180);
px -= DesiredSize.Width / 2;
py -= DesiredSize.Height / 2;
if (tg != null)
{
    TranslateTransform tt = tg.Children[1] as TranslateTransform;
    if (tt != null)
    {
        tt.X = px;
        tt.Y = py;
    }
}

为了在线性刻度内排列指示器,使用了以下代码

LinearScale ls = Owner as LinearScale;
Point offset = ls.GetIndicatorOffset(this);
//the getIndicatorOffset returns only one correct dimension
//for marker indicators the other dimension will have to be calculated again
if (ls.Orientation == Orientation.Horizontal)
{
    offset.X = ls.ActualWidth * (Value - ls.Minimum) / 
          (ls.Maximum - ls.Minimum) - DesiredSize.Width / 2;
}
else
{
    offset.Y = ls.ActualHeight - ls.ActualHeight * (Value - ls.Minimum) / 
              (ls.Maximum - ls.Minimum) - DesiredSize.Height / 2;
}
TransformGroup tg = RenderTransform as TransformGroup;
if (tg != null)
{
    TranslateTransform tt = tg.Children[1] as TranslateTransform;
    if (tt != null)
    {
        tt.X = offset.X;
        tt.Y = offset.Y;
    }
}

此代码首先确定指示器应具有的偏移量。这是通过调用 `LinearScale` 类中的 `GetIndicatorOffset` 内部方法来完成的。此方法最初设计用于返回 `LinearBarIndicator` 的偏移量,并在前面已详细介绍。此方法不会返回标记指示器的正确偏移量。这将在接下来的几行中得到纠正。必须修复此代码,因为偏移量不应取决于指示器类型。

最后,代码设置 `TranslateTransform` 以将指示器移动到所需位置。下图展示了一些使用此类型指示器的示例

构建上面所示指示器所用的代码可以在下面的列表中看到

<scada:MarkerIndicator Value="40" />
<scada:MarkerIndicator Value="20" >
    <scada:MarkerIndicator.MarkerTemplate>
        <DataTemplate>
            <Ellipse Width="15" Height="15" Fill="Red"/>
        </DataTemplate>
    </scada:MarkerIndicator.MarkerTemplate>
</scada:MarkerIndicator>

NeedleIndicator 类

我为此库实现的最后一个指示器是径向刻度的指针指示器。这是另一个具体的指示器类。此类派生自 `Indicator` 基类,并且不定义其他属性。该控件的默认模板可以在下面的列表中看到

<Style TargetType="loc:NeedleIndicator">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="loc:NeedleIndicator">
                <Path x:Name="PART_Needle" 
                   Data="M7,0 L0,10 L5,10 L5,70 L8,70 L8,10 L13,10 Z" Fill="White" />
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

默认模板是一个定义箭头的 `Path`。这可以在下图看到

我将要在这里讨论的第一个方法是 `Arrange` 方法。此方法将用于通过使用一组三个变换来排列指针指示器

  • 比例变换 - 绘制指针一直到刻度附近,而不管刻度的大小
  • 旋转变换 - 根据 `Value` 属性旋转指针
  • 平移变换 - 根据径向类型属性定位指针

此方法的定义可以在下面的列表中看到

public override void Arrange(Size finalSize)
{
    base.Arrange(DesiredSize);
    //arrange the indicator in the center
    SetIndicatorTransforms();
    RadialScale scale = Owner as RadialScale;
    if (scale != null)
    {
        Point center = scale.GetIndicatorOffset();
        TransformGroup tg = RenderTransform as TransformGroup;
        if (tg != null)
        {
            //add a scale in order to make the needle to feet exactly inside the range
            ScaleTransform st = tg.Children[0] as ScaleTransform;
            double rad = scale.GetIndicatorRadius();
            if (st != null && DesiredSize.Height != 0 && 
                !double.IsInfinity(DesiredSize.Height) && rad > 0)
            {
                //factor is the radius devided by the height
                double factor = rad / ( DesiredSize.Height);
                st.ScaleX = factor;
                st.ScaleY = factor;
            }
            TranslateTransform tt = tg.Children[2] as TranslateTransform;
            if (tt != null)
            {
                tt.X = center.X - DesiredSize.Width / 2;
                tt.Y = center.Y - DesiredSize.Height;
            }
        }
    }
}

此方法首先通过调用基类版本并传入指示器的所需大小来排列指示器。

接下来,此方法设置指示器的 `RenderTransform` 属性。这是在 `SetIndicatorTransforms` 方法中完成的。此方法仅在属性具有其默认值(即矩阵变换)时才设置 `RenderTransform` 属性。这可以在下面的列表中看到

private void SetIndicatorTransforms()
{
    if (RenderTransform is MatrixTransform)
    {
        TransformGroup tg = new TransformGroup();
        TranslateTransform tt = new TranslateTransform();
        RotateTransform rt = new RotateTransform();
        ScaleTransform st = new ScaleTransform();

        tg.Children.Add(st);
        tg.Children.Add(rt);
        tg.Children.Add(tt);

        this.RenderTransformOrigin = new Point(0.5, 1);
        this.RenderTransform = tg;
    }
}

下一步是根据所有者当前的大小缩放指示器。这是通过将所有者的半径除以指示器的高度来完成的。`ArrangeOverride` 方法中的最后一步是计算相应的偏移量并设置平移变换。

另一个重要的帮助方法是 `SetIndicatorAngle` 方法。这用于根据 `Value` 属性的当前值修改指示器的旋转变换。定义如下所示

private void SetIndicatorAngle(RadialScale scale, double value)
{
    double angle = scale.GetAngleFromValue(Value);
    if (scale.SweepDirection == SweepDirection.Counterclockwise)
    {
        angle = -angle;
    }
    //rotate the needle
    TransformGroup tg = RenderTransform as TransformGroup;
    if (tg != null)
    {
        RotateTransform rt = tg.Children[1] as RotateTransform;
        if (rt != null)
        {
            rt.Angle = angle;
            Debug.WriteLine("angle changed to " + angle);
        }
    }
}

此方法在值更改处理程序中调用。`OnValueChanged` 处理程序的定义可以在下图看到

protected override void OnValueChanged(double newVal, double oldVal)
{
    RadialScale scale = Owner as RadialScale;
    if (scale != null)
    {
        SetIndicatorAngle(scale, Value);
    }
}

当指示器所有者更改时,也会调用此方法。`UpdateindicatorsOverride` 的定义如下所示

protected override void UpdateIndicatorOverride(Scale owner)
{
    base.UpdateIndicatorOverride(owner);
    SetIndicatorTransforms();
    RadialScale scale = owner as RadialScale;
    if(scale!=null)
    {
        SetIndicatorAngle(scale, Value);
    }
}

下图展示了两组使用此类型控件的径向仪表盘

上面图片左侧仪表盘的代码可以在下面看到。对于右侧的仪表盘,使用了类似的代码。

<scada:RadialScale Grid.RowSpan="2" Grid.ColumnSpan="2"
       RangeThickness="5" MinorTickStep="10" MajorTickStep="50"
       MinAngle="-90" MaxAngle="180">
    <scada:RadialScale.Ranges>
        <scada:GaugeRange Color="Red" Offset="20" />
        <scada:GaugeRange Color="Orange" Offset="40" />
        <scada:GaugeRange Color="WhiteSmoke" Offset="60" />
        <scada:GaugeRange Color="{StaticResource PhoneAccentColor}" Offset="100" />
    </scada:RadialScale.Ranges>
    
    <scada:NeedleIndicator 
       Value="{Binding ElementName=slider,Path=Value}" Background="White"/>
</scada:RadialScale>
<scada:RadialScale MinAngle="100" MaxAngle="170" 
       SweepDirection="Counterclockwise" MinorTickStep="25" 
       MajorTickStep="50" RadialType="Quadrant" Width="123" 
       Height="122" Margin="80,16,16,26" Grid.Row="1"
       RangeThickness="4">
    <scada:NeedleIndicator 
       Value="{Binding ElementName=slider2,Path=Value}" Background="GreenYellow"/>
    <scada:RadialScale.Ranges>
        <scada:GaugeRange Offset="100" Color="GreenYellow"/>
    </scada:RadialScale.Ranges>
    <scada:RadialScale.LabelTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding}" Foreground="GreenYellow" 
                 FontWeight="Bold" FontSize="16"/>
        </DataTemplate>
    </scada:RadialScale.LabelTemplate>
</scada:RadialScale>

您也可以在单个刻度上使用多个指示器。这可以在下图中看到,该图展示了两个带有两个指示器的径向刻度控件

最后的想法

本文只讨论了几种指示器。当然还有许多其他可以实现的。其中两个例子是用于线性刻度的标记指示器和指针指示器。线性刻度的指针指示器可以在指定值处显示带有箭头和标签的线。这也可以通过您刚刚看到的标记指示器来复制。下图展示了线性刻度的指针指示器可能的样子

当然,还有许多其他的自定义和扩展可能性。

请随时发表您的评论和建议。此外,如果您想为本文投票,请为系列中的第一篇文章投票 Smile | :)

历史

  • 创建于 2011年3月21日。
  • 更新于 2011年3月23日。
  • 更新于 2011年3月28日。
  • 更新源代码于 2011年4月4日。
  • 更新源代码于 2011年4月10日。
  • 更新源代码于 2013年2月24日。
© . All rights reserved.