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

RadialControls - 普通控件,BenT

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2015 年 7 月 18 日

GPL3

6分钟阅读

viewsIcon

27729

关于 RadialControls 的教程 - 一个适用于 Windows 8 应用的圆形控件库。

引言

在 WPF 中绘制圆形很困难。更具体地说,当我设计一个 Windows 8 天气应用程序时,我很快意识到我需要自己制作控件才能获得我想要的外观,因此 RadialControls 应运而生。

背景

RadialControls 包含两部分:一组简化了构建更复杂控件的原始组件,以及一些展示它们如何协同工作的示例。本文将介绍其中一个示例:ArcSlider 控件...

The time picker example control

图片中的时间选择器使用 ArcSlider 控件来显示时钟的时针和分针。下一节我将概述构成滑块和时间选择器的控件,然后重点介绍滑块。

让光明降临...

RadialControls 有两个基本组件:一个 光晕 (halo) 和一个 圆环 (ring)。还有其他组件,例如 滑块 (sliders)扇形 (slices),可以替代圆环组件。再看看图片中的时间选择器...

时间选择器由 3 个逻辑圆环组成,它们在 光晕 内排列成 带 (bands)。每个圆环都有一个 厚度 (thickness),因此光晕的运作方式有点像圆形的 StackPanel (任何剩余空间都分配给内圈)。

<control:Halo>
    <control:HaloDisc Fill="DeepSkyBlue" control:Halo.Band="2"/>
    <local:ArcSlider x:Name="Minutes" control:Halo.Band="2"/>

    <control:HaloDisc Fill="RoyalBlue" control:Halo.Band="1"/>
    <local:ArcSlider x:Name="Hours" control:Halo.Band="1"/>

    <control:HaloDisc Fill="Black"/>

    <TextBlock x:Name="Display" FontSize="30" FontWeight="SemiBold"
        HorizontalAlignment="Center" VerticalAlignment="Center"/>
</control:Halo>

此时,概述所有控件是很有意义的...

  • Halo - 用于将圆环排列成同心圆或
  • HaloRing - 使用变换将其每个子项排列成圆形
  • HaloChain - 类似于 HaloRing,但所有子项都组合在一起
  • HaloDisc - 创建一个扩展的、填充的圆,适用于背景
  • HaloArc/Slice - 在指定角度绘制圆形弧线/圆形
  • Slider - 一个基本的圆形滑块,我们将在下一节进行探讨

时间选择器摘录中包含其中一些。您也可以查看 RingLabel 示例,了解 HaloChain 的用法。继续按照计划,我现在将重点介绍用于创建 ArcSlider 的控件。

倾斜的斜坡...

ArcSlider 示例使用 HaloArc 来替换基本 Slider 控件自带的普通圆形,并添加一些动画使其表现得像一个按钮。忽略花哨的部分,该控件看起来像这样。

<control:Slider>
    <control:Slider.Template>
        <ControlTemplate>
            <control:HaloArc x:Name="Arc" Tension="0.5" Spread="20"
                Angle="{Binding Angle, Mode=TwoWay,
                    RelativeSource={RelativeSource TemplatedParent}}"
                Offset="{Binding Offset, Mode=TwoWay,
                    RelativeSource={RelativeSource TemplatedParent}}"/>
        </ControlTemplate>
    </control:Slider.Template>
</control:Slider>

增加(或减少)角度将使滑块在圆形上移动。偏移量 (offset) 属性决定了圆上的起始点,默认为 0 度,即滑块从圆的顶部开始。

弧线使用数据绑定连接到基本滑块控件,后者负责计算用户滑动滑块时的角度(相对于偏移量)。我将在下面更详细地介绍滑块。

我稍后也会介绍弧线控件。现在,我将提及张力 (tension) 属性,它用于对齐;例如,张力为 0.5 意味着弧线围绕其当前角度居中,如图所示。现在开始滑块的乐趣!

转来转去...

将外观与行为分离总是有益的。尽管 ArcSlider 完全覆盖了它,但滑块控件确实有一个默认模板,该模板在 Generic 主题文件中定义。但我更想专注于行为...

protected override void OnApplyTemplate()
{
   AddHandler(PointerPressedEvent, new PointerEventHandler(StealPointer), true);

   AddHandler(PointerReleasedEvent, new PointerEventHandler(ReleasePointer), true);
   AddHandler(PointerCanceledEvent, new PointerEventHandler(ReleasePointer), true);
   AddHandler(PointerCaptureLostEvent, new PointerEventHandler(ReleasePointer), true);

   AddHandler(PointerMovedEvent, new PointerEventHandler(UpdateValue), true);
}

StealPointerReleasePointer 事件处理程序用于捕获和释放指针。捕获指针意味着当按下指针时,所有指针事件都会被重定向到滑块。现在是棘手的部分...

给定一个 PointerMovedEvent,控件需要计算滑块的新角度,然后根据偏移量进行调整(例如,对于 30 的偏移量,180 度角变为 150 度)。要计算角度...

