概述

New UWP Community Toolkit  V2.2.0 的版本发布日志中提到了 RadialGauge 的调整,本篇我们结合代码详细讲解  RadialGauge 的实现。

RadialGauge 是一种径向仪表盘控件,使用圆盘面上的指针来显示一定范围的值,这种显示和交互方式,让数据可视化的表现力和吸引力都有很大提高。在实际应用中也有很广泛的使用,如时钟显示,数据展示,仪表盘模拟等等。我们来看一下官方的介绍和官网示例中的展示:

Source: https://github.com/Microsoft/UWPCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/RadialGauge

Doc: https://docs.microsoft.com/zh-cn/windows/uwpcommunitytoolkit/controls/radialgauge

Namespace: Microsoft.Toolkit.Uwp.UI.Controls; Nuget: Microsoft.Toolkit.Uwp.UI.Controls;

开发过程

代码分析

先来看看 RadialGauge 的结构组成:

  • RadialGauge.cs - RadialGauge 的控件定义和事件处理类
  • RadialGauge.xaml - RadialGauge 的样式文件

1. RadialGauge.xaml

RadialGauge 控件的样式文件,结合上面官方示例的显示图,我们看 Template 部分;主要由以下几个部分组成:

  • PART_Container - 底层容器,包含了下面三个控件部分
  • PART_Scale - 比例尺控件
  • PART_Trail - 仪表盘实际值显示控件
  • Value and Unit - 实际值文本和单位显示控件
<Style TargetType="local:RadialGauge">
    <Setter Property="UseSystemFocusVisuals" Value="True"></Setter>
    <Setter Property="Foreground" Value="{ThemeResource RadialGaugeForegroundBrush}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:RadialGauge">
                <Viewbox>
                    <Grid x:Name="PART_Container"
                        Width="200"
                        Height="200"
                        Background="Transparent">

                        <!--  Scale  -->
                        <Path Name="PART_Scale"
                            Stroke="{TemplateBinding ScaleBrush}"
                            StrokeThickness="{TemplateBinding ScaleWidth}" />

                        <!--  Trail  -->
                        <Path Name="PART_Trail"
                            Stroke="{TemplateBinding TrailBrush}"
                            StrokeThickness="{TemplateBinding ScaleWidth}" />

                        <!--  Value and Unit  -->
                        <StackPanel HorizontalAlignment="Center"
                                VerticalAlignment="Bottom">
                            <TextBlock Name="PART_ValueText"
                                    Margin="0,0,0,2"
                                    FontSize="20"
                                    FontWeight="SemiBold"
                                    Foreground="{TemplateBinding Foreground}"
                                    Text="{TemplateBinding Value}"
                                    TextAlignment="Center" />
                            <TextBlock Margin="0"
                                    FontSize="16"
                                    Foreground="{ThemeResource RadialGaugeAccentBrush}"
                                    Text="{TemplateBinding Unit}"
                                    TextAlignment="Center" />
                        </StackPanel>
                    </Grid>
                </Viewbox>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

2. RadialGauge.cs

我们先看看 RadialGauge 类的组成:

  

从上面第一张图中,我们可以看到 RadialGauge 注册了很多依赖属性,不一一列举了,大致分为几个类型:取值和角度属性,显示画刷属性,单位相关属性;属性也对应了修改时的回调事件,下面我们找出几个重点的事件处理方法来讲解:

① OnValueChanged(d)

在数值变化后,触发 OnValueChanged(d) 事件的方法;首先根据设置的取舍值,矫正当前的 Value,计算出对应的角度;给仪表盘的指针赋值,让指针指向当前角度;然后是给显示当前值区间的弧形赋值,如果当前角度值为 360,则整个填充仪表盘,否则根据角度计算出填充的区域,给 ArcSegment,PathFigure,PathGeometry 赋值;最后给仪表盘的数值文本控件赋值;

OnScaleChanged(d) 在刻度修改时触发,本质上讲,数值修改和刻度修改是相通的,所以处理方式也类似,这里不做赘述;

