实现一个雷达扫描图。
源代码在TK_King/雷达 (gitee.com),自行下载就好了
制作思路
首先我们使用自定义的控件。你可以使用vs自动添加,也可以手动创建类。注意手动创建时要创建Themes/Generic.xaml的文件路径哦。
控件继承自itemscontrol,取名叫做Radar。
我们第一步思考如何实现圆形或者轮,特别是等距的轮。
我们可以使用简单的itemscontrol的WPF控件,通过自定义ItemTemplate就可以简单的创建了。
因为要显示圆,所以使用Ellipse是最简单的事情。
又因为要在同一个区域内,显示同心圆,我们将面板改为Grid,利用叠加的特性去构造同心圆。
既然我们用了itemscontrol 来承载圈轮,直接让这个圈可自定义呢?
所以,我们构造一个集合依赖属性。关于集合依赖属性我们可以参加MSDN集合类型依赖属性 - WPF .NET | Microsoft Docs
/// <summary> /// 每圈的大小 /// </summary> public FreezableCollection<RadarSize> RadarCircle { get { return (FreezableCollection<RadarSize>)GetValue(RadarCircleProperty); } set { SetValue(RadarCircleProperty, value); } } /// <summary> /// 每圈的大小 /// </summary> public static readonly DependencyProperty RadarCircleProperty = DependencyProperty.Register("RadarCircle", typeof(FreezableCollection<RadarSize>), typeof(Radar), new PropertyMetadata(new PropertyChangedCallback(OnRadarCircelValueChanged)));
对应泛型类可以参考源代码,基本元素就是绑定ellipse的参数
<ItemsControl Grid.ColumnSpan="2" Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Center" x:Name="ic" ItemsSource="{TemplateBinding RadarCircle }"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Grid IsItemsHost="True"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <Ellipse Width="{Binding Width}" Height="{Binding Height}" Stroke="{Binding Color}"/> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>
哇啦,图像就出来了。
同理,我们创建分割线也是同样的过程。
对于分割线的切割算法,我们使用圆上点的坐标可以通过( rcos,rsin)=》(x,y) ,也就是极坐标。
关于此部分代码是放在布局块内ArrangeOverride,也可以放置在OnReader。
下面是局部代码,完整可以参考源代码
var angle = 180.0 / 6; circlesize = size.Height > size.Width ? size.Width : size.Height; RadarFillWidth = circlesize; var midx = circlesize / 2.0; var midy = circlesize / 2.0; circlesize = circlesize / 2; RadarRadius = circlesize; //默认为6个 for (int i = 0; i < 6; i++) { var baseangel = angle * i; var l1 = new Point(midx + circlesize * Math.Cos(Rad(baseangel)), midy - circlesize * Math.Sin(Rad(baseangel))); var half = baseangel + 180; var l2 = new Point(midx + circlesize * Math.Cos(Rad(half)), midy - circlesize * Math.Sin(Rad(half))); RadarLineSize radarLine = new RadarLineSize(); radarLine.Start = l1; radarLine.End = l2; radarLine.Color = RadarLineColor; RadarLine.Add(radarLine); } return size;
依赖属性
/// <summary> /// 雷达图的分割线,目前固定为6,可以自行修改 /// </summary> public FreezableCollection<RadarLineSize> RadarLine { get { return (FreezableCollection<RadarLineSize>)GetValue(RadarLineProperty); } set { SetValue(RadarLineProperty, value); } } /// <summary> /// 雷达图的分割线,目前固定为6,可以自行修改 /// </summary> public static readonly DependencyProperty RadarLineProperty = DependencyProperty.Register("RadarLine", typeof(FreezableCollection<RadarLineSize>), typeof(Radar));
xaml代码
<ItemsControl Grid.ColumnSpan="2" Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Center" x:Name="ic2" ItemsSource="{TemplateBinding RadarLine }"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Grid IsItemsHost="True"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <Line X1="{Binding Start.X}" Y1="{Binding Start.Y}" X2="{Binding End.X}" Y2="{Binding End.Y}" Stroke="{Binding Color}"/> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>
下一步就是扇形扫描了。
我们使用一个完整的圆,将其内部颜色填充为线性刷就可以得到一个效果不错的扫描了。
/// <summary> /// 雷达扫描的颜色 /// </summary> public Brush RadarColor { get { return (Brush)GetValue(RadarColorProperty); } set { SetValue(RadarColorProperty, value); } } /// <summary> /// 雷达扫描的颜色 /// </summary> public static readonly DependencyProperty RadarColorProperty = DependencyProperty.Register("RadarColor", typeof(Brush), typeof(Radar));
为了更好的定义这个圆,我们将radar的template使用grid面板等距分成四个区域(其实没啥用,主要是为了扇形扫描时做圆心选择的line,也可以不分成四个)。
在考虑动画,只需要做圆形360的选择就可以了。为了更好应用,我们创一个paly的依赖属性来播放动画。
/// <summary> /// 是否播放动画 /// </summary> public bool Play { get { return (bool)GetValue(PlayProperty); } set { SetValue(PlayProperty, value); } } /// <summary> /// 是否播放动画 /// </summary> public static readonly DependencyProperty PlayProperty = DependencyProperty.Register("Play", typeof(bool), typeof(Radar), new PropertyMetadata(false));
xaml代码( 部分)
<Style.Resources> <LinearGradientBrush x:Key="radarcolor" StartPoint="0,0" EndPoint="0,1"> <GradientStop Offset="0" Color="Lime" /> <GradientStop Offset="0.5" Color="Transparent" /> </LinearGradientBrush> </Style.Resources> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:Radar}"> <Grid x:Name="grid" > <Grid.RowDefinitions> <RowDefinition Height="2*"/> <RowDefinition Height="2*"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="2*"/> <ColumnDefinition Width="2*"/> </Grid.ColumnDefinitions> <ItemsControl Grid.ColumnSpan="2" Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Center" x:Name="ic" ItemsSource="{TemplateBinding RadarCircle }"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Grid IsItemsHost="True"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <Ellipse Width="{Binding Width}" Height="{Binding Height}" Stroke="{Binding Color}"/> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> <ItemsControl Grid.ColumnSpan="2" Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Center" x:Name="ic2" ItemsSource="{TemplateBinding RadarLine }"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Grid IsItemsHost="True"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <Line X1="{Binding Start.X}" Y1="{Binding Start.Y}" X2="{Binding End.X}" Y2="{Binding End.Y}" Stroke="{Binding Color}"/> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> <Ellipse Fill="{TemplateBinding RadarColor}" Grid.ColumnSpan="2" Grid.RowSpan="2" x:Name="ep" RenderTransformOrigin="0.5,0.5" Width="{TemplateBinding RadarFillWidth}" Height="{TemplateBinding RadarFillWidth}"> <Ellipse.RenderTransform> <RotateTransform x:Name="rtf" /> </Ellipse.RenderTransform> </Ellipse> </Grid> <ControlTemplate.Triggers> <Trigger Property="Play" Value="True"> <Trigger.EnterActions> <BeginStoryboard x:Name="bs" > <Storyboard > <DoubleAnimation Storyboard.TargetName="rtf" Storyboard.TargetProperty="Angle" From="0" To="360" Duration="0:0:2" RepeatBehavior="Forever"/> </Storyboard> </BeginStoryboard> </Trigger.EnterActions> </Trigger> <Trigger Property="Play" Value="False"> <Trigger.EnterActions> <RemoveStoryboard BeginStoryboardName="bs"/> </Trigger.EnterActions> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value>
效果
那么剩下就是扫描点的操作。
因为我们的控件是继承ItemsControl,我们到现在还没有利用ItemsSource这个属性。
所以我们要制作一个子控件来呈现扫描点。
由于子控件较为简单,只不过是一个圆而已。我们就让子控件继承Control就好了。
一切从简,我们不弄布局这一套了,直接在父控件中使用Canvas面板,子控件增加属性Left,Top这两个依赖属性。
重点说一下,子控件中存在一个linscar的方法,是为了将点如果在雷达外侧时,按照同角度缩放到最外层的方法。就是通过半径重新计算一边极坐标。
/// <summary> /// 线性缩放 /// </summary> /// <param name="size">半径</param> internal void LineScar(double size) { var midpoint = new Vector(size, size); var vp = new Vector(Left, Top); var sub = vp - midpoint; var angle = Vector.AngleBetween(sub, new Vector(size, 1)); angle = angle > 0 ? angle : angle + 360; //距离大于半径,根据半径重新绘制 if (sub.Length >= size) { Top = size - size * Math.Sin(Rad(angle)) - Width / 2; Left = size + size * Math.Cos(Rad(angle)) - Width / 2; } }
那么在父项中如何摆放呢?
我们刚才说父项使用canvas绘图,所以我们在radar中修改itempanel的面板属性,下面代码存在于父项xaml
<Setter Property="ItemsPanel"> <Setter.Value> <ItemsPanelTemplate> <Canvas IsItemsHost="True"/> </ItemsPanelTemplate> </Setter.Value> </Setter>
子项代码如下,比较少就贴了
xaml代码
<Style TargetType="local:RadarItem"> <Setter Property="VerticalAlignment" Value="Top" /> <Setter Property="HorizontalAlignment" Value="Left" /> <Setter Property="Padding" Value="0" /> <Setter Property="Margin" Value="0" /> <Setter Property="Canvas.Top" Value="{Binding RelativeSource={RelativeSource Mode=Self},Path=Top}" /> <Setter Property="Canvas.Left" Value="{Binding RelativeSource={RelativeSource Mode=Self},Path=Left}" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:RadarItem"> <Border > <Ellipse Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" Fill="{TemplateBinding Color}" /> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style>
radarItem
/// <summary> /// 雷达子项 /// </summary> public class RadarItem : Control { static RadarItem() { DefaultStyleKeyProperty.OverrideMetadata(typeof(RadarItem), new FrameworkPropertyMetadata(typeof(RadarItem))); } public RadarItem() { } /// <summary> /// 转弧度 /// </summary> /// <param name="val">角度</param> /// <returns>弧度制</returns> double Rad(double val) { return val * Math.PI / 180; } /// <summary> /// 线性缩放 /// </summary> /// <param name="size">半径</param> internal void LineScar(double size) { var midpoint = new Vector(size, size); var vp = new Vector(Left, Top); var sub = vp - midpoint; var angle = Vector.AngleBetween(sub, new Vector(size, 1)); angle = angle > 0 ? angle : angle + 360; //距离大于半径,根据半径重新绘制 if (sub.Length >= size) { Top = size - size * Math.Sin(Rad(angle)) - Width / 2; Left = size + size * Math.Cos(Rad(angle)) - Width / 2; } } /// <summary> /// 顶部距离,用canvas.top绘制 /// </summary> public double Top { get { return (double)GetValue(TopProperty); } set { SetValue(TopProperty, value); } } /// <summary> /// 顶部距离,用canvas.top绘制 /// </summary> public static readonly DependencyProperty TopProperty = DependencyProperty.Register("Top", typeof(double), typeof(RadarItem), new PropertyMetadata(0.0)); /// <summary> /// 左侧距离,用于canvas.left绘制 /// </summary> public double Left { get { return (double)GetValue(LeftProperty); } set { SetValue(LeftProperty, value); } } /// <summary> /// 左侧距离,用于canvas.left绘制 /// </summary> public static readonly DependencyProperty LeftProperty = DependencyProperty.Register("Left", typeof(double), typeof(RadarItem), new PropertyMetadata(0.0)); /// <summary> /// 填充颜色 /// </summary> public Brush Color { get { return (Brush)GetValue(ColorProperty); } set { SetValue(ColorProperty, value); } } /// <summary> /// 填充颜色 /// </summary> public static readonly DependencyProperty ColorProperty = DependencyProperty.Register("Color", typeof(Brush), typeof(RadarItem), new PropertyMetadata(new SolidColorBrush(Colors.Red))); }
于是乎我们就得到了一个雷达扫描图