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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (21投票s)

2011 年 3 月 20 日

CPOL

15分钟阅读

viewsIcon

54631

downloadIcon

1006

本文是该系列文章中的第二篇,介绍了适用于 WP7 的一组仪表控件。

引言

本文接续本系列的第一篇。该第一篇文章介绍了适用于 Windows Phone 7 平台的自定义仪表控件的设计考虑因素和使用方法。本文将深入探讨该问题,并介绍刻度类的实现。如果您还没有阅读第一篇文章,请先从那篇开始,它将帮助您更好地理解本文内容。

本系列文章

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

刻度基类

正如您在上一篇文章中看到的,刻度层级包含三个类:一个基类(Scale),用于保存每个仪表通用的逻辑和属性,以及两个派生类,用于实现线性仪表和径向仪表特有的功能。

下图展示了无论形状如何,仪表都应具备的所有通用属性的图表。

这些都是依赖属性。您可以看到,Scale 具有用于保存值区间(MinimumMaximum)、指示器、范围以及一些范围属性(粗细、是否使用默认范围、以及是否使用范围的颜色来绘制落入该范围的刻度线和标签)的属性。基类还具有用于配置标签和刻度线模板的属性。

关于其中一些属性,有几点需要讨论。我将要讨论的第一个属性是 Indicators 属性。它用于暴露刻度将拥有的指示器集合。指示器不会直接保存在刻度面板中。相反,我定义了一个私有的 Canvas 来保存指示器。然后将此 Canvas 实例添加到 ScaleChildren 集合中。Indicators 属性暴露了 CanvasChildren 属性。这可以在下面的代码中看到

Canvas indicatorContainer;
public Scale()
{
    indicatorContainer = new Canvas();            
    Children.Add(indicatorContainer);
    //...
}
public UIElementCollection Indicators
{
    get { return indicatorContainer.Children; }
}

下一个属性是 Ranges 属性。此属性用于保存用户决定的所有最优范围。此属性的类型为 ObservableCollection<GaugeRange>。每个范围都有一个最大值(偏移量)和一个颜色。类型定义可以在下面的代码中看到

public class GaugeRange
{
    public double Offset { get; set; }
    public Color Color { get; set; }
}

一个特定的范围将从前一个范围结束的地方绘制到 Offset。第一个范围从 0 开始。在此处要提及的另一件事是,此集合仅描述范围。为了将范围添加到我们的刻度中,我们需要定义形状并根据颜色设置画笔,然后将它们添加到 ScaleChildren 集合中。有两个方法用于从刻度中添加和删除范围。这些方法是 CreateRanges()ClearRanges()。由于范围创建取决于范围类型,因此这两个方法是抽象的,需要在派生类中进行重写。声明可以在下面看到

protected abstract void CreateRanges();
protected abstract void ClearRanges();

我将要讨论的下一组属性是刻度线和标签自定义属性。这些属性设置刻度线和标签的模板。这些是普通的数据模板属性,但这里要展示的漂亮代码是处理缺少模板的代码。该类中有几个方法用于应用模板。如果用户指定了模板,则方法将应用这些模板;如果用户未指定任何模板,则方法将应用隐式模板。这些模板是通过加载预定义的 XAML 字符串获得的。目前,这是硬编码的。这两个方法可以在下面的列表中看到

protected virtual DataTemplate CreateDataTemplate(TickType tType)
{
    string template = "";
    if (tType == TickType.Label)
        template = @"<TextBlock Text=""{Binding}"" FontSize=""13"" FontWeight=""Bold"" />";
    else if (tType == TickType.Minor)
        template = @"<Rectangle Fill=""Snow"" Width=""2"" Height=""5"" />";
    else
        template = @"<Rectangle Fill=""Snow"" Width=""2"" Height=""10"" />";

    string xaml =
        @"<DataTemplate
           xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
           xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"">"+
           template+@"</DataTemplate>";

    DataTemplate dt = (DataTemplate)XamlReader.Load(xaml);
    return dt;
}

private DataTemplate GetTickTemplate(TickType tType)
{
    //after adding template properties also check that those arent null
    if (tType == TickType.Label)
    {
        if (LabelTemplate != null)
            return LabelTemplate;
        return CreateDataTemplate(tType);
    }
    else if (tType == TickType.Minor)
    {
        if (MinorTickTemplate != null)
            return MinorTickTemplate;
        return CreateDataTemplate(tType);
    }
    else
    {
        if (MajorTickTemplate != null)
            return MajorTickTemplate;
        return CreateDataTemplate(tType);
    }
}