private double SliderAngle(PointerRoutedEventArgs e)
{
    var centre = new Point(
        ActualWidth / 2, ActualHeight / 2
    );

    var thumb = e.GetCurrentPoint(parent)
        .Position.RelativeTo(centre);

    var vertical = new Vector(0, -1);
    return thumb.AngleTo(vertical);
}

此方法首先使用 `Point` 上的 `RelativeTo` 扩展方法创建一个 Vector。可以将其想象成一个从圆心指向指针的箭头,如下图所示。

Calculating a slider angle

一旦有了指向指针的向量,该方法就会计算该向量相对于直接向上的向量的角度。背后的数学有点棘手;如果您想了解更多信息,请在此处查看 这里

因此,滑块控件使用事件处理程序来捕获、跟踪和释放指针,在此过程中更新角度。剩下要做的就是让某些东西绑定到角度属性 - 使角度可见。

<Style TargetType="control:Slider">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="control:Slider">
                <control:HaloRing>
                    <Grid control:HaloRing.Angle="{Binding Angle, Mode=TwoWay,
                              RelativeSource={RelativeSource TemplatedParent}}"
                          control:HaloRing.Offset="{Binding Offset, Mode=TwoWay,
                              RelativeSource={RelativeSource TemplatedParent}}">
                        <ContentControl Template="{TemplateBinding Thumb}"/>
                    </Grid>
                </control:HaloRing>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

您看到的是默认滑块模板的简化版本。模板并不重要(请记住 ArcSlider 完全覆盖了它),但它引入了另一个我想介绍的控件... HaloRing

生命之圆...

我创建 RadialControls 是因为我想要一种简单的方法来处理圆形。圆的一个问题是如何排列小控件,例如按钮或椭圆(如默认滑块模板中所示)。

HaloRing 解决了这个问题。它定义了两个附加属性:偏移量 (offset) 和角度 (angle),当应用于子控件时,会导致子控件出现在一个圆中,以及圆环中的任何其他子控件。

Example of a HaloRing in action

它的工作方式类似于面板:圆环控件重写了 Panel 类的布局方法,以指定其子控件的排列方式。我不会深入细节,但这是要点...

protected override Size ArrangeOverride(Size finalSize)
{
    var thickness = RingThickness();
    var size = RingSize(finalSize);

    var radius = (
        Math.Min(size.Width, size.Height) - thickness
    )

    foreach(var child in Children)
    {
        ArrangeChid(child, size);
        TransformChild(child, radius);
    }

    return size;
}

ArrangeOverride 接收一个大小,即圆环可用的空间。然后它获取圆环的厚度(基于其子控件的最大期望大小),并将指定的大小向上推,使其至少等于厚度。

现在是重点:控件将在其区域的中心排列每个子项,然后将子项变换到其在圆上的位置,该位置使用偏移量和角度属性指定。

Showing how to arrange items in a HaloRing

变换分为两部分:首先使用三角学计算圆上的点,将子控件平移到偏移量的位置。然后将子控件绕圆旋转到指定角度。

关于圆环控件还有很多可以说的,但我跑题了!到目前为止,我们已经介绍了滑块控件如何用作 ArcSlider 示例的基础。我现在将介绍使 ArcSlider 拥有其出色的外观的组件。

谁建造了弧线...

如果您不知道,弧线是圆周围边界的一部分。HaloArc 通过扩展 Path 类来绘制弧线,并设置如下所示的路径 Data 属性。

private PathFigure figure = new PathFigure();
private ArcSegment segment = new ArcSegment();
private PathGeometry path = new PathGeometry();

public HaloArc()
{
    segment.SweepDirection = SweepDirection.Clockwise;
    figure.Segments = new PathSegmentCollection { segment };
    path.Figures = new PathFigureCollection { figure };

    Data = path;
}

Data 属性设置为 PathGeometry,它由一个 PathFigure 组成,该 PathFigure 又由一个 ArcSegment(实际绘制内容的部分!)组成。现在来处理排列...

protected override Size ArrangeOverride(Size finalSize)
{
    var circle = new Circle(finalSize);
    circle.Radius -= StrokeThickness / 2;

    ArrangePath(circle);
    return finalSize;
}

Circle 是我用来建模(三角形)圆形的辅助工具。我减去一半的路径厚度,以便弧的笔画位于 finalSize 指定的区域内。然后我使用由此产生的圆来设置路径的元素...

private void ArrangePath(Circle circle)
{
    var tension = Tension % 1;
    var angle = Angle + Offset;

    var startAngle = angle - tension * Spread;
    var endAngle = angle + (1 - tension) * Spread;

    figure.StartPoint = circle.PointAt(startAngle);
    segment.Point = circle.PointAt(endAngle);

    segment.Size = circle.Size();
    segment.IsLargeArc = (Spread > 180);
}

前 4 行只是计算弧线开始和结束的角度。开始角度设置为图形上的一个点(微软,为什么?!),结束角度设置为弧段的(结束)点。

Showing how a HaloArc is constructed

就是这样!PointAt 方法使用了一些三角学,但它应该是不言自明的(查看 Utilities/Extensions)。这样我们就完成了本次游览:感谢您的阅读,希望您觉得有所帮助!

结论

该吃饭了。

© . All rights reserved.