概述 New UWP Community Toolkit V2.2.0 的版本發佈日誌中提到了 RadialGauge 的調整,本篇我們結合代碼詳細講解 RadialGauge 的實現。 RadialGauge 是一種徑向儀錶盤控制項,使用圓盤面上的指針來顯示一定範圍的值,這種顯示和交互方式,讓數據可 ...
概述
New UWP Community Toolkit V2.2.0 的版本發佈日誌中提到了 RadialGauge 的調整,本篇我們結合代碼詳細講解 RadialGauge 的實現。
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)) { if (radialGauge.StepSize != 0) { radialGauge.Value = radialGauge.RoundToMultiple(radialGauge.Value, radialGauge.StepSize); } var middleOfScale = 100 - radialGauge.ScalePadding - (radialGauge.ScaleWidth / 2); 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; if (radialGauge.ValueAngle - radialGauge.NormalizedMinAngle == 360) { // Draw full circle. var eg = new EllipseGeometry(); eg.Center = new Point(100, 100); eg.RadiusX = 100 - radialGauge.ScalePadding - (radialGauge.ScaleWidth / 2); 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 > (180 + 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 = new Vector3(100 - ((float)radialGauge.TickWidth / 2), 0.0f, 0); tick.CenterPoint = new Vector3((float)radialGauge.TickWidth / 2, 100.0f, 0); 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 = new Vector3(100 - ((float)radialGauge.ScaleTickWidth / 2), (float)radialGauge.ScalePadding, 0); tick.CenterPoint = new Vector3((float)radialGauge.ScaleTickWidth / 2, 100 - (float)radialGauge.ScalePadding, 0); 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 = new Vector3((float)radialGauge.NeedleWidth / 2, (float)radialGauge.NeedleLength, 0); radialGauge._needle.Offset = new Vector3(100 - ((float)radialGauge.NeedleWidth / 2), 100 - (float)radialGauge.NeedleLength, 0); radialGauge._root.Children.InsertAtTop(radialGauge._needle); OnValueChanged(radialGauge); }
下麵來看一下 RadialGauge 的滑鼠點擊和觸摸手勢交互事件處理方法,主要處理邏輯在 SetGaugeValueFromPoint(point) 方法中:
首先計算出當前點擊或觸摸點相對比儀錶盤圓心的坐標,根據坐標計算出角度;再根據最大角度和最小角度的值,計算出可變化的實際區間;最後用當前角度與最小角度的差值,與實際區間做一個比例換算,得到當前角度對應在儀錶盤裡的數值;
private void SetGaugeValueFromPoint(Point p) { var pt = new Point(p.X - (ActualWidth / 2), -p.Y + (ActualHeight / 2)); var angle = Math.Atan2(pt.X, pt.Y) / Degrees2Radians; var divider = Mod(NormalizedMaxAngle - NormalizedMinAngle, 360); if (divider == 0) { divider = 360; } var value = Minimum + ((Maximum - Minimum) * Mod(angle - NormalizedMinAngle, 360) / 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!!!