CreateDataTemplate 方法被标记为 virtual,以便可以在派生类中重写它,为线性和径向刻度提供不同的模板。

在基类中要讨论的最后一件事是刻度线和标签的创建。刻度线和标签保存在两个私有变量中。定义可以在下面看到

List<Tick> labels=new List<Tick>();
List<Tick> ticks=new List<Tick>();

如您所见,刻度线和标签由 Tick 类表示。该类用于表示标签、小刻度线和大刻度线。该类的定义可以在下面看到

public enum TickType { Minor, Major, Label }
public class Tick:ContentPresenter
{
    public double Value
    {
        get { return (double)GetValue(ValueProperty); }
        set { SetValue(ValueProperty, value); }
    }
    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register("ValueProperty", typeof(double), 
                                    typeof(Tick), new PropertyMetadata(0.0));

    public TickType TickType
    {
        get { return (TickType)GetValue(TickTypeProperty); }
        set { SetValue(TickTypeProperty, value); }
    }
    public static readonly DependencyProperty TickTypeProperty =
        DependencyProperty.Register("TickType", typeof(TickType), 
        typeof(Tick), new PropertyMetadata(TickType.Minor));

}

这些标签和刻度线在类构造函数中首次创建,然后每次刻度线相关属性或仪表大小更改时被销毁并重新创建。标签创建方法可以在下面的代码中看到

private void CreateLabels()
{
    double max = Maximum;
    double min = Minimum;
    for (double v = min; v <= max; v += MajorTickStep)
    {
        Tick tick = new Tick() { Value = v, TickType = TickType.Label };
        labels.Add(tick);
        //also set the content and template for the label
        tick.ContentTemplate = GetTickTemplate(TickType.Label);
        tick.Content = v;
        Children.Insert(0, tick);
    }
}

标签仅为大刻度线创建。该函数从最小值开始,为每个大刻度线创建一个标签。它创建一个刻度实例,并设置值、类型和模板。正如您所见,模板是使用上述函数分配的。最后,代码设置内容并将标签添加到 ScaleChildren 集合中。该函数使用 Insert 而不是 Add,这样我们就可以确保指示器始终位于其他刻度图形的顶部。

刻度线的创建方式类似。唯一的区别是进行了一个额外的检查来确定小刻度线。刻度线创建方法的代码可以在下面看到

private void CreateTicks()
{
    double max = Maximum;
    double min = Minimum;
    int num = 0;//the tick index
    double val = min;//the value of the tick
    while (val <= max)
    {
        DataTemplate template = null;
        Tick tick = new Tick();
        tick.Value = val;
        if (num % MinorTickStep == 0)
        {
            tick.TickType = TickType.Minor;
            template = GetTickTemplate(TickType.Minor);
        }
        if (num % MajorTickStep == 0)
        {
            tick.TickType = TickType.Major;
            template = GetTickTemplate(TickType.Major);
        }
        tick.ContentTemplate = template;
        tick.Content = val;
        ticks.Add(tick);
        Children.Insert(0, tick);

        val += MinorTickStep;
        num += MinorTickStep;
    }
}

您可以从上述方法中看到,对于同时是小刻度线和大刻度线候选的值,会选择大刻度线。还有删除标签和刻度线的方法。这些方法会清除私有列表,并从 ScaleChildren 集合中删除刻度线。定义可以在下面看到

private void ClearLabels()
{
    for (int i = 0; i < labels.Count; i++)
    {
        Children.Remove(labels[i]);
    }
    labels.Clear();
}
private void ClearTicks()
{
    for (int i = 0; i < ticks.Count; i++)
    {
        Children.Remove(ticks[i]);
    }
    ticks.Clear();
}

此类中我尚未讨论的最后一段代码是用于排列标签、刻度线、范围和指示器的代码。基于刻度类型,所有这些元素都将以不同的方式排列。因此,特定的排列代码将在派生类中实现。此外,由于这是一个容器,我还应该重写 ArrangeOverrideMeasureOverride 方法。由于测量代码取决于刻度类型,因此我只重写了 ArrangeOverride 方法。此方法代码如下

protected override Size ArrangeOverride(Size finalSize)
{
    ArrangeLabels(finalSize);
    ArrangeTicks(finalSize);
    ArrangeRanges(finalSize);
    indicatorContainer.Arrange(new Rect(new Point(), finalSize));
    
    ArrangeIndicators(finalSize);
    //at the end just return the final size
    return finalSize;
}

三个 Arrange 方法是 abstract 的,应该在派生的刻度类中重写。声明可以在下面看到