private static void OnValueChanged(DependencyObject d)
{
    RadialGauge radialGauge = (RadialGauge)d;
    if (!double.IsNaN(radialGauge.Value))
    {
        )
        {
            radialGauge.Value = radialGauge.RoundToMultiple(radialGauge.Value, radialGauge.StepSize);
        }

         - radialGauge.ScalePadding - (radialGauge.ScaleWidth / );
        var valueText = radialGauge.GetTemplateChild(ValueTextPartName) as TextBlock;
        radialGauge.ValueAngle = radialGauge.ValueToAngle(radialGauge.Value);

        // Needle
        if (radialGauge._needle != null)
        {
            radialGauge._needle.RotationAngleInDegrees = (float)radialGauge.ValueAngle;
        }

        // Trail
        var trail = radialGauge.GetTemplateChild(TrailPartName) as Path;
        if (trail != null)
        {
            if (radialGauge.ValueAngle > radialGauge.NormalizedMinAngle)
            {
                trail.Visibility = Visibility.Visible;

                )
                {
                    // Draw full circle.
                    var eg = new EllipseGeometry();
                    eg.Center = , );
                    eg.RadiusX =  - radialGauge.ScalePadding - (radialGauge.ScaleWidth / );
                    eg.RadiusY = eg.RadiusX;
                    trail.Data = eg;
                }
                else
                {
                    // Draw arc.
                    var pg = new PathGeometry();
                    var pf = new PathFigure();
                    pf.IsClosed = false;
                    pf.StartPoint = radialGauge.ScalePoint(radialGauge.NormalizedMinAngle, middleOfScale);
                    var seg = new ArcSegment();
                    seg.SweepDirection = SweepDirection.Clockwise;
                    seg.IsLargeArc = radialGauge.ValueAngle > ( + radialGauge.NormalizedMinAngle);
                    seg.Size = new Size(middleOfScale, middleOfScale);
                    seg.Point = radialGauge.ScalePoint(Math.Min(radialGauge.ValueAngle, radialGauge.NormalizedMaxAngle), middleOfScale);  // On overflow, stop trail at MaxAngle.
                    pf.Segments.Add(seg);
                    pg.Figures.Add(pf);
                    trail.Data = pg;
                }
            }
            else
            {
                trail.Visibility = Visibility.Collapsed;
            }
        }

        // Value Text
        if (valueText != null)
        {
            valueText.Text = radialGauge.Value.ToString(radialGauge.ValueStringFormat);
        }
    }
}

② OnFaceChanged(d)

任何外观有变化,或刻度值有变化时就会触发,控件整体的 UI 重绘;首先是 Ticks 重绘,然后是 Scale 重绘,后面是 Needle 的重绘,可以看到三种重绘的实现都很类似;最后是执行处理数值变化的方法;