protected abstract void ArrangeTicks(Size finalSize);
protected abstract void ArrangeLabels(Size finalSize);
protected abstract void ArrangeRanges(Size finalSize);

线性刻度

此类型刻度的特定属性可以在下图看到

这些都是依赖属性,用于指定刻度的方向(水平或垂直)以及刻度元素的(TopLeftBottomRight)位置。

我想讨论的第一个方法是范围创建和移除方法。范围创建代码如下所示

protected override void CreateRanges()
{
    //insert the default range
    if (UseDefaultRange)
    {
        def.Fill = DefaultRangeBrush;
        Children.Add(def);
    }
    if (Ranges == null)
        return;
    //it is presumed that the ranges are ordered
    foreach (GaugeRange r in Ranges)
    {
        Rectangle rect = new Rectangle();
        rect.Fill = new SolidColorBrush(r.Color);
        ranges.Add(rect);
        Children.Add(rect);
    }
}

该方法首先检查 UseDefaultRange 属性是否为 true。如果是,则创建默认范围,设置其画笔,并将其添加到刻度的 Children 集合中。然后,它遍历 Ranges 集合,对于每个范围描述,它创建一个矩形,设置画笔,并将其添加到刻度的 Children 集合中。由于这是线性刻度,因此范围是通过使用矩形表示的。矩形也保存在私有列表中。移除范围的方法显示在下面的代码中

protected override void ClearRanges()
{
    //remove the default range
    if (UseDefaultRange)
    {
        Children.Remove(def);
    }
    if (Ranges == null)
        return;
    for (int i = 0; i < ranges.Count; i++)
    {
        Children.Remove(ranges[i]);
    }
    ranges.Clear();
}

该方法首先移除默认范围,从刻度的 Children 集合中移除范围矩形,最后清除私有列表。

此类中的另一个重要方法是 MeasureOverride 方法。这将用于告知框架线性刻度将占用多少空间。空间取决于刻度线、标签、范围和指示器的尺寸以及刻度的方向。该方法可以在下面的列表中看到

protected override Size MeasureOverride(Size availableSize)
{
    foreach (Tick label in GetLabels())
        label.Measure(availableSize);
    foreach (Tick tick in GetTicks())
        tick.Measure(availableSize);
    foreach (Rectangle rect in ranges)
        rect.Measure(availableSize);
    
    if(Indicators!=null)
        foreach (UIElement ind in Indicators)
            ind.Measure(availableSize);

    double width = 0;
    double height = 0;
    double lblMax=0, tickMax=0, indMax=0;
    if (Orientation == Orientation.Horizontal)
    {
        lblMax = GetLabels().Max(p => p.DesiredSize.Height);
        tickMax = GetTicks().Max(p => p.DesiredSize.Height);
        if (Indicators != null && Indicators.Count > 0)
            indMax = Indicators.Max(p => p.DesiredSize.Height);
        height = 3 + lblMax + tickMax + indMax;
        if(!double.IsInfinity(availableSize.Width))
            width = availableSize.Width;
    }
    else
    {
        lblMax = GetLabels().Max(p => p.DesiredSize.Width);
        tickMax = GetTicks().Max(p => p.DesiredSize.Width);
        if (Indicators != null && Indicators.Count > 0)
            indMax = Indicators.Max(p => p.DesiredSize.Width);
        width = 3 + lblMax + tickMax + indMax;
        if (!double.IsInfinity(availableSize.Height))
            height = availableSize.Height;
    }

    return new Size(width, height);
}

该方法首先测量所有面板的子项,然后返回期望的大小。正如您所见,线性刻度的大小主要取决于其方向。

在线性刻度情况下要讨论的最后一些方法是用于排列刻度线标签和范围的三个抽象方法。用于排列标签的方法可以在下面的列表中看到

protected override void ArrangeLabels(Size finalSize)
{
    var labels = GetLabels();
    double maxLength = labels.Max(p => p.DesiredSize.Width)+1;
    foreach (Tick tick in labels)
    {
        if (Orientation == Orientation.Horizontal)
        {
            double offset = GetSegmentOffset(finalSize.Width, tick.Value);
            if (TickPlacement == LinearTickPlacement.TopLeft)
            {                        
                tick.Arrange(new Rect(new Point(offset-tick.DesiredSize.Width/2, 1), 
                                                tick.DesiredSize));
            }
            else
            {
                tick.Arrange(new Rect(new Point(offset - tick.DesiredSize.Width / 2, 
                   finalSize.Height - tick.DesiredSize.Height - 1), tick.DesiredSize));
            }
        }
        else
        {
            double offset = GetSegmentOffset(finalSize.Height, tick.Value);
            if (TickPlacement == LinearTickPlacement.TopLeft)
            {
                tick.Arrange(new Rect(new Point(maxLength - 
                  tick.DesiredSize.Width,finalSize.Height - offset - 
                  tick.DesiredSize.Height / 2), tick.DesiredSize));
            }
            else
            {
                tick.Arrange(new Rect(new Point(finalSize.Width - 
                  maxLength,finalSize.Height - offset - 
                  tick.DesiredSize.Height / 2), tick.DesiredSize));
            }
        }
    }
}

正如您所见,该方法首先计算标签偏移量,然后使用计算出的偏移量和标签的期望大小对每个标签调用 Arrange 方法。偏移量是使用下面的辅助函数计算的

private double GetSegmentOffset(double length, double value)
{
    double offset = length * (value - Minimum) / (Maximum - Minimum);
    return offset;
}

排列刻度线的方法类似。唯一的区别是增加了偏移量以补偿标签的大小。此方法代码如下

protected override void ArrangeTicks(Size finalSize)
{
    var ticks = GetTicks();
    
    foreach (Tick tick in ticks)
    {
        if (Orientation == Orientation.Horizontal)
        {
            double maxLength = ticks.Max(p => p.DesiredSize.Height);
            double offset = GetSegmentOffset(finalSize.Width, tick.Value);
            if (TickPlacement == LinearTickPlacement.TopLeft)
            {
                double yOff=GetLabels().Max(p=>p.DesiredSize.Height)+1;
                tick.Arrange(new Rect(new Point(offset - tick.DesiredSize.Width / 2, 
                  yOff + maxLength - tick.DesiredSize.Height), tick.DesiredSize));
            }
            else
            {
                double yOff = finalSize.Height - maxLength - 
                  GetLabels().Max(p => p.DesiredSize.Height) - 2;
                tick.Arrange(new Rect(new Point(offset - 
                  tick.DesiredSize.Width / 2, yOff), tick.DesiredSize));
            }
        }
        else
        {
            double maxLength = ticks.Max(p => p.DesiredSize.Width) + 
                   GetLabels().Max(p => p.DesiredSize.Width) + 2;
            double offset = GetSegmentOffset(finalSize.Height, tick.Value);
            if (TickPlacement == LinearTickPlacement.TopLeft)
            {
                tick.Arrange(new Rect(new Point(maxLength - tick.DesiredSize.Width,
                  finalSize.Height- offset - 
                  tick.DesiredSize.Height / 2), tick.DesiredSize));
            }
            else
            {
                tick.Arrange(new Rect(new Point(finalSize.Width - maxLength,
                  finalSize.Height- offset - tick.DesiredSize.Height / 2), 
                  tick.DesiredSize));
            }
        }
    }
}

排列范围的代码使用与前两种方法类似的技术。此方法首先排列默认范围(如果存在),然后遍历其余范围以排列它们。关于此函数有趣的一点是,对于用户定义的范围,该方法根据前一个范围计算最优范围尺寸,而不是简单地从 0 开始。此方法代码如下