private static void OnFaceChanged(DependencyObject d)
{
    RadialGauge radialGauge = (RadialGauge)d;

    var container = radialGauge.GetTemplateChild(ContainerPartName) as Grid;
    if (container == null || DesignTimeHelpers.IsRunningInLegacyDesignerMode)
    {
        // Bad template.
        return;
    }

    radialGauge._root = container.GetVisual();
    radialGauge._root.Children.RemoveAll();
    radialGauge._compositor = radialGauge._root.Compositor;

    // Ticks.
    SpriteVisual tick;
    for (double i = radialGauge.Minimum; i <= radialGauge.Maximum; i += radialGauge.TickSpacing)
    {
        tick = radialGauge._compositor.CreateSpriteVisual();
        tick.Size = new Vector2((float)radialGauge.TickWidth, (float)radialGauge.TickLength);
        tick.Brush = radialGauge._compositor.CreateColorBrush(radialGauge.TickBrush.Color);
        tick.Offset =  - ((), );
        tick.CenterPoint = , );
        tick.RotationAngleInDegrees = (float)radialGauge.ValueToAngle(i);
        radialGauge._root.Children.InsertAtTop(tick);
    }

    // Scale Ticks.
    for (double i = radialGauge.Minimum; i <= radialGauge.Maximum; i += radialGauge.TickSpacing)
    {
        tick = radialGauge._compositor.CreateSpriteVisual();
        tick.Size = new Vector2((float)radialGauge.ScaleTickWidth, (float)radialGauge.ScaleWidth);
        tick.Brush = radialGauge._compositor.CreateColorBrush(radialGauge.ScaleTickBrush.Color);
        tick.Offset =  - ((), ();
        tick.CenterPoint = ,  - ();
        tick.RotationAngleInDegrees = (float)radialGauge.ValueToAngle(i);
        radialGauge._root.Children.InsertAtTop(tick);
    }

    // Needle.
    radialGauge._needle = radialGauge._compositor.CreateSpriteVisual();
    radialGauge._needle.Size = new Vector2((float)radialGauge.NeedleWidth, (float)radialGauge.NeedleLength);
    radialGauge._needle.Brush = radialGauge._compositor.CreateColorBrush(radialGauge.NeedleBrush.Color);
    radialGauge._needle.CenterPoint = , ();
    radialGauge._needle.Offset =  - ((),  - ();
    radialGauge._root.Children.InsertAtTop(radialGauge._needle);

    OnValueChanged(radialGauge);
}

下面来看一下 RadialGauge 的鼠标点击和触摸手势交互事件处理方法,主要处理逻辑在 SetGaugeValueFromPoint(point) 方法中:

首先计算出当前点击或触摸点相对比仪表盘圆心的坐标,根据坐标计算出角度;再根据最大角度和最小角度的值,计算出可变化的实际区间;最后用当前角度与最小角度的差值,与实际区间做一个比例换算,得到当前角度对应在仪表盘里的数值;

private void SetGaugeValueFromPoint(Point p)
{
    ), -p.Y + (ActualHeight / ));

    var angle = Math.Atan2(pt.X, pt.Y) / Degrees2Radians;
    );
    )
    {
        divider = ;
    }

    ) / divider);
    if (value < Minimum || value > Maximum)
    {
        // Ignore positions outside the scale angle.
        return;
    }

    Value = value;
}

另外,RadialGauge 控件还支持键盘快捷键操作,当按下 Ctrl 键时,数值变化的幅度是正常变化的 5 倍;而当按下 Left 或 Right 键时,数值会变为最小值或最大值。

调用示例

我们给 RadialGauge 控件设置的范围是 0~180,当前值是 116;最小角度是 210,最大角度是 150;以及每个部分的颜色设置,可以从示例运行图中看出:

<controls:RadialGauge
        x:Name="RadialGauge"
        Grid.Column="1"
        Value="116"
        Minimum="0"
        Maximum="180"
        StepSize="1"
        IsInteractive="True"
        TickSpacing="18"
        ScaleWidth="8"
        MinAngle="210"
        MaxAngle="150"
        Unit="Units"
        TickBrush="LightGreen"
        ScaleTickBrush="LightBlue"
        ValueBrush="ForestGreen"
        NeedleBrush="ForestGreen"
        NeedleWidth="5"
        TickLength="18" />

总结

到这里我们就把 UWP Community Toolkit 中的 RadialGauge 控件的源代码实现过程和简单的调用示例讲解完成了,希望能对大家更好的理解和使用这个控件有所帮助。欢迎大家多多交流,谢谢!

最后,再跟大家安利一下 UWPCommunityToolkit 的官方微博:https://weibo.com/u/6506046490大家可以通过微博关注最新动态。

衷心感谢 UWPCommunityToolkit 的作者们杰出的工作,Thank you so much, UWPCommunityToolkit authors!!!