protected override void ArrangeRanges(Size finalSize)
{
    if (UseDefaultRange)
    {
        if (Orientation == Orientation.Horizontal)
        {
            double yOff;
            if (TickPlacement == LinearTickPlacement.TopLeft)
            {
                yOff = GetLabels().Max(p => p.DesiredSize.Height) + 
                       GetTicks().Max(p => p.DesiredSize.Height) + 1;
                def.Arrange(new Rect(new Point(0, yOff), 
                            new Size(finalSize.Width, RangeThickness)));
            }
            else
            {
                yOff = GetLabels().Max(p => p.DesiredSize.Height) + 
                       GetTicks().Max(p => p.DesiredSize.Height) + 3;
                def.Arrange(new Rect(new Point(0, finalSize.Height - yOff - 
                            RangeThickness), new Size(finalSize.Width, RangeThickness)));
            }

        }
        else
        {
            double off = GetLabels().Max(p => p.DesiredSize.Width) + 
                         GetTicks().Max(p => p.DesiredSize.Width) + 2;
            if (TickPlacement == LinearTickPlacement.TopLeft)
            {
                def.Arrange(new Rect(new Point(off, 0), 
                            new Size(RangeThickness, finalSize.Height)));
            }
            else
            {
                def.Arrange(new Rect(new Point(finalSize.Width - off - RangeThickness, 0), 
                                     new Size(RangeThickness, finalSize.Height)));
            }
        }
    }
    double posOffset = 0;
    for (int i = 0; i < ranges.Count; i++)
    {
        Rectangle rect = ranges[i];
        rect.Fill = new SolidColorBrush(Ranges[i].Color);

        if (Orientation == Orientation.Horizontal)
        {
            double yOff;
            if (TickPlacement == LinearTickPlacement.TopLeft)
            {
                yOff = GetLabels().Max(p => p.DesiredSize.Height) + 
                  GetTicks().Max(p => p.DesiredSize.Height) + 1;
                rect.Arrange(new Rect(new Point(posOffset, yOff), 
                  new Size(GetSegmentOffset(finalSize.Width, 
                  Ranges[i].Offset) - posOffset, RangeThickness)));
            }
            else
            {
                yOff = GetLabels().Max(p => p.DesiredSize.Height) + 
                       GetTicks().Max(p => p.DesiredSize.Height) + 3;
                rect.Arrange(new Rect(new Point(posOffset, finalSize.Height - 
                     yOff - RangeThickness), new Size(GetSegmentOffset(finalSize.Width, 
                     Ranges[i].Offset) - posOffset, RangeThickness)));
            }
            posOffset = GetSegmentOffset(finalSize.Width, Ranges[i].Offset);
        }
        else
        {
            double off = GetLabels().Max(p => p.DesiredSize.Width) + 
                         GetTicks().Max(p => p.DesiredSize.Width) + 2;
            double segLength = GetSegmentOffset(finalSize.Height, Ranges[i].Offset);
            if (TickPlacement == LinearTickPlacement.TopLeft)
            {
                rect.Arrange(new Rect(new Point(off, finalSize.Height - segLength), 
                             new Size(RangeThickness, segLength - posOffset)));
            }
            else
            {
                rect.Arrange(new Rect(new Point(finalSize.Width - off - RangeThickness, 
                     finalSize.Height - segLength), 
                     new Size(RangeThickness, segLength - posOffset)));
            }
            posOffset = segLength;
        }
    }
}

正如您所见,在遍历范围集合之前,方法会初始化一个偏移量变量。此变量将用于计算每个范围的正确起始位置。此偏移量在每次迭代后更新,以便下一个范围从前一个范围结束的地方开始。计算出水平和垂直偏移量后,将调用 Arrange 方法。当前范围的长度将通过从当前范围的偏移量中减去当前偏移量来计算。这有效地确定了最优长度,并且不是每个范围都从 0 开始。

下图展示了带有 LinearScale 的一些屏幕截图。每张图片展示了不同的自定义设置。

上图展示了四个 LinearScale 实例。第一个使用默认设置。第二个线性刻度具有 5 的 RangeThickness,并为标签和两种刻度类型提供了模板。第三个刻度将 TickPlacement 属性设置为 LinearTickPlacement.BottomRight,具有两个范围,并将 RangeThickness 属性设置为 5。最后一个刻度还将 TickPlacement 属性设置为 LinearTickPlacement.BottomRight。它还为小刻度线和大刻度线提供了模板。

上图展示了相同的四个 LinearScale 实例。唯一的区别是这次将 Orientation 属性设置为 Orientation.Vectical

径向刻度

径向刻度特有的属性可以在下图看到

这些都是依赖属性。MinAngleMaxAngle 决定了刻度的起始角度和扫掠角。TickPlacement 决定了刻度线是向内绘制还是向外绘制。RadialType 属性指的是我们想要的刻度圆的子类型。我们可以有一个整圆刻度、半圆刻度或四分之一圆刻度。此选项主要用于节省屏幕空间。例如,如果用户选择 90 度范围,他可能不想浪费圆的其他三个象限。为了节省空间,他可以指定他想要一个四分之一圆。此属性将与最小、最大角度和扫掠方向属性结合使用。此属性将影响刻度区域内旋转中心的定位以及范围。如果角度和扫掠方向不匹配,用户可以选择更改它们以获得他想要的结果。这比在代码中实现复杂的逻辑来确定四分之一圆并强制执行角度值更容易。

范围创建和移除方法与为线性刻度实现的方法类似。唯一的区别是,范围为径向刻度时,不是使用矩形,而是使用 Path 类的实例。

MeasureOverride 方法在所有容器子项上调用 Measure,然后仅返回期望的大小。我们将使用所有可用空间并将刻度居中放置。定义可以在下面看到

protected override Size MeasureOverride(Size availableSize)
{
    double width = 0.0;
    double height = 0.0;
    if (!double.IsInfinity(availableSize.Width))
        width = availableSize.Width;
    if (!double.IsInfinity(availableSize.Height))
        height = availableSize.Height;
    Size size = new Size(width, height);
    //measure all the children
    foreach (Tick label in GetLabels())
        label.Measure(availableSize);
    foreach (Tick tick in GetTicks())
        tick.Measure(availableSize);
    foreach (Path path in ranges)
        path.Measure(availableSize);
    if (Indicators != null)
        foreach (UIElement ind in Indicators)
            ind.Measure(availableSize);
    //return the available size as everything else will be 
    //arranged to fit inside.
    return size;
}

要讨论的最后一些方法是用于排列标签、刻度线和范围的三个抽象方法。这三个方法利用了一个辅助类。我将首先介绍这些方法,然后描述辅助类。

排列径向刻度标签的代码可以在下面看到

protected override void ArrangeLabels(Size finalSize)
{
    double maxRad = RadialScaleHelper.GetRadius(RadialType, finalSize, 
                    MinAngle, MaxAngle, SweepDirection);
    Point center = RadialScaleHelper.GetCenterPosition(RadialType, 
                   finalSize, MinAngle, MaxAngle, SweepDirection);
    double x = center.X;
    double y = center.Y;
    double rad = maxRad;
    if (TickPlacement == RadialTickPlacement.Inward)
    {
        rad = maxRad - RangeThickness - GetTicks().Max(p => p.DesiredSize.Height);
    }
    var labels = GetLabels();

    for (int i = 0; i < labels.Count; i++)
    {
        PositionTick(labels[i], x, y, rad - labels[i].DesiredSize.Height / 2);
    }
}

代码首先使用辅助类方法获取刻度的中心和最大半径。在此之后,根据刻度线方向计算实际半径。最后,代码遍历每个标签并对其进行定位。这是使用 PositionTick 方法完成的。此方法定义可以在下面看到

private void PositionTick(Tick tick, double x, double y, double rad)
{
    // Tick tick = ticks[i];
    double tickW = tick.DesiredSize.Width;
    double tickH = tick.DesiredSize.Height;

    double angle = GetAngleFromValue(tick.Value);
    if (SweepDirection == SweepDirection.Counterclockwise)
        angle = -angle;
    //position the tick
    double px = x + (rad) * Math.Sin(angle * Math.PI / 180);
    double py = y - (rad) * Math.Cos(angle * Math.PI / 180);
    px -= tickW / 2;
    py -= tickH / 2;
    tick.Arrange(new Rect(new Point(px, py), tick.DesiredSize));

    //rotate the tick (if not label or
    //if it is label and has rotation enabled)
    if ((EnableLabelRotation && tick.TickType==TickType.Label) || 
         tick.TickType!=TickType.Label)
    {
        RotateTransform tr = new RotateTransform();
        tr.Angle = angle;
        tick.RenderTransformOrigin = new Point(0.5, 0.5);
        tick.RenderTransform = tr;
    }
}

该方法使用极坐标系公式根据中心和半径计算刻度线(标签或刻度线)的位置。然后,该方法调用刻度线的 Arrange(),然后设置 RenderTransform 以旋转刻度线。您可以从上面的代码中看到,只有当刻度线不是标签时,或者当它是标签且 EnableLabelRotation 属性设置为 true 时,刻度线才会被旋转。

排列刻度线的方法可以在下面的列表中看到

protected override void ArrangeTicks(Size finalSize)
{
    double maxRad = RadialScaleHelper.GetRadius(RadialType, finalSize, 
                    MinAngle, MaxAngle, SweepDirection);
    Point center = RadialScaleHelper.GetCenterPosition(RadialType, 
                   finalSize, MinAngle, MaxAngle, SweepDirection);
    double x = center.X;
    double y = center.Y;

    var ticks = GetTicks();
    var labels = GetLabels();

    double rad = maxRad - labels.Max(p => p.DesiredSize.Height) - 
                 ticks.Max(p => p.DesiredSize.Height) - 1;
    if (TickPlacement == RadialTickPlacement.Inward)
    {
        rad = maxRad - RangeThickness;
    }

    for (int i = 0; i < ticks.Count; i++)
    {
        if (TickPlacement == RadialTickPlacement.Outward)
            PositionTick(ticks[i], x, y, rad + ticks[i].DesiredSize.Height / 2);
        else
            PositionTick(ticks[i], x, y, rad - ticks[i].DesiredSize.Height / 2);
    }
}

正如您所见,该方法非常相似,并且根据刻度线方向属性,使用不同的半径调用刻度线放置方法。最后一个方法是排列范围的方法。ArrangeRanges() 方法的第一部分将用于排列默认范围(如果 UseDefaultRange 属性设置为 true)。这可以在下面看到