New UWP Community Toolkit - RadialGauge的更多相关文章

  1. New UWP Community Toolkit

    概述 UWP Community Toolkit 是一个 UWP App 自定义控件.应用服务和帮助方法的集合,能够很大程度的简化和指引开发者的开发工作,相信广大 UWPer 并不陌生. 下面是截取自 ...

  2. New UWP Community Toolkit - XAML Brushes

    概述 上一篇 New UWP Community Toolkit 文章中,我们对 V2.2.0 版本的重要更新做了简单回顾.接下来会针对每个重要更新,结合 SDK 源代码和调用代码详细讲解. 本篇我们 ...

  3. New UWP Community Toolkit - Markdown

    概述 前面 New UWP Community Toolkit 文章中,我们对 V2.2.0 版本的重要更新做了简单回顾,其中简单介绍了 MarkdownTextBlock 和 MarkdownDoc ...

  4. New UWP Community Toolkit - Staggered panel

    概述 前面 New UWP Community Toolkit 文章中,我们对 2.2.0 版本的重要更新做了简单回顾,其中简单介绍了 Staggered panel,本篇我们结合代码详细讲解  St ...

  5. New UWP Community Toolkit - Carousel

    概述 New UWP Community Toolkit  V2.2.0 的版本发布日志中提到了 Carousel 的调整,本篇我们结合代码详细讲解  Carousel 的实现. Carousel 是 ...

  6. New UWP Community Toolkit - RadialProgressBar

    概述 UWP Community Toolkit  中有一个圆形的进度条控件 - RadialProgressBar,本篇我们结合代码详细讲解  RadialProgressBar 的实现. Radi ...

  7. New UWP Community Toolkit - RangeSelector

    概述 前面 New UWP Community Toolkit 文章中,我们对 V2.2.0 版本的重要更新做了简单回顾,其中简单介绍了 RangeSelector,本篇我们结合代码详细讲解一下 Ra ...

  8. New UWP Community Toolkit - ImageEx

    概述 UWP Community Toolkit  中有一个图片的扩展控件 - ImageEx,本篇我们结合代码详细讲解  ImageEx 的实现. ImageEx 是一个图片的扩展控件,包括 Ima ...

  9. New UWP Community Toolkit - AdaptiveGridView

    概述 UWP Community Toolkit  中有一个自适应的 GridView 控件 - AdaptiveGridView,本篇我们结合代码详细讲解  AdaptiveGridView 的实现 ...

随机推荐

  1. touch.js下载使用方式

    touch.js下载地址 https://gitee.com/mirrors/touch-js Touch 在开发移动端的应用中会使用到很多的手势操作,例如一指拖动.两指旋转等等,为了方便开放者快速集 ...

  2. lightoj 1025 区间dp

    #include<bits/stdc++.h> using namespace std; typedef long long ll; char a[70]; ll dp[70][70]; ...

  3. Windows Developer Day Review

    北京时间 3 月 8 日凌晨 1 点钟,今年的第一次 Windows Developer Day 正式召开.    因为时间太晚看不了直播,我也是第二天早上在公司看的重播.整个会议过程有很多值得去研究 ...

  4. freemarker之list遍历(八)

    1.设置数据源 /** * * @Title:student * @Description: * @param:@param name * @return: void * @throws */ pri ...

  5. freemarker中的if...elseif...else语句(七)

    freemarker中的if...elseif...else语句 1.设计示例 <#if student.studentAge lt 12> ${student.studentName}不 ...

  6. 试着讲清楚:js代码运行机制

    一. js运行机制 js执行引擎 经常看文章的说到js是带线程的,其实这个说法非常的模糊,准确的是js执行引擎是单线程的,js执行引擎就是js代码的执行器,有了这个概念就可以下来说说js是如何运行的了 ...

  7. 创建文本节点createTextNode

    <!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content ...

  8. [HDU4812]D Tree

    vjudge 题意:给一棵树,每个点上有一个权值,求一条路径使得路径上权值的乘积膜\(10^6+3\)的结果为\(K\),输出路径的两个端点\(x,y\).如有多解,设\(x<y\),输出\(x ...

  9. 【BZOJ1717】产奶的模式(后缀数组)

    [BZOJ1717]产奶的模式(后缀数组) 题面 权限题 hihocoder 洛谷 题解 \(hihocoder\)里面讲的非常好了 这题要求的就是最长可重叠重复K次子串 所谓相同的子串 我们可以理解 ...

  10. 【BZOJ2006】超级钢琴(主席树,优先队列)

    [BZOJ2006]超级钢琴(主席树,优先队列) 题面 BZOJ 题解 既然是一段区间 首先就要变成单点 所以求一个前缀和 这个时候贪心很明显了: 枚举每一个点和可以和它组成一段的可行的点 全部丢进一 ...