double maxRad = RadialScaleHelper.GetRadius(RadialType, finalSize, 
                MinAngle, MaxAngle, SweepDirection);
Point center = RadialScaleHelper.GetCenterPosition(RadialType, 
               finalSize, MinAngle, MaxAngle, SweepDirection);
double x = center.X;
double y = center.Y;
//calculate the ranges' radius
double rad = maxRad;
if (TickPlacement == RadialTickPlacement.Outward)
{
    rad = maxRad - GetLabels().Max(p => p.DesiredSize.Height) - 
                   GetTicks().Max(p => p.DesiredSize.Height) - 1;
}
//draw the default range
if (UseDefaultRange)
{
    double min = MinAngle, max = MaxAngle;
    if (SweepDirection == SweepDirection.Counterclockwise)
    {
        min = -min;
        max = -max;
    }                    
    //the null check needs to be done because
    //otherwise the arrange pass will be called 
    //recursevely as i set new content for the path in every call
    Geometry geom= RadialScaleHelper.CreateArcGeometry(min, max, rad, 
                   RangeThickness, SweepDirection);
    if (def.Data == null || def.Data.Bounds!=geom.Bounds)
        def.Data = geom;
    //arrange the default range. move the start point of the 
    //figure (0, 0) in the center point (center)
    def.Arrange(new Rect(center, finalSize));
}

代码首先获取中心和可能的最大半径,然后使用 CreateArcGeometry() 方法设置范围形状。最后,代码使用中心点调用默认范围的 Arrange()

排列其余范围的代码类似。您可以在下面看到

double prevAngle = MinAngle;
if (SweepDirection == SweepDirection.Counterclockwise)
    prevAngle = -prevAngle; 
for (int i = 0; i < ranges.Count; i++)
{
    Path range = ranges[i];
    GaugeRange rng = Ranges[i];
    double nextAngle = GetAngleFromValue(rng.Offset);
    if (SweepDirection == SweepDirection.Counterclockwise)
        nextAngle = -nextAngle;

    range.Fill = new SolidColorBrush(rng.Color);

    if (range.Data == null)
        range.Data = RadialScaleHelper.CreateArcGeometry(prevAngle, 
                     nextAngle, rad, RangeThickness, SweepDirection);

    range.Arrange(new Rect(center, finalSize));
    prevAngle = nextAngle;
}

除了排列范围外,此代码还设置每个范围的颜色。为了排列这些范围,代码需要获取结束角度。代码从最小角度开始,然后在每个范围排列完毕后递增。

下图展示了带有 RadialScale 的一些屏幕截图。每张图片展示了不同的自定义设置。

在上图中,左侧部分显示了一个具有默认设置的径向刻度。右侧部分设置了一个范围。范围的厚度为 5,范围的偏移量为 60。

在上图中,左侧部分显示了一个 TickPlacement 属性设置为 LinearTickPlacement.Inward 的径向刻度,RangeThickness 属性为 5,刻度有两个范围。右侧部分显示了一个径向刻度,其中更改了标签、小刻度线和大刻度线的模板。

上图展示了两个 SweepDirection 属性设置为 SweepDirection.Counterclockwise 的仪表。左侧部分,MinAngle 为 0,MaxAngle 为 270。右侧部分,MinAngle 为 90,MaxAngle 为 360,UseDefaultRangefalse,并且我们定义了一个范围。

在最后一张图中,我们有两个径向刻度,具有更多的刻度线和标签自定义设置。

RadialScaleHelper

这是一个辅助类,由 RadialScale 类型使用,用于计算径向刻度的中心位置、径向刻度半径以及范围几何图形。该类有三个公共方法。第一个方法返回刻度的逻辑中心。这是将用作旋转中心的点。第二个方法返回刻度的期望半径。此半径将取决于最小和最大角度以及 RadialType 设置。最后一个方法将用于获取范围形状。

径向仪表可以是圆、半圆或四分之一圆。在每种情况下,旋转中心都不同。GetCenterPosition() 方法将用于根据径向类型、刻度的最终尺寸、最小和最大角度以及扫掠方向来确定此中心。中心位置的情况可以在下图更好地看出

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

public static Point GetCenterPosition(RadialType type, Size finalSize, double minAngle, 
              double maxAngle, SweepDirection sweepDir)
{
    //get the quadrants of the limits
    int q1 = GetQuadrant(minAngle);
    int q2 = GetQuadrant(maxAngle);
    if (sweepDir == SweepDirection.Counterclockwise)
    {
        q1++; q2++;
        if (q1 > 4) q1 = 1;
        if (q2 > 4) q2 = 1;
    }
    else
    {
        //q2->q4 and q4->q2
        if (q1 % 2 == 0) q1 = 6 - q1;
        if (q2 % 2 == 0) q2 = 6 - q2;
    }
    //calculate the difference
    int diff = q2 - q1;
    if (Math.Abs(diff) == 0)
    {
        //quarter possibility
        if (type == RadialType.Quadrant)
        {
            return GetCenterForQuadrant(q2, finalSize);
        }
        else if (type == RadialType.Semicircle)
        {
            if (q1 == 1 || q1 == 2)
                return new Point(finalSize.Width / 2, finalSize.Height);
            else
                return new Point(finalSize.Width / 2, 0);
        }
        else
        {
            //full circle
            return new Point(finalSize.Width / 2, finalSize.Height / 2);
        }
    }
    else if (Math.Abs(diff) == 1 || (Math.Abs(diff)==3 && (maxAngle-minAngle)<=180))
    {
        //semicircle possibility
        if (type == RadialType.Quadrant || type == RadialType.Semicircle)
        {
            return GetCenterForSemicircle(q1, q2, finalSize);
        }
        else
        {
            //full circle
            return new Point(finalSize.Width / 2, finalSize.Height / 2);
        }
    }
    else
    {
        //full circle
        return new Point(finalSize.Width / 2, finalSize.Height / 2);
    }
}

由于屏幕象限与屏幕上的象限不匹配,因此该方法首先将屏幕象限转换为几何象限。之后,根据差异,返回一个不同的点。在 GetRadius 方法中进行相同的检查以获得可能的最大半径。

最后一个方法是构建范围几何图形的方法。每个范围将有四个段。这些可以在下图看到

此方法的代码可以在下面看到

public static Geometry CreateArcGeometry(double minAngle, double maxAngle, 
       double radius, int thickness, SweepDirection sweepDirection)
{
    //the range will have 4 segments (arc, line, arc, line)
    //if the sweep angle is bigger than 180 use the large arc
    //first use the same sweep direction as the control. invert for the second arc.
    PathFigure figure = new PathFigure();
    figure.IsClosed = true;
    figure.StartPoint = new Point((radius - thickness) * 
           Math.Sin(minAngle * Math.PI / 180),
           -(radius - thickness) * Math.Cos(minAngle * Math.PI / 180));

    //first arc segment
    ArcSegment arc = new ArcSegment();
    arc.Point = new Point((radius - thickness) * Math.Sin(maxAngle * Math.PI / 180),
        -(radius - thickness) * Math.Cos(maxAngle * Math.PI / 180));
    arc.Size = new Size(radius - thickness, radius - thickness);
    arc.SweepDirection = sweepDirection;
    if (Math.Abs(maxAngle - minAngle) > 180) arc.IsLargeArc = true;
    figure.Segments.Add(arc);
    //first line segment
    LineSegment line = new LineSegment();
    line.Point = new Point(radius * Math.Sin(maxAngle * Math.PI / 180),
        -radius * Math.Cos(maxAngle * Math.PI / 180));
    figure.Segments.Add(line);
    //second arc segment
    arc = new ArcSegment();
    arc.Point = new Point(radius * Math.Sin(minAngle * Math.PI / 180),
        -radius * Math.Cos(minAngle * Math.PI / 180));
    arc.Size = new Size(radius, radius);
    arc.SweepDirection = SweepDirection.Counterclockwise;
    if (sweepDirection == SweepDirection.Counterclockwise)
        arc.SweepDirection = SweepDirection.Clockwise;
    if (Math.Abs(maxAngle - minAngle) > 180) arc.IsLargeArc = true;
    figure.Segments.Add(arc);

    PathGeometry path = new PathGeometry();
    path.Figures.Add(figure);
    return path;
}

最后的想法

以上就是刻度实现的所有内容。希望这篇第二篇文章能为您阐明此控件库中刻度的实现。请查看此系列文章的最后一篇,了解指示器的实现。

如果您喜欢这篇文章并觉得代码有用,请花一点时间发表评论并为本系列的第一篇文章投票。

历史

  • 创建于 2011 年 3 月 19 日。
  • 更新于 2011 年 3 月 21 日。
  • 更新源代码于 2011 年 3 月 23 日。
  • 更新源代码于 2011 年 3 月 30 日。
  • 更新源代码和示例于 2011 年 4 月 05 日。
  • 更新源代码于 2011 年 4 月 10 日。
  • 更新源代码于 2013 年 2 月 24 日。
© . All rights reserved.